Skip to content

Commit 42b5388

Browse files
authored
[ADR-0006] Phase 2: Tree-Only Mode (Hide Timeline) (#3562)
## Problem The trace timeline view always shows the timeline bar column (ticks, span bars, minimap viewing layer, column resizer). Users working primarily with the span hierarchy — analyzing service dependencies or call patterns — have no way to reclaim that horizontal space for the service/operation name tree. ## Changes ### Tree-only mode When the "Show Timeline" toggle in the trace view settings menu is turned off, the timeline bar column is hidden entirely and the service/operation name column expands to fill the available width: - **`SpanBarRow`** — new `timelineBarsVisible` prop; span-view cell (ticks + `SpanBar`) is omitted; name column expands to full width via `effectiveColumnDivision`. - **`SpanDetailRow`** — new `timelineBarsVisible` prop; left accent cell (`SpanTreeOffset` + expanded-accent switch) is omitted entirely when bars are hidden (avoids zero-width cell overflow); detail content cell expands to full width. - **`TimelineHeaderRow`** — new `timelineBarsVisible` prop; ticks, `TimelineViewingLayer`, and `VerticalResizer` are omitted; title/collapser cell expands to full width. The Redux-stored `spanNameColumnWidth` is left unchanged so the column ratio restores correctly when bars are re-shown. ### Consistent naming throughout the component tree The Redux state field was renamed `timelineVisible` → `timelineBarsVisible` (`TTraceTimeline`, `duck.ts`, all components and tests). The localStorage key `'timelineVisible'` is kept unchanged for backward compatibility with stored user preferences; comments in `duck.ts` document this intentional divergence. The Redux action and action creator were also renamed for consistency: `SET_TIMELINE_VISIBLE` → `SET_TIMELINE_BARS_VISIBLE`, `setTimelineVisible` → `setTimelineBarsVisible`. ## Testing - Added `describe('tree-only mode')` blocks to `SpanBarRow.test.js`, `SpanDetailRow.test.js`, and `TimelineHeaderRow.test.js` covering: - Name column expands to full width (`width={1}`) - Timeline cell / accent cell is not rendered - Ticks, viewing layer, and resizer are not rendered - All existing tests updated to pass the new required `timelineBarsVisible` prop. - `duck.test.js` updated to use the renamed action creator (`actions.setTimelineBarsVisible`). - `TracePage/index.test.js` updated for the renamed state field and dispatch prop. ## AI Usage in this PR (choose one) See [AI Usage Policy](https://github.com/jaegertracing/jaeger/blob/main/CONTRIBUTING_GUIDELINES.md#ai-usage-policy). - [ ] **None**: No AI tools were used in creating this PR - [ ] **Light**: AI provided minor assistance (formatting, simple suggestions) - [ ] **Moderate**: AI helped with code generation or debugging specific parts - [x] **Heavy**: AI generated most or all of the code changes --------- Signed-off-by: Yuri Shkuro <github@ysh.us>
1 parent 93ad38c commit 42b5388

File tree

17 files changed

+178
-100
lines changed

17 files changed

+178
-100
lines changed

packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe('<TracePageHeader>', () => {
112112
showViewOptions: false,
113113
slimView: false,
114114
textFilter: '',
115-
timelineVisible: true,
115+
timelineBarsVisible: true,
116116
toSearch: null,
117117
viewType: ETraceViewType.TraceTimelineViewer,
118118
updateNextViewRangeTime: jest.fn(),

packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type TracePageHeaderEmbedProps = {
5050
showViewOptions: boolean;
5151
slimView: boolean;
5252
textFilter: string | TNil;
53-
timelineVisible: boolean;
53+
timelineBarsVisible: boolean;
5454
toSearch: string | null;
5555
trace: IOtelTrace;
5656
viewType: ETraceViewType;
@@ -143,7 +143,7 @@ export function TracePageHeaderFn(props: TracePageHeaderEmbedProps & { forwarded
143143
disableJsonView,
144144
slimView,
145145
textFilter,
146-
timelineVisible,
146+
timelineBarsVisible,
147147
toSearch,
148148
trace,
149149
viewType,
@@ -215,7 +215,7 @@ export function TracePageHeaderFn(props: TracePageHeaderEmbedProps & { forwarded
215215
enableSidePanel={enableSidePanel}
216216
onDetailPanelModeToggle={onDetailPanelModeToggle}
217217
onTimelineToggle={onTimelineToggle}
218-
timelineVisible={timelineVisible}
218+
timelineBarsVisible={timelineBarsVisible}
219219
/>
220220
{showViewOptions && (
221221
<AltViewOptions

packages/jaeger-ui/src/components/TracePage/TracePageHeader/TraceViewSettings.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const defaultProps = {
1616
enableSidePanel: false,
1717
onDetailPanelModeToggle: jest.fn(),
1818
onTimelineToggle: jest.fn(),
19-
timelineVisible: true,
19+
timelineBarsVisible: true,
2020
};
2121

2222
describe('<TraceViewSettings>', () => {

packages/jaeger-ui/src/components/TracePage/TracePageHeader/TraceViewSettings.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type Props = {
1616
enableSidePanel: boolean;
1717
onDetailPanelModeToggle: () => void;
1818
onTimelineToggle: () => void;
19-
timelineVisible: boolean;
19+
timelineBarsVisible: boolean;
2020
};
2121

2222
const CHECK_STYLE = { marginRight: 8, fontSize: 14 };
@@ -29,15 +29,15 @@ export default function TraceViewSettings(props: Props) {
2929
enableSidePanel,
3030
onDetailPanelModeToggle,
3131
onTimelineToggle,
32-
timelineVisible,
32+
timelineBarsVisible,
3333
} = props;
3434

3535
const [kbdModalVisible, setKbdModalVisible] = React.useState(false);
3636

3737
const items: MenuProps['items'] = [
3838
{
3939
key: 'timeline',
40-
icon: timelineVisible ? <IoCheckmark style={CHECK_STYLE} /> : CHECK_PLACEHOLDER,
40+
icon: timelineBarsVisible ? <IoCheckmark style={CHECK_STYLE} /> : CHECK_PLACEHOLDER,
4141
label: 'Show Timeline',
4242
onClick: onTimelineToggle,
4343
},

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe('<SpanBarRow>', () => {
5151
isChildrenExpanded: true,
5252
isDetailExpanded: false,
5353
isMatchingFilter: false,
54+
timelineBarsVisible: true,
5455
onDetailToggled: jest.fn(),
5556
onChildrenToggled: jest.fn(),
5657
numTicks: 5,
@@ -212,6 +213,20 @@ describe('<SpanBarRow>', () => {
212213
expect(svcName).toHaveClass('span-svc-name', 'is-children-collapsed');
213214
});
214215

216+
describe('tree-only mode (timelineBarsVisible=false)', () => {
217+
it('does not render the span-view cell', () => {
218+
render(<SpanBarRow {...defaultProps} timelineBarsVisible={false} />);
219+
expect(screen.queryByTestId('span-bar')).not.toBeInTheDocument();
220+
});
221+
222+
it('renders the span name column at full width', () => {
223+
const { container } = render(<SpanBarRow {...defaultProps} timelineBarsVisible={false} />);
224+
const nameCell = container.querySelector('.span-name-column');
225+
expect(nameCell).toHaveStyle('flex-basis: 100%');
226+
expect(nameCell).toHaveStyle('max-width: 100%');
227+
});
228+
});
229+
215230
it('sets longLabel and hintSide to right when viewStart <= 1 - viewEnd', () => {
216231
const getViewedBounds = jest.fn().mockReturnValue({ start: 0.2, end: 0.3 });
217232
const props = {

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.tsx

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type SpanBarRowProps = {
2424
isChildrenExpanded: boolean;
2525
isDetailExpanded: boolean;
2626
isMatchingFilter: boolean;
27+
timelineBarsVisible: boolean;
2728
onDetailToggled: (spanID: string) => void;
2829
onChildrenToggled: (spanID: string) => void;
2930
numTicks: number;
@@ -68,6 +69,7 @@ const SpanBarRow: React.FC<SpanBarRowProps> = ({
6869
isChildrenExpanded,
6970
isDetailExpanded,
7071
isMatchingFilter,
72+
timelineBarsVisible,
7173
numTicks,
7274
rpc = null,
7375
noInstrumentedServer,
@@ -117,6 +119,8 @@ const SpanBarRow: React.FC<SpanBarRowProps> = ({
117119
const hasLinks = span.links && span.links.length > 0;
118120
const hasInboundLinks = span.inboundLinks && span.inboundLinks.length > 0;
119121

122+
const effectiveColumnDivision = timelineBarsVisible ? columnDivision : 1;
123+
120124
return (
121125
<TimelineRow
122126
className={`
@@ -126,7 +130,7 @@ const SpanBarRow: React.FC<SpanBarRowProps> = ({
126130
${isMatchingFilter ? 'is-matching-filter' : ''}
127131
`}
128132
>
129-
<TimelineRow.Cell className="span-name-column" width={columnDivision}>
133+
<TimelineRow.Cell className="span-name-column" width={effectiveColumnDivision}>
130134
<div className={`span-name-wrapper ${isMatchingFilter ? 'is-matching-filter' : ''}`}>
131135
<SpanTreeOffset
132136
childrenVisible={isChildrenExpanded}
@@ -193,29 +197,31 @@ const SpanBarRow: React.FC<SpanBarRowProps> = ({
193197
)}
194198
</div>
195199
</TimelineRow.Cell>
196-
<TimelineRow.Cell
197-
className="span-view"
198-
style={{ cursor: 'pointer' }}
199-
width={1 - columnDivision}
200-
onClick={_detailToggle}
201-
>
202-
<Ticks numTicks={numTicks} />
203-
<SpanBar
204-
criticalPath={criticalPath}
205-
rpc={rpc}
206-
viewStart={viewStart}
207-
viewEnd={viewEnd}
208-
getViewedBounds={getViewedBounds}
209-
color={color}
210-
shortLabel={label}
211-
longLabel={longLabel}
212-
hintSide={hintSide}
213-
traceStartTime={traceStartTime}
214-
span={span}
215-
traceDuration={traceDuration}
216-
useOtelTerms={useOtelTerms}
217-
/>
218-
</TimelineRow.Cell>
200+
{timelineBarsVisible && (
201+
<TimelineRow.Cell
202+
className="span-view"
203+
style={{ cursor: 'pointer' }}
204+
width={1 - columnDivision}
205+
onClick={_detailToggle}
206+
>
207+
<Ticks numTicks={numTicks} />
208+
<SpanBar
209+
criticalPath={criticalPath}
210+
rpc={rpc}
211+
viewStart={viewStart}
212+
viewEnd={viewEnd}
213+
getViewedBounds={getViewedBounds}
214+
color={color}
215+
shortLabel={label}
216+
longLabel={longLabel}
217+
hintSide={hintSide}
218+
traceStartTime={traceStartTime}
219+
span={span}
220+
traceDuration={traceDuration}
221+
useOtelTerms={useOtelTerms}
222+
/>
223+
</TimelineRow.Cell>
224+
)}
219225
</TimelineRow>
220226
);
221227
};

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ describe('<SpanDetailRow>', () => {
4141
const props = {
4242
color: 'some-color',
4343
columnDivision: 0.5,
44+
timelineBarsVisible: true,
4445
detailState: new DetailState(),
4546
onDetailToggled: jest.fn(),
4647
linksGetter: jest.fn(),
@@ -123,6 +124,17 @@ describe('<SpanDetailRow>', () => {
123124
expect(receivedProps.traceStartTime).toBe(props.traceStartTime);
124125
});
125126

127+
describe('tree-only mode (timelineBarsVisible=false)', () => {
128+
it('renders the SpanDetail at full width', () => {
129+
const { container } = render(<SpanDetailRow {...props} timelineBarsVisible={false} />);
130+
const cells = container.querySelectorAll('[style*="flex-basis"]');
131+
// left cell = 0%, right cell (detail) = 100%
132+
const fullWidthCell = Array.from(cells).find(el => el.style.flexBasis === '100%');
133+
expect(fullWidthCell).toBeTruthy();
134+
expect(fullWidthCell.querySelector('[data-testid="mocked-span-detail"]')).toBeInTheDocument();
135+
});
136+
});
137+
126138
it('adds span when calling linksGetter', () => {
127139
render(<SpanDetailRow {...props} />);
128140
expect(MockSpanDetail).toHaveBeenCalled();

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import './SpanDetailRow.css';
1616
type SpanDetailRowProps = {
1717
color: string;
1818
columnDivision: number;
19+
timelineBarsVisible: boolean;
1920
detailState: DetailState;
2021
onDetailToggled: (spanID: string) => void;
2122
linksGetter: (attributes: ReadonlyArray<IAttribute>, index: number) => Hyperlink[];
@@ -41,6 +42,7 @@ const SpanDetailRow = React.memo((props: SpanDetailRowProps) => {
4142
const {
4243
color,
4344
columnDivision,
45+
timelineBarsVisible,
4446
detailState,
4547
eventsToggle,
4648
resourceToggle,
@@ -58,19 +60,21 @@ const SpanDetailRow = React.memo((props: SpanDetailRowProps) => {
5860
} = props;
5961
return (
6062
<TimelineRow className="detail-row">
61-
<TimelineRow.Cell width={columnDivision}>
62-
<SpanTreeOffset span={span} showChildrenIcon={false} color={color} />
63-
<span>
64-
<span
65-
className="detail-row-expanded-accent"
66-
aria-checked="true"
67-
onClick={_detailToggle}
68-
role="switch"
69-
style={{ borderColor: color }}
70-
/>
71-
</span>
72-
</TimelineRow.Cell>
73-
<TimelineRow.Cell width={1 - columnDivision}>
63+
{timelineBarsVisible && (
64+
<TimelineRow.Cell width={columnDivision}>
65+
<SpanTreeOffset span={span} showChildrenIcon={false} color={color} />
66+
<span>
67+
<span
68+
className="detail-row-expanded-accent"
69+
aria-checked="true"
70+
onClick={_detailToggle}
71+
role="switch"
72+
style={{ borderColor: color }}
73+
/>
74+
</span>
75+
</TimelineRow.Cell>
76+
)}
77+
<TimelineRow.Cell width={timelineBarsVisible ? 1 - columnDivision : 1}>
7478
<div className="detail-info-wrapper" style={{ borderTopColor: color }}>
7579
<SpanDetail
7680
detailState={detailState}

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ describe('<TimelineHeaderRow>', () => {
6969
onColummWidthChange: jest.fn(),
7070
onExpandAll: jest.fn(),
7171
onExpandOne: jest.fn(),
72+
timelineBarsVisible: true,
7273
updateNextViewRangeTime: jest.fn(),
7374
updateViewRangeTime: jest.fn(),
7475
viewRangeTime: {
@@ -129,4 +130,28 @@ describe('<TimelineHeaderRow>', () => {
129130
render(<TimelineHeaderRow {...props} />);
130131
expect(screen.getByTestId('timeline-collapser')).toBeInTheDocument();
131132
});
133+
134+
describe('tree-only mode (timelineBarsVisible=false)', () => {
135+
it('does not render the Ticks', () => {
136+
render(<TimelineHeaderRow {...props} timelineBarsVisible={false} />);
137+
expect(screen.queryByTestId('ticks')).not.toBeInTheDocument();
138+
});
139+
140+
it('does not render the TimelineViewingLayer', () => {
141+
render(<TimelineHeaderRow {...props} timelineBarsVisible={false} />);
142+
expect(screen.queryByTestId('timeline-viewing-layer')).not.toBeInTheDocument();
143+
});
144+
145+
it('does not render the VerticalResizer', () => {
146+
render(<TimelineHeaderRow {...props} timelineBarsVisible={false} />);
147+
expect(screen.queryByTestId('vertical-resizer')).not.toBeInTheDocument();
148+
});
149+
150+
it('renders the name column at full width', () => {
151+
const { container } = render(<TimelineHeaderRow {...props} timelineBarsVisible={false} />);
152+
const cells = container.querySelectorAll('.TimelineRow--cellMock');
153+
expect(cells).toHaveLength(1);
154+
expect(cells[0]).toHaveAttribute('data-width', '1');
155+
});
156+
});
132157
});

packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type TimelineHeaderRowProps = {
2222
onColummWidthChange: (width: number) => void;
2323
onExpandAll: () => void;
2424
onExpandOne: () => void;
25+
timelineBarsVisible: boolean;
2526
updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void;
2627
updateViewRangeTime: TUpdateViewRangeTimeFunction;
2728
viewRangeTime: IViewRangeTime;
@@ -38,16 +39,18 @@ export default function TimelineHeaderRow(props: TimelineHeaderRowProps) {
3839
onColummWidthChange,
3940
onExpandAll,
4041
onExpandOne,
42+
timelineBarsVisible,
4143
updateViewRangeTime,
4244
updateNextViewRangeTime,
4345
viewRangeTime,
4446
} = props;
4547
const [viewStart, viewEnd] = viewRangeTime.current;
4648
const startTime = (viewStart * duration) as IOtelSpan['startTime'];
4749
const endTime = (viewEnd * duration) as IOtelSpan['endTime'];
50+
const effectiveNameColumnWidth = timelineBarsVisible ? nameColumnWidth : 1;
4851
return (
4952
<TimelineRow className="TimelineHeaderRow">
50-
<TimelineRow.Cell className="ub-flex ub-px2" width={nameColumnWidth}>
53+
<TimelineRow.Cell className="ub-flex ub-px2" width={effectiveNameColumnWidth}>
5154
<h3 className="TimelineHeaderRow--title">
5255
Service &amp; {props.useOtelTerms ? 'Span Name' : 'Operation'}
5356
</h3>
@@ -58,16 +61,20 @@ export default function TimelineHeaderRow(props: TimelineHeaderRowProps) {
5861
onExpandOne={onExpandOne}
5962
/>
6063
</TimelineRow.Cell>
61-
<TimelineRow.Cell width={1 - nameColumnWidth}>
62-
<TimelineViewingLayer
63-
boundsInvalidator={nameColumnWidth}
64-
updateNextViewRangeTime={updateNextViewRangeTime}
65-
updateViewRangeTime={updateViewRangeTime}
66-
viewRangeTime={viewRangeTime}
67-
/>
68-
<Ticks numTicks={numTicks} startTime={startTime} endTime={endTime} showLabels />
69-
</TimelineRow.Cell>
70-
<VerticalResizer position={nameColumnWidth} onChange={onColummWidthChange} min={0.15} max={0.85} />
64+
{timelineBarsVisible && (
65+
<>
66+
<TimelineRow.Cell width={1 - nameColumnWidth}>
67+
<TimelineViewingLayer
68+
boundsInvalidator={nameColumnWidth}
69+
updateNextViewRangeTime={updateNextViewRangeTime}
70+
updateViewRangeTime={updateViewRangeTime}
71+
viewRangeTime={viewRangeTime}
72+
/>
73+
<Ticks numTicks={numTicks} startTime={startTime} endTime={endTime} showLabels />
74+
</TimelineRow.Cell>
75+
<VerticalResizer position={nameColumnWidth} onChange={onColummWidthChange} min={0.15} max={0.85} />
76+
</>
77+
)}
7178
</TimelineRow>
7279
);
7380
}

0 commit comments

Comments
 (0)