Status: Proposed Date: 2026-03-04
The Jaeger trace timeline view currently displays span details by expanding them in-place below the span bar row. When a user clicks a span, a detail row is inserted into the virtualized list showing accordion sections for Attributes, Resource, Events, Links, and Warnings.
This inline expansion was a significant UX improvement over the old Zipkin UI modal dialog -- it keeps the timeline visible and allows multiple spans to be expanded simultaneously. However, it has notable limitations compared to the approach taken by many modern/vendor tracing UIs:
- Limited real estate: Detail content is constrained in height, attributes are shown as collapsed summary lines rather than full tables.
- Scroll displacement: Expanding a detail row pushes subsequent spans down, causing the user to lose their position in the overall trace. With large traces, this is disorienting.
- No independent scrolling: The detail content scrolls with the trace timeline. A user examining a span's attributes cannot simultaneously keep an eye on the trace structure.
- Height estimation: The virtualized
ListViewuses fixed height estimates (161px / 197px) for detail rows, which don't reflect actual content height and cause layout jitter.
Many modern tracing UIs address these issues by showing span details in a right-side panel that scrolls independently of the trace view. While this limits display to one span at a time, the trade-offs (more space for details, independent scrolling, stable trace layout) are compelling for many workflows.
Additionally, users working primarily with the span hierarchy (e.g., analyzing service dependencies or call patterns) have no way to hide the timeline bars to reclaim horizontal space for service/operation names.
TracePage (index.tsx)
TracePageHeader (view mode switcher, search, slim view toggle)
TraceTimelineViewer (index.tsx)
TimelineHeaderRow (column headers + VerticalResizer divider)
VirtualizedTraceView (Redux-connected virtualized span list)
ListView (custom virtual scroller, window-scroll mode)
SpanBarRow (left: service tree, right: timeline bar)
SpanDetailRow (inline detail, inserted when span expanded)
SpanDetail (Attributes, Resource, Events, Links, Warnings accordions)
Key state in Redux (TTraceTimeline via duck.ts):
detailStates: Map<string, DetailState>-- which spans have details expanded, and accordion open/close state per spanchildrenHiddenIDs: Set<string>-- collapsed parent spans in the treespanNameColumnWidth: number-- ratio (0-1) for the left/right column split, persisted to localStorage
The two-column layout uses flexbox with TimelineRow.Cell components. VerticalResizer provides drag-to-resize between columns (min 0.15, max 0.85, default 0.25).
Row generation in VirtualizedTraceView.generateRowStates() iterates through spans and inserts {isDetail: true} rows for any span present in the detailStates Map.
Introduce two independent, optional layout enhancements:
- Side panel mode: When enabled, clicking a span shows its details in a fixed right-side panel instead of expanding inline. Only one span's details are shown at a time. The panel scrolls independently.
- Tree-only mode: When enabled, the timeline bar column is hidden entirely, and the service/operation hierarchy fills the available width.
These are independent boolean toggles, yielding four valid layout combinations:
| Detail Mode | Timeline Bars | Description |
|---|---|---|
| Inline (default) | Visible | Current behavior, no changes |
| Inline | Hidden | Tree-only with inline expand |
| Side panel | Visible | Timeline visible, detail in right panel |
| Side panel | Hidden | Tree-only with detail in right panel |
The current inline behavior remains the default. Users opt in to the new layouts via toggle controls in the timeline header.
Side panel mode is shipped as an experimental feature behind a config flag. Two new options are added to the Config type (types/config.ts) and default-config.ts:
// in types/config.ts, under the existing Config type
// traceTimeline controls the trace timeline viewer layout options.
traceTimeline?: {
// enableSidePanel enables the side panel layout option in the trace timeline.
// When false, the side panel toggle is hidden and only inline detail mode is available.
// Default: false (experimental, opt-in).
enableSidePanel?: boolean;
// defaultDetailPanelMode sets the initial detail panel mode when enableSidePanel is true.
// 'inline' preserves the current behavior as the default.
// 'sidepanel' makes the side panel the default experience.
// Users can still toggle between modes at runtime; this only controls the initial state.
// Default: 'inline'.
defaultDetailPanelMode?: 'inline' | 'sidepanel';
};Default config in default-config.ts:
traceTimeline: {
enableSidePanel: false,
defaultDetailPanelMode: 'inline',
},The tree-only mode toggle (hide timeline bars) does not require a feature flag since it is a simpler, lower-risk enhancement that does not change the detail viewing paradigm.
When enableSidePanel is false:
- The side panel toggle icon is not rendered in the
TimelineHeaderRow - The Redux initial state always uses
detailPanelMode: 'inline' - Any
detailPanelModevalue in localStorage is ignored
When enableSidePanel is true:
- The side panel toggle icon appears in the
TimelineHeaderRow - The Redux initial state reads
detailPanelModefrom localStorage, falling back todefaultDetailPanelModefrom config - Users can switch between inline and side panel modes at runtime
The config flows through the existing pattern: useConfig() hook in TracePage → prop to TraceTimelineViewer → prop to TimelineHeaderRow and VirtualizedTraceView.
The layout toggles are infrequent global settings, not navigation controls, so they belong at the page header level rather than in the TimelineHeaderRow (which would become too crowded, and where the expand/collapse icons are logically scoped to tree navigation).
The TracePageHeader title row currently has this layout:
[← Back] [ExternalLinks] [▶ Title] [SearchBar] [⌘] [View ▾] [Archive] [↗]
The [⌘] button (KeyboardShortcutsHelp component) is replaced with a Settings gear icon (IoSettingsOutline from react-icons/io5) that opens an antd Dropdown menu. The dropdown contains:
┌──────────────────────────┐
│ ✓ Show Timeline │ ← toggles timelineBarsVisible
│ Show Details in Panel │ ← toggles detailPanelMode (only when enableSidePanel config is true)
│ ──────────────────────── │
│ Keyboard Shortcuts │ ← opens the existing KeyboardShortcutsHelp modal
└──────────────────────────┘
Menu item details:
- "Show Timeline" -- checkmark when
timelineBarsVisible === true. Clicking toggles the value. Always present. - "Show Details in Panel" -- checkmark when
detailPanelMode === 'sidepanel'. Clicking toggles between'inline'and'sidepanel'. Only rendered whenenableSidePanelconfig is true. - Divider -- antd menu divider separating layout settings from other items.
- "Keyboard Shortcuts" -- clicking opens the existing
KeyboardShortcutsHelpmodal (reuse thegetHelpModal()function and modal state). No checkmark.
Implementation approach:
The new component TraceViewSettings (or inline in TracePageHeader) replaces KeyboardShortcutsHelp in the title row. It follows the same Dropdown + Button pattern as AltViewOptions:
<Dropdown menu={{ items: settingsItems }} trigger={['click']}>
<Button className="TraceViewSettings">
<IoSettingsOutline />
</Button>
</Dropdown>The checkmark pattern uses antd's menu item API -- items with a check icon prefix (e.g., IoCheckmark from react-icons/io5) when active, or empty space when inactive. This matches common dropdown toggle patterns.
Props needed by TraceViewSettings:
timelineBarsVisible: booleandetailPanelMode: 'inline' | 'sidepanel'enableSidePanel: booleanonTimelineBarsToggle: () => voidonDetailPanelModeToggle: () => void
These are dispatched via Redux from the TraceTimelineViewer duck, wired through TracePage → TracePageHeader → TraceViewSettings.
// Added to TTraceTimeline
detailPanelMode: 'inline' | 'sidepanel'; // default: 'inline'
timelineBarsVisible: boolean; // default: true
sidePanelWidth: number; // ratio 0-1, default: 0.45No new state is needed for tracking which span is selected -- the existing detailStates: Map<string, DetailState> serves this role in both modes. In side panel mode, the map has at most one entry (enforced by the reducer). The side panel reads the single entry from detailStates to determine which span to display.
Layout preferences (detailPanelMode, timelineBarsVisible, sidePanelWidth) are persisted to localStorage following the existing spanNameColumnWidth pattern in duck.ts.
The side panel lives outside the virtualized ListView, as a sibling element in a flex container:
TraceTimelineViewer (flex row)
TraceTimelineViewerMain (flex: 1 - sidePanelWidth)
TimelineHeaderRow
VirtualizedTraceView (with ListView, window-scroll mode)
VerticalResizer (reused from components/common/)
SpanDetailSidePanel (flex: sidePanelWidth, overflow-y: auto)
generateRowStates() becomes mode-aware. In side panel mode, it skips inserting {isDetail: true} rows entirely. The memoized function receives detailPanelMode as an additional parameter to invalidate correctly.
When timelineBarsVisible === false, components use effectiveColumnWidth = 1.0 locally in the render path. The Redux-stored spanNameColumnWidth is left untouched so it restores correctly when bars are re-shown.
The existing DETAIL_TOGGLE(spanID) action is reused for both modes. The span row click always dispatches the same action -- it does not need to know how details will be displayed. The reducer handles the action differently based on the current detailPanelMode:
- Inline mode (current behavior, unchanged): toggles the span's entry in
detailStatesMap. Multiple spans can be expanded simultaneously. - Side panel mode: if the span is already in
detailStates, removes it (closing the panel). Otherwise, clearsdetailStatesand adds a single entry for the clicked span. This enforces the one-span-at-a-time constraint using the same data structure.
All existing sub-actions (DETAIL_TAGS_TOGGLE, DETAIL_LOGS_TOGGLE, etc.) work unchanged in both modes -- they operate on a DetailState entry in the map by spanID, regardless of how many entries the map has.
This keeps the component tree (SpanBarRow, VirtualizedTraceView) completely mode-agnostic. The detailPanelMode only affects:
- The
DETAIL_TOGGLEreducer (single vs. multiple entries indetailStates) generateRowStates(skips inline detail rows in sidepanel mode)TraceTimelineViewerrendering (conditionally renders the side panel)
- Users get more horizontal space for span detail content (attributes as full tables, etc.)
- Independent scrolling means examining details doesn't lose trace context
- The trace layout stays stable when selecting different spans (no row insertion/removal)
- Tree-only mode gives maximum space for service/operation hierarchy navigation
- All four layout combinations serve valid use cases
- Fully backward compatible: existing inline behavior is unchanged and remains the default
- Side panel shows only one span at a time (inline mode allows multiple). This is acceptable because the panel provides a better single-span experience, and inline mode remains available.
TraceTimelineViewertakes on additional layout responsibility as a flex container.- The
generateRowStatesmemoization gains a new parameter, adding a cache invalidation dimension.
- Comparison mode (showing two spans' details side by side) -- could be a future enhancement
Wire up config options, state plumbing, and UI toggle icons with no rendering changes.
Files to modify:
types/config.ts-- addtraceTimeline?: { enableSidePanel?: boolean; defaultDetailPanelMode?: 'inline' | 'sidepanel' }toConfigtypeconstants/default-config.ts-- addtraceTimeline: { enableSidePanel: false, defaultDetailPanelMode: 'inline' }defaultstypes/TTraceTimeline.ts-- adddetailPanelMode,timelineBarsVisible,sidePanelWidthTraceTimelineViewer/duck.ts-- new actions (SET_DETAIL_PANEL_MODE,SET_TIMELINE_BARS_VISIBLE,SET_SIDE_PANEL_WIDTH), reducers, localStorage persistence; modifyDETAIL_TOGGLEreducer to enforce single-entry constraint ondetailStateswhen in sidepanel mode;newInitialState()reads config fordefaultDetailPanelModeand respectsenableSidePanelflagTracePageHeader/TraceViewSettings.tsx(new) -- settings gear dropdown replacingKeyboardShortcutsHelpbutton; contains "Show Timeline" toggle, "Show Details in Panel" toggle (gated byenableSidePanel), and "Keyboard Shortcuts" menu itemTracePageHeader/TracePageHeader.tsx-- replace<KeyboardShortcutsHelp>with<TraceViewSettings>; wire new propsTraceTimelineViewer/index.tsx-- wire new Redux state/dispatchTracePage/index.tsx-- readconfig.traceTimeline, connect layout state from Redux, pass toTracePageHeader
Outcome: Toggle controls visible (side panel toggle gated by config), preferences persist to localStorage, no visual layout changes.
Ship independently of the side panel.
Files to modify:
TraceTimelineViewer/SpanBarRow.tsx-- new proptimelineBarsVisible; when false, render only name cell at full widthTraceTimelineViewer/SpanBarRow.css-- full-width stylingTraceTimelineViewer/SpanDetailRow.tsx-- full-width detail when bars hiddenTraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx-- hide ticks, viewing layer,VerticalResizerTraceTimelineViewer/VirtualizedTraceView.tsx-- read and passtimelineBarsVisibleTraceTimelineViewer/index.tsx-- pass through to children
Outcome: Users can toggle timeline bars on/off. Inline expansion still works. Column width preference preserved.
Core side panel functionality.
Files to create:
TraceTimelineViewer/SpanDetailSidePanel/index.tsx-- wraps existingSpanDetailcomponent, independent scrollTraceTimelineViewer/SpanDetailSidePanel/index.css-- panel styling
Files to modify:
TraceTimelineViewer/index.tsx-- flex layout, conditional panel +VerticalResizerrenderingTraceTimelineViewer/VirtualizedTraceView.tsx--generateRowStatesskips detail rows in sidepanel mode (no changes torenderSpanBarRow-- it still dispatchesdetailToggleas before)TraceTimelineViewer/SpanBarRow.tsx--isSelectedprop for visual highlight (derived fromdetailStatesinVirtualizedTraceView)
Outcome: Clicking a span in side panel mode shows details in right panel. Independent scrolling. Adjustable width.
- Close button in panel header (dispatches
DETAIL_TOGGLEfor the displayed span, removing it fromdetailStates) - Next/prev span navigation in panel header
- Keyboard shortcuts for panel navigation (
TracePage/keyboard-shortcuts.ts) - Mode-switching transition logic: switching from inline to sidepanel keeps only the first entry in
detailStates(if any); switching from sidepanel to inline keeps the current entry (user can then expand additional spans)
- Verify all four layout combinations
- Analytics tracking for layout mode changes
- Responsive guardrails (min panel width 0.2, max 0.7)
- Embedded mode compatibility
- Performance verification with large traces (10K+ spans)
| File | Role |
|---|---|
packages/jaeger-ui/src/types/config.ts |
Config type with traceTimeline feature flag |
packages/jaeger-ui/src/constants/default-config.ts |
Default config values |
packages/jaeger-ui/src/types/TTraceTimeline.ts |
Redux state type definitions |
packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.ts |
Redux actions, reducers, localStorage |
packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.tsx |
Container layout, panel rendering |
packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx |
Mode-aware row generation |
packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.tsx |
Tree-only mode, selection highlight |
packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.tsx |
Full-width in tree-only mode |
packages/jaeger-ui/src/components/TracePage/TracePageHeader/TraceViewSettings.tsx |
Settings gear dropdown (new, replaces KeyboardShortcutsHelp) |
packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.tsx |
Page header, hosts settings dropdown |
packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx |
Reused in side panel |
packages/jaeger-ui/src/components/common/VerticalResizer.tsx |
Reused as-is for panel divider |