Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/adr/0006-side-panel-span-details.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,11 @@ Core side panel functionality.

**Outcome:** Clicking a span in side panel mode shows details in right panel. Independent scrolling. Adjustable width.

### Phase 4: Side Panel Polish and Integration
### Phase 4: Side Panel Polish

- Close button in panel header (dispatches `DETAIL_TOGGLE` for the displayed span, removing it from `detailStates`)
- 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)
- When a span is shown in the side panel, its Attributes and Resource sections are expanded by default, taking advantage of the extra screen real estate the panel provides. The inline detail view is unaffected.
- The side panel shows up to 10 events/logs by default before requiring "show more", compared to 3 in the inline view.
- Switching from inline to side panel mode keeps at most one span selected (the first currently expanded span, if any). Switching back to inline mode preserves the selected span so the user can then expand additional spans alongside it.

### Phase 5: Combined Modes, Analytics, Final Polish

Expand All @@ -297,5 +296,6 @@ Core side panel functionality.
| `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/TracePage/TraceTimelineViewer/SpanDetail/index.tsx` | Reused in side panel; `eventsInitialVisibleCount` prop for side panel tuning |
| `packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.ts` | Expansion state; `forDetailPanelMode(mode)` factory for mode-specific defaults |
| `packages/jaeger-ui/src/components/common/VerticalResizer.tsx` | Reused as-is for panel divider |
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2026 The Jaeger Authors.
// SPDX-License-Identifier: Apache-2.0

import DetailState from './DetailState';

