From 47ce65815473fc255121ac8257c5d5ac044e509e Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:33:27 +0100 Subject: [PATCH] Trace view: Improved span details view (#108101) * Details view improvents * Update tests * Label update * Prettier * Improve span tree offset --- .../SpanDetail/AccordianKeyValues.test.tsx | 4 +- .../SpanDetail/AccordianKeyValues.tsx | 8 +- .../SpanDetail/AccordianLogs.test.tsx | 8 +- .../SpanDetail/AccordianLogs.tsx | 16 ++- .../TraceTimelineViewer/SpanDetail/index.tsx | 18 +-- .../SpanDetailRow.test.tsx | 17 --- .../TraceTimelineViewer/SpanDetailRow.tsx | 113 +++++++++--------- .../TimelineHeaderRow/TimelineCollapser.tsx | 89 +++++++++----- .../components/common/LabeledList.tsx | 32 ++++- 9 files changed, 167 insertions(+), 138 deletions(-) diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx index 09ceafa6391..597b6d125c6 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx @@ -96,9 +96,7 @@ describe('AccordianKeyValues test', () => { it('renders the summary instead of the table when it is not expanded', () => { setupAccordian({ isOpen: false } as AccordianKeyValuesProps); - expect( - screen.getByRole('switch', { name: 'test accordian: span.kind = client omg = mos-def' }) - ).toBeInTheDocument(); + expect(screen.getByRole('switch', { name: 'test accordian: span.kind client omg mos-def' })).toBeInTheDocument(); expect(screen.queryByRole('table')).not.toBeInTheDocument(); expect(screen.queryAllByRole('cell')).toHaveLength(0); }); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx index b182fa5df41..d8d621cf218 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx @@ -68,7 +68,6 @@ export const getStyles = (theme: GrafanaTheme2) => { label: 'summaryItem', display: 'inline', paddingRight: '0.5rem', - borderRight: `1px solid ${autoColor(theme, '#ddd')}`, '&:last-child': { paddingRight: 0, borderRight: 'none', @@ -77,11 +76,7 @@ export const getStyles = (theme: GrafanaTheme2) => { summaryLabel: css({ label: 'summaryLabel', color: autoColor(theme, '#777'), - }), - summaryDelim: css({ - label: 'summaryDelim', - color: autoColor(theme, '#bbb'), - padding: '0 0.2em', + paddingRight: '0.5rem', }), }; }; @@ -116,7 +111,6 @@ export function KeyValuesSummary({ data = null }: KeyValuesSummaryProps) { // `i` is necessary in the key because item.key can repeat
  • {item.key} - = {String(item.value)}
  • ))} diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx index f69f3c8808f..18085f3fdcb 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx @@ -56,7 +56,7 @@ describe('AccordianLogs tests', () => { it('shows the number of log entries', () => { setup(); - expect(screen.getByRole('switch', { name: 'Events (2)' })).toBeInTheDocument(); + expect(screen.getByRole('switch', { name: 'Events 2' })).toBeInTheDocument(); }); it('hides log entries when not expanded', () => { @@ -103,12 +103,10 @@ describe('AccordianLogs tests', () => { setup({ isOpen: true, openedItems: new Set() } as AccordianLogsProps); expect( screen.getByRole('switch', { - name: '15μs (foo event name) : message = oh the next log message more = stuff', + name: '15μs (foo event name) : message oh the next log message more stuff', }) ).toBeInTheDocument(); - expect( - screen.getByRole('switch', { name: '5μs: message = oh the log message something = else' }) - ).toBeInTheDocument(); + expect(screen.getByRole('switch', { name: '5μs: message oh the log message something else' })).toBeInTheDocument(); }); it('renders event name and duration when events list is open', () => { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx index 1de9660cbb6..5584760b04b 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx @@ -18,7 +18,7 @@ import * as React from 'react'; import { GrafanaTheme2, TraceLog } from '@grafana/data'; import { Trans } from '@grafana/i18n'; -import { Icon, useStyles2 } from '@grafana/ui'; +import { Counter, Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; import { formatDuration } from '../utils'; @@ -31,24 +31,22 @@ const getStyles = (theme: GrafanaTheme2) => { return { AccordianLogs: css({ label: 'AccordianLogs', - border: `1px solid ${autoColor(theme, '#d8d8d8')}`, position: 'relative', marginBottom: '0.25rem', }), AccordianLogsHeader: css({ label: 'AccordianLogsHeader', - background: autoColor(theme, '#e4e4e4'), color: 'inherit', - display: 'block', - padding: '0.25rem 0.5rem', + display: 'flex', + alignItems: 'center', + padding: '0.25rem 0.1em', '&:hover': { - background: autoColor(theme, '#dadada'), + background: autoColor(theme, '#e8e8e8'), }, }), AccordianLogsContent: css({ label: 'AccordianLogsContent', background: autoColor(theme, '#f0f0f0'), - borderTop: `1px solid ${autoColor(theme, '#d8d8d8')}`, padding: '0.5rem 0.5rem 0.25rem 0.5rem', }), AccordianLogsFooter: css({ @@ -90,7 +88,7 @@ export default function AccordianLogs({ arrow = isOpen ? ( ) : ( - + ); HeaderComponent = 'a'; headerProps = { @@ -108,7 +106,7 @@ export default function AccordianLogs({ Events {' '} - ({logs.length}) + {isOpen && (
    diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index 0b8065b09c6..b9735b03720 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -28,12 +28,13 @@ import { TraceLog, PluginExtensionResourceAttributesContext, PluginExtensionPoints, + IconName, } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; import { usePluginLinks } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; -import { Divider, Icon, TextArea, useStyles2 } from '@grafana/ui'; +import { Icon, TextArea, useStyles2 } from '@grafana/ui'; import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; import { autoColor } from '../../Theme'; @@ -159,9 +160,6 @@ const getStyles = (theme: GrafanaTheme2) => { label: 'AccordianWarningsLabel', color: autoColor(theme, '#d36c08'), }), - AccordianKeyValuesItem: css({ - marginBottom: theme.spacing(0.5), - }), Textarea: css({ wordBreak: 'break-all', whiteSpace: 'pre', @@ -173,6 +171,7 @@ const getStyles = (theme: GrafanaTheme2) => { display: 'flex', flexWrap: 'wrap', gap: '10px', + marginBottom: theme.spacing(2), }), }; }; @@ -186,6 +185,7 @@ export type TraceFlameGraphs = { }; export type SpanDetailProps = { + color: string; detailState: DetailState; logItemToggle: (spanID: string, log: TraceLog) => void; logsToggle: (spanID: string) => void; @@ -215,6 +215,7 @@ export type SpanDetailProps = { export default function SpanDetail(props: SpanDetailProps) { const { + color, detailState, logItemToggle, logsToggle, @@ -263,6 +264,9 @@ export default function SpanDetail(props: SpanDetailProps) { } = span; const { timeZone } = props; + const durationIcon: IconName = 'hourglass'; + const startIcon: IconName = 'clock-nine'; + let overviewItems = [ { key: 'svc', @@ -273,11 +277,13 @@ export default function SpanDetail(props: SpanDetailProps) { key: 'duration', label: t('explore.span-detail.overview-items.label.duration', 'Duration:'), value: formatDuration(duration), + icon: durationIcon, }, { key: 'start', label: t('explore.span-detail.overview-items.label.start-time', 'Start Time:'), value: formatDuration(relativeStartTime) + getAbsoluteTime(startTime, timeZone), + icon: startIcon, }, ...(span.childSpanCount > 0 ? [ @@ -353,11 +359,10 @@ export default function SpanDetail(props: SpanDetailProps) { {operationName}
    - +
    {linksComponent}
    -
    {process.tags && ( { expect(() => setup()).not.toThrow(); }); - it('calls toggle on click', async () => { - const mockToggle = jest.fn(); - setup({ onDetailToggled: mockToggle } as unknown as SpanDetailRowProps); - expect(mockToggle).not.toHaveBeenCalled(); - - const detailRow = screen.getByTestId('detail-row-expanded-accent'); - await userEvent.click(detailRow); - expect(mockToggle).toHaveBeenCalled(); - }); - - it('renders the span tree offset', () => { - setup(); - - expect(screen.getByTestId('SpanTreeOffset--indentGuide')).toBeInTheDocument(); - }); - it('renders the SpanDetail', () => { setup(); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx index a6e8219214d..201f0570b27 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx @@ -13,15 +13,13 @@ // limitations under the License. import { css } from '@emotion/css'; -import classNames from 'classnames'; import { PureComponent } from 'react'; import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceLog } from '@grafana/data'; import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; import { TimeZone } from '@grafana/schema'; -import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui'; +import { stylesFactory, withTheme2 } from '@grafana/ui'; -import { autoColor } from '../Theme'; import { SpanLinkFunc } from '../types/links'; import { TraceSpan, TraceSpanReference } from '../types/trace'; @@ -64,10 +62,22 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { }), infoWrapper: css({ label: 'infoWrapper', - border: `1px solid ${autoColor(theme, '#d3d3d3')}`, - borderTop: '3px solid', padding: '0.75rem', }), + cell: css({ + label: 'cell', + display: 'flex !important', + width: '100% !important', + }), + indentSpacer: css({ + label: 'indentSpacer', + flex: 'none', + }), + detailWrapper: css({ + label: 'detailWrapper', + flex: '1', + minWidth: 0, + }), }; }); @@ -115,7 +125,6 @@ export class UnthemedSpanDetailRow extends PureComponent { render() { const { color, - columnDivision, detailState, logItemToggle, logsToggle, @@ -131,72 +140,68 @@ export class UnthemedSpanDetailRow extends PureComponent { traceStartTime, traceDuration, traceName, - hoverIndentGuideIds, - addHoverIndentGuideId, - removeHoverIndentGuideId, theme, createSpanLink, focusedSpanId, createFocusSpanLink, datasourceType, datasourceUid, - visibleSpanIds, traceFlameGraphs, setTraceFlameGraphs, setRedrawListView, timeRange, app, + hoverIndentGuideIds, + addHoverIndentGuideId, + removeHoverIndentGuideId, + visibleSpanIds, } = this.props; const styles = getStyles(theme); return ( - - - - - -
    - +
    +
    +
    +
    + +
    +
    ); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.tsx index 5ad66c10762..1d8b3c356ff 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.tsx @@ -14,10 +14,11 @@ import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { IconButton, useStyles2 } from '@grafana/ui'; +import { Button, useStyles2 } from '@grafana/ui'; -const getStyles = () => ({ +const getStyles = (theme: GrafanaTheme2) => ({ TimelineCollapser: css({ alignItems: 'center', display: 'flex', @@ -25,6 +26,16 @@ const getStyles = () => ({ justifyContent: 'center', marginRight: '0.5rem', }), + buttonsContainer: css({ + display: 'flex', + flexDirection: 'row', + gap: '0.5rem', + paddingRight: theme.spacing(1), + }), + buttonContainer: css({ + display: 'flex', + alignItems: 'center', + }), }); type CollapserProps = { @@ -40,34 +51,52 @@ export function TimelineCollapser(props: CollapserProps) { return (
    - - - - +
    +
    +
    +
    +
    +
    ); } diff --git a/public/app/features/explore/TraceView/components/common/LabeledList.tsx b/public/app/features/explore/TraceView/components/common/LabeledList.tsx index 8b8156168de..b45e49b4cfa 100644 --- a/public/app/features/explore/TraceView/components/common/LabeledList.tsx +++ b/public/app/features/explore/TraceView/components/common/LabeledList.tsx @@ -16,8 +16,8 @@ import { css } from '@emotion/css'; import cx from 'classnames'; import * as React from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; +import { GrafanaTheme2, IconName } from '@grafana/data'; +import { Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../Theme'; @@ -45,7 +45,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => { borderRight: `1px solid ${autoColor(theme, '#ddd')}`, padding: '0 8px', } - : {}), + : { padding: '0 4px' }), }), LabeledListLabel: css({ label: 'LabeledListLabel', @@ -56,24 +56,44 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => { label: 'LabeledListValue', marginRight: divider ? undefined : '0.55rem', }), + LabeledListIcon: css({ + label: 'LabeledListIcon', + marginRight: '0.25rem', + marginTop: '-0.1rem', + }), + LabeledListServiceLine: css({ + label: 'LabeledListServiceLine', + display: 'inline-block', + width: '1.25rem', + height: '0.35rem', + marginRight: '0.5rem', + verticalAlign: 'middle', + borderRadius: theme.shape.radius.default, + }), }; }; type LabeledListProps = { className?: string; divider?: boolean; - items: Array<{ key: string; label: React.ReactNode; value: React.ReactNode }>; + items: Array<{ key: string; label: React.ReactNode; value: React.ReactNode; icon?: IconName }>; + color?: string; }; export default function LabeledList(props: LabeledListProps) { - const { className, divider = false, items } = props; + const { className, divider = false, items, color } = props; const styles = useStyles2(getStyles(divider)); return (
      - {items.map(({ key, label, value }) => { + {items.map(({ key, label, value, icon }) => { return ( + // If label is service, create small line on left with color
    • + {label === 'Service:' && ( +
      + )} + {icon && } {label} {value}