describe('DetailState', () => {
describe('constructor defaults', () => {
it('starts with attributes, resource, warnings, and links closed', () => {
const state = new DetailState();
expect(state.isAttributesOpen).toBe(false);
expect(state.isResourceOpen).toBe(false);
expect(state.isWarningsOpen).toBe(false);
expect(state.isLinksOpen).toBe(false);
});

it('starts with events open and no opened items', () => {
const state = new DetailState();
expect(state.events.isOpen).toBe(true);
expect(state.events.openedItems.size).toBe(0);
});
});

describe('forDetailPanelMode()', () => {
it('expands attributes and resource for sidepanel mode', () => {
const state = DetailState.forDetailPanelMode('sidepanel');
expect(state.isAttributesOpen).toBe(true);
expect(state.isResourceOpen).toBe(true);
});

it('leaves warnings, links, and events at standard defaults for sidepanel mode', () => {
const state = DetailState.forDetailPanelMode('sidepanel');
expect(state.isWarningsOpen).toBe(false);
expect(state.isLinksOpen).toBe(false);
expect(state.events.isOpen).toBe(true);
expect(state.events.openedItems.size).toBe(0);
});

it('returns collapsed defaults for inline mode', () => {
const state = DetailState.forDetailPanelMode('inline');
expect(state.isAttributesOpen).toBe(false);
expect(state.isResourceOpen).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { IEvent } from '../../../../types/otel';
import type { SpanDetailPanelMode } from '../../../../types/config';

/**
* Which items of a {@link SpanDetail} component are expanded.
Expand Down Expand Up @@ -72,6 +73,18 @@ export default class DetailState {
return next;
}

// Returns the appropriate default DetailState for the given panel mode.
// In side-panel mode, attribute sections are expanded by default to take
// advantage of the extra screen real estate the panel provides.
static forDetailPanelMode(mode: SpanDetailPanelMode): DetailState {
const state = new DetailState();
if (mode === 'sidepanel') {
state.isAttributesOpen = true;
state.isResourceOpen = true;
}
return state;
}

// Legacy method names for backward compatibility
toggleTags() {
return this.toggleAttributes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type SpanDetailProps = {
currentViewRangeTime: [number, number];
traceDuration: number;
useOtelTerms: boolean;
eventsInitialVisibleCount?: number;
};

export default function SpanDetail(props: SpanDetailProps) {
Expand All @@ -53,6 +54,7 @@ export default function SpanDetail(props: SpanDetailProps) {
currentViewRangeTime,
traceDuration,
useOtelTerms,
eventsInitialVisibleCount,
} = props;

const { isAttributesOpen, isResourceOpen, events: eventsState, isWarningsOpen, isLinksOpen } = detailState;
Expand Down Expand Up @@ -129,6 +131,7 @@ export default function SpanDetail(props: SpanDetailProps) {
traceDuration={traceDuration}
spanID={span.spanID}
useOtelTerms={useOtelTerms}
initialVisibleCount={eventsInitialVisibleCount}
/>
)}
{warnings && warnings.length > 0 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import { SpanDetailSidePanelImpl } from './index';
import SpanDetail from '../SpanDetail';
import DetailState from '../SpanDetail/DetailState';
import traceGenerator from '../../../../demo/trace-generators';
import transformTraceData from '../../../../model/transform-trace-data';
Expand Down Expand Up @@ -81,4 +82,17 @@ describe('<SpanDetailSidePanelImpl>', () => {
fireEvent.click(screen.getByTestId('focus-span-button'));
expect(baseProps.focusUiFindMatches).toHaveBeenCalledWith(baseProps.trace, 'test-find', false);
});

it('passes eventsInitialVisibleCount={10} to SpanDetail', () => {
render(<SpanDetailSidePanelImpl {...baseProps} />);
const props = SpanDetail.mock.lastCall[0];
expect(props.eventsInitialVisibleCount).toBe(10);
});

it('passes a side-panel default detailState with attributes and resource expanded when no span is selected', () => {
render(<SpanDetailSidePanelImpl {...baseProps} />);
const { detailState } = SpanDetail.mock.lastCall[0];
expect(detailState.isAttributesOpen).toBe(true);
expect(detailState.isResourceOpen).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function SpanDetailSidePanelImpl(props: TProps) {
const spanID = getSelectedSpanID(detailStates) ?? trace.rootSpans?.[0]?.spanID;
if (!spanID) return null;

const detailState = detailStates.get(spanID) ?? new DetailState();
const detailState = detailStates.get(spanID) ?? DetailState.forDetailPanelMode('sidepanel');
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In side-panel mode, if detailStates already contains an entry created while in inline mode (e.g., user expanded a span inline and then switched to side panel), this code will reuse that existing detailState as-is. That means Attributes/Resource may remain collapsed, which conflicts with the Phase 4 requirement that spans shown in the side panel have those sections expanded by default. Consider normalizing the retained/selected DetailState on mode switch (in the reducer) or when rendering in the side panel (e.g., merge/apply side-panel defaults when the existing state matches inline defaults).

Suggested change
const detailState = detailStates.get(spanID) ?? DetailState.forDetailPanelMode('sidepanel');
const detailState = DetailState.forDetailPanelMode('sidepanel');

Copilot uses AI. Check for mistakes.
const span = trace.spanMap.get(spanID);
if (!span) return null;

Expand All @@ -102,6 +102,7 @@ export function SpanDetailSidePanelImpl(props: TProps) {
currentViewRangeTime={currentViewRangeTime}
traceDuration={trace.duration}
useOtelTerms={useOtelTerms}
eventsInitialVisibleCount={10}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,22 @@ describe('TraceTimelineViewer/duck', () => {
expect(store.getState().detailStates.has(spanB)).toBe(true);
expect(store.getState().detailStates.has(spanA)).toBe(false);
});

it('creates a DetailState with attributes and resource expanded by default', () => {
store.dispatch(actions.detailToggle(spanA));
const detailState = store.getState().detailStates.get(spanA);
expect(detailState.isAttributesOpen).toBe(true);
expect(detailState.isResourceOpen).toBe(true);
});

it('auto-initializes with side-panel defaults when a subsection is toggled for an unknown span', () => {
store.dispatch(actions.detailTagsToggle(spanA));
const detailState = store.getState().detailStates.get(spanA);
// toggled from the sidepanel default (true), so should now be false
expect(detailState.isAttributesOpen).toBe(false);
// resource was not toggled, so it retains the sidepanel default
expect(detailState.isResourceOpen).toBe(true);
});
});

describe('setDetailPanelMode', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import DetailState from './SpanDetail/DetailState';
import { TNil } from '../../../types';
import { IOtelSpan, IOtelTrace, IEvent } from '../../../types/otel';
import TTraceTimeline from '../../../types/TTraceTimeline';
import type { SpanDetailPanelMode } from '../../../types/config';
import filterSpans from '../../../utils/filter-spans';
import generateActionTypes from '../../../utils/generate-action-types';
import getConfig from '../../../utils/config/get-config';
Expand All @@ -26,7 +27,7 @@ export type TSpanIdValue = { spanID: string };
type TSpansValue = { spans: IOtelSpan[] };
type TTraceUiFindValue = { trace: IOtelTrace; uiFind: string | TNil; allowHide?: boolean };
export type TWidthValue = { width: number };
export type TDetailPanelModeValue = { mode: 'inline' | 'sidepanel' };
export type TDetailPanelModeValue = { mode: SpanDetailPanelMode };
export type TTimelineVisibleValue = { visible: boolean };
export type TActionTypes =
| TSpanIdLogValue
Expand Down Expand Up @@ -329,7 +330,7 @@ function detailToggle(state: TTraceTimeline, { spanID }: TSpanIdValue) {
return { ...state, detailStates: new Map() };
}
const detailStates = new Map<string, DetailState>();
detailStates.set(spanID, new DetailState());
detailStates.set(spanID, DetailState.forDetailPanelMode('sidepanel'));
return { ...state, detailStates };
}
// Inline mode: toggle as before, multiple spans can be expanded.
Expand Down Expand Up @@ -390,7 +391,7 @@ function detailSubsectionToggle(
state: TTraceTimeline,
{ spanID }: TSpanIdValue
) {
const old = state.detailStates.get(spanID) ?? new DetailState();
const old = state.detailStates.get(spanID) ?? DetailState.forDetailPanelMode(state.detailPanelMode);
let detailState;
if (subSection === 'tags') {
detailState = old.toggleTags();
Expand All @@ -415,7 +416,7 @@ const detailWarningsToggle = detailSubsectionToggle.bind(null, 'warnings');
const detailReferencesToggle = detailSubsectionToggle.bind(null, 'references');

function detailLogItemToggle(state: TTraceTimeline, { spanID, logItem }: TSpanIdLogValue) {
const old = state.detailStates.get(spanID) ?? new DetailState();
const old = state.detailStates.get(spanID) ?? DetailState.forDetailPanelMode(state.detailPanelMode);
const detailState = old.toggleLogItem(logItem);
const detailStates = new Map(state.detailStates);
detailStates.set(spanID, detailState);
Expand Down
6 changes: 3 additions & 3 deletions packages/jaeger-ui/src/components/TracePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import TraceStatistics from './TraceStatistics/index';
import TraceSpanView from './TraceSpanView/index';
import TraceFlamegraph from './TraceFlamegraph/index';
import TraceLogsView from './TraceLogsView/index';
import { StorageCapabilities, TraceGraphConfig } from '../../types/config';
import type { SpanDetailPanelMode, StorageCapabilities, TraceGraphConfig } from '../../types/config';

import './index.css';
import memoizedTraceCriticalPath from './CriticalPath/index';
Expand All @@ -59,7 +59,7 @@ type TDispatchProps = {
archiveTrace: (id: string) => void;
fetchTrace: (id: string) => void;
focusUiFindMatches: (trace: IOtelTrace, uiFind: string | TNil) => void;
setDetailPanelMode: (mode: 'inline' | 'sidepanel') => void;
setDetailPanelMode: (mode: SpanDetailPanelMode) => void;
setTimelineBarsVisible: (visible: boolean) => void;
};

Expand All @@ -78,7 +78,7 @@ type TOwnProps = {

type TReduxProps = {
archiveTraceState: TraceArchive | TNil;
detailPanelMode: 'inline' | 'sidepanel';
detailPanelMode: SpanDetailPanelMode;
embedded: null | EmbeddedState;
id: string;
searchUrl: null | string;
Expand Down
3 changes: 2 additions & 1 deletion packages/jaeger-ui/src/types/TTraceTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

import DetailState from '../components/TracePage/TraceTimelineViewer/SpanDetail/DetailState';
import TNil from './TNil';
import type { SpanDetailPanelMode } from './config';

type TTraceTimeline = {
childrenHiddenIDs: Set<string>;
detailStates: Map<string, DetailState>;
detailPanelMode: 'inline' | 'sidepanel';
detailPanelMode: SpanDetailPanelMode;
hoverIndentGuideIds: Set<string>;
shouldScrollToFirstUiFindMatch: boolean;
sidePanelWidth: number;
Expand Down
4 changes: 3 additions & 1 deletion packages/jaeger-ui/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { TPathAgnosticDecorationSchema } from '../model/path-agnostic-decoration
import { IWebAnalyticsFunc } from './tracking';
import { TNil } from '.';

export type SpanDetailPanelMode = 'inline' | 'sidepanel';

export type ConfigMenuItem = {
label: string;
url?: string;
Expand Down Expand Up @@ -216,7 +218,7 @@ export type Config = {
// 'inline' preserves the current behavior as the default.
// 'sidepanel' makes the side panel the default experience for new users.
// Default: 'inline'.
defaultDetailPanelMode?: 'inline' | 'sidepanel';
defaultDetailPanelMode?: SpanDetailPanelMode;
};

// useOpenTelemetryTerms determines whether the UI uses legacy Jaeger terminology
Expand Down