@workflow/web-shared 5.0.0-beta.3 → 5.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -1
  2. package/dist/components/new-trace-viewer/components/event-list.js +7 -4
  3. package/dist/components/new-trace-viewer/components/event-list.js.map +1 -1
  4. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
  5. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
  6. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
  7. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js.map +1 -0
  8. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
  9. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
  10. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
  11. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js.map +1 -0
  12. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
  13. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
  14. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
  15. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js.map +1 -0
  16. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
  17. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
  18. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
  19. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js.map +1 -0
  20. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -1
  21. package/dist/components/new-trace-viewer/components/split-pane.js +2 -3
  22. package/dist/components/new-trace-viewer/components/split-pane.js.map +1 -1
  23. package/dist/components/new-trace-viewer/components/timeline.d.ts +10 -11
  24. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
  25. package/dist/components/new-trace-viewer/components/timeline.js +57 -53
  26. package/dist/components/new-trace-viewer/components/timeline.js.map +1 -1
  27. package/dist/components/new-trace-viewer/context.d.ts.map +1 -1
  28. package/dist/components/new-trace-viewer/context.js +5 -7
  29. package/dist/components/new-trace-viewer/context.js.map +1 -1
  30. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -1
  31. package/dist/components/new-trace-viewer/detail-panel.js +2 -1
  32. package/dist/components/new-trace-viewer/detail-panel.js.map +1 -1
  33. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
  34. package/dist/components/new-trace-viewer/trace-viewer.js +37 -17
  35. package/dist/components/new-trace-viewer/trace-viewer.js.map +1 -1
  36. package/dist/components/new-trace-viewer/utils.d.ts +3 -8
  37. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
  38. package/dist/components/new-trace-viewer/utils.js +12 -65
  39. package/dist/components/new-trace-viewer/utils.js.map +1 -1
  40. package/dist/components/trace-viewer/util/timing.d.ts +1 -1
  41. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  42. package/dist/components/trace-viewer/util/timing.js +5 -8
  43. package/dist/components/trace-viewer/util/timing.js.map +1 -1
  44. package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
  45. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  46. package/dist/components/workflow-traces/trace-span-construction.js +16 -3
  47. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  48. package/dist/lib/trace-builder.d.ts.map +1 -1
  49. package/dist/lib/trace-builder.js +2 -1
  50. package/dist/lib/trace-builder.js.map +1 -1
  51. package/dist/lib/utils.d.ts +9 -7
  52. package/dist/lib/utils.d.ts.map +1 -1
  53. package/dist/lib/utils.js +27 -27
  54. package/dist/lib/utils.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/components/new-trace-viewer/components/event-list.tsx +27 -20
  57. package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
  58. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
  59. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
  60. package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
  61. package/src/components/new-trace-viewer/components/split-pane.tsx +2 -3
  62. package/src/components/new-trace-viewer/components/timeline.tsx +119 -115
  63. package/src/components/new-trace-viewer/context.tsx +7 -9
  64. package/src/components/new-trace-viewer/detail-panel.tsx +2 -1
  65. package/src/components/new-trace-viewer/trace-viewer.tsx +120 -60
  66. package/src/components/new-trace-viewer/utils.ts +19 -93
  67. package/src/components/trace-viewer/util/timing.ts +5 -9
  68. package/src/components/workflow-traces/trace-span-construction.ts +22 -4
  69. package/src/lib/trace-builder.ts +2 -1
  70. package/src/lib/utils.ts +29 -29
@@ -1,47 +1,45 @@
1
1
  'use client';
2
2
 
3
+ import { ArrowLeft, ArrowRight } from 'lucide-react';
3
4
  import type { ReactNode } from 'react';
4
5
  import { memo, useEffect, useMemo, useRef, useState } from 'react';
5
6
  import { cn } from '../../../lib/utils';
6
7
  import type { Span } from '../../trace-viewer/types';
7
8
  import { formatDuration, getHighResInMs } from '../../trace-viewer/util/timing';
8
- import type { SegmentStatus, TimeCompression } from '../utils';
9
+ import type { SegmentStatus, TimeMarker } from '../utils';
9
10
  import {
10
- computeCompressedTimeMarkers,
11
11
  computeSpanGaps,
12
12
  computeSpanSegments,
13
- computeTimeMarkers,
13
+ getSpanDurationMs,
14
14
  getResourceColor,
15
15
  } from '../utils';
16
16
 
17
- const QUEUED_BACKGROUND =
18
- 'radial-gradient(circle, var(--ds-gray-400) 2px, transparent 2px) center / 20px 20px space no-repeat, var(--ds-gray-500)';
19
-
20
- const HATCHED_BACKGROUND =
21
- 'repeating-linear-gradient(-45deg, var(--ds-gray-400) 0px, var(--ds-gray-400) 3px, var(--ds-gray-500) 3px, var(--ds-gray-500) 6px)';
22
-
23
17
  const SEGMENT_CONFIG: Record<
24
18
  SegmentStatus,
25
19
  { className?: string; style?: React.CSSProperties }
26
20
  > = {
27
- queued: { style: { background: QUEUED_BACKGROUND } },
21
+ queued: { className: 'bg-gray-500' },
28
22
  retrying: {
29
23
  className: 'box-border bg-gray-500',
30
24
  },
31
- waiting: { style: { background: HATCHED_BACKGROUND } },
25
+ waiting: { className: 'bg-gray-500' },
32
26
  running: { className: 'bg-blue-700' },
33
27
  failed: { className: 'bg-red-700' },
34
28
  succeeded: { className: 'bg-green-700' },
35
- sleeping: { className: 'bg-amber-700' },
29
+ sleeping: { className: 'bg-gray-500' },
36
30
  received: { className: 'bg-blue-700' },
37
31
  };
38
32
 
39
- const FIXED_BAR_WIDTH_PX = 4;
33
+ const BAR_HEIGHT_PX = 24;
34
+ const TINY_BAR_BOX_SIZE_PX = 24;
35
+ const TINY_BAR_WIDTH_PX = 4;
40
36
  const SEGMENT_GAP_PX = 1;
41
37
  // Keep this in sync with the rendered row height in the timeline/event list.
42
- const ROW_HEIGHT = 34;
38
+ const ROW_HEIGHT = 40;
43
39
  const CONTAINER_PAD_Y = 8;
44
40
  const END_CAP_HEIGHT = 8;
41
+ export const TIMELINE_PADDING_PX = 16;
42
+ const ORIGIN_MARKER_EPSILON = 0.000001;
45
43
 
46
44
  const DeltaIndicator = memo(function DeltaIndicator({
47
45
  leftFrac,
@@ -78,34 +76,42 @@ const DeltaIndicator = memo(function DeltaIndicator({
78
76
 
79
77
  const TimelineBar = memo(function TimelineBar({
80
78
  span,
81
- compression,
79
+ viewStart,
80
+ viewDuration,
82
81
  containerWidth,
83
82
  isSelected,
84
83
  onClick,
85
84
  }: {
86
85
  span: Span;
87
- compression: TimeCompression;
86
+ viewStart: number;
87
+ viewDuration: number;
88
88
  containerWidth: number;
89
89
  isSelected: boolean;
90
90
  onClick: () => void;
91
91
  }): ReactNode {
92
92
  const startTime = getHighResInMs(span.startTime);
93
93
  const endTime = getHighResInMs(span.endTime);
94
- const totalDurationMs = endTime - startTime;
94
+ const totalDurationMs = getSpanDurationMs(span);
95
95
 
96
- const leftFrac = compression.toVisual(startTime);
97
- const rightFrac = compression.toVisual(endTime);
98
- const widthFrac = rightFrac - leftFrac;
96
+ const leftFracRaw =
97
+ viewDuration > 0 ? (startTime - viewStart) / viewDuration : 0;
98
+ const rightFracRaw =
99
+ viewDuration > 0 ? (endTime - viewStart) / viewDuration : 0;
100
+ const widthFrac = rightFracRaw - leftFracRaw;
99
101
 
100
- const leftPct = leftFrac * 100;
102
+ const leftPct = leftFracRaw * 100;
101
103
  const widthPct = widthFrac * 100;
102
104
 
103
105
  const pixelWidth = widthFrac * containerWidth;
104
- const isCompressed = containerWidth > 0 && pixelWidth < FIXED_BAR_WIDTH_PX;
106
+ const visibleLeftFrac = Math.max(0, Math.min(1, leftFracRaw));
107
+ const visibleRightFrac = Math.max(0, Math.min(1, rightFracRaw));
108
+ const visiblePixelWidth =
109
+ Math.max(0, visibleRightFrac - visibleLeftFrac) * containerWidth;
110
+ const isTinyBar =
111
+ containerWidth > 0 && visiblePixelWidth < TINY_BAR_BOX_SIZE_PX;
105
112
  const [isRowHovered, setIsRowHovered] = useState(false);
106
113
 
107
114
  const segments = useMemo(() => computeSpanSegments(span), [span]);
108
- const finalSegment = segments[segments.length - 1];
109
115
 
110
116
  const workflowStatus = (span.attributes.data as Record<string, unknown>)
111
117
  ?.status as string | undefined;
@@ -114,24 +120,6 @@ const TimelineBar = memo(function TimelineBar({
114
120
  const fallbackColor = isErrored
115
121
  ? (colors.errorBar ?? 'var(--ds-red-700)')
116
122
  : colors.bar;
117
- const compressedSegmentStatus = isErrored
118
- ? 'failed'
119
- : span.resource === 'hook'
120
- ? 'received'
121
- : finalSegment?.status;
122
- const compressedSegmentStyle =
123
- compressedSegmentStatus === 'queued'
124
- ? { background: 'var(--ds-gray-500)' }
125
- : compressedSegmentStatus
126
- ? SEGMENT_CONFIG[compressedSegmentStatus].style
127
- : undefined;
128
- const compressedSegmentClassName = compressedSegmentStatus
129
- ? SEGMENT_CONFIG[compressedSegmentStatus].className
130
- : undefined;
131
-
132
- const hasCompressedStatus = Boolean(
133
- compressedSegmentClassName || compressedSegmentStyle
134
- );
135
123
  const renderDurationLabel = (label: string) => (
136
124
  <span
137
125
  className="pointer-events-none absolute inset-0 flex items-center justify-start overflow-hidden px-1 text-[10px] font-mono font-medium leading-none whitespace-nowrap text-left text-white tabular-nums"
@@ -147,22 +135,19 @@ const TimelineBar = memo(function TimelineBar({
147
135
  isRowHovered &&
148
136
  pixelWidth >= getMinDurationLabelWidthPx(totalDurationLabel);
149
137
 
150
- const barContent = isCompressed ? (
151
- <div
152
- className={cn(
153
- 'relative h-4 rounded-[0.25rem] top-[3px]',
154
- compressedSegmentClassName
155
- )}
156
- style={{
157
- width: '100%',
158
- background: hasCompressedStatus ? undefined : fallbackColor,
159
- ...compressedSegmentStyle,
160
- }}
161
- >
162
- {showBarDurationLabel ? renderDurationLabel(totalDurationLabel) : null}
138
+ const showBoundaryArrow = isTinyBar && (leftFracRaw < 0 || rightFracRaw > 1);
139
+ const BoundaryArrow = leftFracRaw < 0.5 ? ArrowLeft : ArrowRight;
140
+ const barContent = showBoundaryArrow ? (
141
+ <div className="flex h-6 w-6 items-center justify-center rounded-[0.25rem]">
142
+ <BoundaryArrow className="size-3 text-gray-900" />
163
143
  </div>
144
+ ) : isTinyBar ? (
145
+ <div
146
+ className="h-6 rounded-[0.25rem]"
147
+ style={{ background: fallbackColor }}
148
+ />
164
149
  ) : segments.length > 0 ? (
165
- <div className="relative w-full h-4 top-[3px]">
150
+ <div className="relative h-6 w-full">
166
151
  {segments.map((seg, i) => {
167
152
  const segPixelWidth =
168
153
  (seg.endFraction - seg.startFraction) * pixelWidth;
@@ -200,7 +185,7 @@ const TimelineBar = memo(function TimelineBar({
200
185
  </div>
201
186
  ) : (
202
187
  <div
203
- className="relative h-4 rounded-[0.25rem] top-[3px]"
188
+ className="relative h-6 rounded-[0.25rem]"
204
189
  style={{
205
190
  width: '100%',
206
191
  minWidth: 4,
@@ -218,24 +203,37 @@ const TimelineBar = memo(function TimelineBar({
218
203
  aria-expanded={isSelected}
219
204
  aria-level={1}
220
205
  className={cn(
221
- 'h-[34px] relative flex items-center hover:bg-gray-100 aria-selected:bg-gray-100 rounded-sm aria-selected:hover:bg-gray-200'
206
+ 'h-10 relative flex items-center hover:bg-gray-100 aria-selected:bg-gray-100 aria-selected:hover:bg-gray-200'
222
207
  )}
223
208
  onMouseEnter={() => setIsRowHovered(true)}
224
209
  onMouseLeave={() => setIsRowHovered(false)}
225
210
  onClick={onClick}
226
211
  >
227
212
  <div
228
- className="absolute top-1.5 h-[22px] rounded-sm"
213
+ className="absolute inset-y-0"
229
214
  style={{
230
- left: isCompressed
231
- ? `min(${leftPct}%, calc(100% - ${FIXED_BAR_WIDTH_PX}px))`
232
- : `${leftPct}%`,
233
- width: isCompressed
234
- ? `${FIXED_BAR_WIDTH_PX}px`
235
- : `max(${widthPct}%, 4px)`,
215
+ left: TIMELINE_PADDING_PX,
216
+ right: TIMELINE_PADDING_PX,
236
217
  }}
237
218
  >
238
- {barContent}
219
+ <div
220
+ className="absolute top-1/2 -translate-y-1/2"
221
+ style={{
222
+ left: showBoundaryArrow
223
+ ? `min(max(${leftPct}%, 0px), calc(100% - ${TINY_BAR_BOX_SIZE_PX}px))`
224
+ : isTinyBar
225
+ ? `min(${leftPct}%, calc(100% - ${TINY_BAR_WIDTH_PX}px))`
226
+ : `${leftPct}%`,
227
+ width: showBoundaryArrow
228
+ ? `${TINY_BAR_BOX_SIZE_PX}px`
229
+ : isTinyBar
230
+ ? `${TINY_BAR_WIDTH_PX}px`
231
+ : `max(${widthPct}%, 4px)`,
232
+ height: BAR_HEIGHT_PX,
233
+ }}
234
+ >
235
+ {barContent}
236
+ </div>
239
237
  </div>
240
238
  </div>
241
239
  );
@@ -244,43 +242,18 @@ const TimelineBar = memo(function TimelineBar({
244
242
  export { TimelineBar };
245
243
 
246
244
  export function TimelineHeader({
247
- viewStart,
248
- viewDuration,
249
- rootStart,
250
- compression,
251
- isZoomed,
252
- onResetZoom,
245
+ markers,
253
246
  hoverInfo,
254
247
  }: {
255
- viewStart: number;
256
- viewDuration: number;
257
- rootStart: number;
258
- compression: TimeCompression;
259
- isZoomed: boolean;
260
- onResetZoom: () => void;
248
+ markers: TimeMarker[];
261
249
  hoverInfo?: { fraction: number; label: string } | null;
262
250
  }): ReactNode {
263
- const viewEnd = viewStart + viewDuration;
264
-
265
- const markers = useMemo(
266
- () =>
267
- compression.isCompressed
268
- ? computeCompressedTimeMarkers(
269
- compression,
270
- viewStart,
271
- viewEnd,
272
- rootStart
273
- )
274
- : computeTimeMarkers(viewDuration, viewStart - rootStart),
275
- [compression, viewStart, viewEnd, viewDuration, rootStart]
276
- );
277
-
278
251
  return (
279
- <div className="relative bg-background-100 border-b border-gray-alpha-400 h-10 min-h-10 flex items-end px-2 pb-1">
252
+ <div className="relative bg-background-100 border-b border-gray-alpha-400 h-10 min-h-10 flex items-end px-4 pb-1">
280
253
  <div className="relative h-full flex-1">
281
- {markers.map((m, i) => (
254
+ {markers.map((m) => (
282
255
  <span
283
- key={i}
256
+ key={`${m.position}-${m.label}`}
284
257
  className="absolute bottom-1 font-mono text-xs font-normal leading-4 text-gray-900 whitespace-nowrap"
285
258
  style={{ left: `${m.position * 100}%` }}
286
259
  >
@@ -296,29 +269,24 @@ export function TimelineHeader({
296
269
  </span>
297
270
  )}
298
271
  </div>
299
- {isZoomed && (
300
- <button
301
- type="button"
302
- className="absolute right-2 top-1/2 -translate-y-1/2 z-[5] flex items-center py-0.5 px-2 border border-gray-alpha-400 rounded-md bg-background-100 font-sans text-[11px] font-medium text-gray-900 cursor-pointer whitespace-nowrap transition-[color,border-color] duration-[120ms] ease-in-out hover:text-gray-1000 hover:border-gray-600"
303
- onClick={onResetZoom}
304
- >
305
- Reset zoom
306
- </button>
307
- )}
308
272
  </div>
309
273
  );
310
274
  }
311
275
 
312
276
  export function Timeline({
313
277
  spans,
314
- compression,
278
+ viewStart,
279
+ viewEnd,
280
+ markers,
315
281
  selectedId,
316
282
  onSelect,
317
283
  hoverFraction,
318
284
  altHeld = false,
319
285
  }: {
320
286
  spans: Span[];
321
- compression: TimeCompression;
287
+ viewStart: number;
288
+ viewEnd: number;
289
+ markers: TimeMarker[];
322
290
  selectedId: string | null;
323
291
  onSelect: (spanId: string) => void;
324
292
  hoverFraction?: number | null;
@@ -326,6 +294,8 @@ export function Timeline({
326
294
  }): ReactNode {
327
295
  const containerRef = useRef<HTMLDivElement>(null);
328
296
  const [containerWidth, setContainerWidth] = useState(0);
297
+ const viewDuration = viewEnd - viewStart;
298
+ const timelineWidth = Math.max(0, containerWidth - TIMELINE_PADDING_PX * 2);
329
299
 
330
300
  useEffect(() => {
331
301
  const el = containerRef.current;
@@ -338,30 +308,64 @@ export function Timeline({
338
308
  }, []);
339
309
 
340
310
  const gaps = useMemo(
341
- () => computeSpanGaps(spans, compression),
342
- [spans, compression]
311
+ () => computeSpanGaps(spans, viewStart, viewEnd),
312
+ [spans, viewStart, viewEnd]
343
313
  );
344
314
 
345
315
  return (
346
- <div ref={containerRef} className="relative py-2">
316
+ <div ref={containerRef} className="relative h-full overflow-hidden">
317
+ <div
318
+ aria-hidden
319
+ className="absolute inset-y-0 pointer-events-none"
320
+ style={{
321
+ left: TIMELINE_PADDING_PX,
322
+ right: TIMELINE_PADDING_PX,
323
+ }}
324
+ >
325
+ {markers.map((marker) =>
326
+ Math.abs(marker.value) > ORIGIN_MARKER_EPSILON ? (
327
+ <div
328
+ key={`${marker.position}-${marker.label}`}
329
+ className="absolute top-0 bottom-0 w-px bg-gray-alpha-300"
330
+ style={{ left: `${marker.position * 100}%` }}
331
+ />
332
+ ) : null
333
+ )}
334
+ </div>
347
335
  {hoverFraction != null && (
348
336
  <div
349
- className="absolute top-0 bottom-0 w-px bg-gray-alpha-400 pointer-events-none z-10"
350
- style={{ left: `${hoverFraction * 100}%` }}
351
- />
337
+ className="absolute inset-y-0 pointer-events-none z-10"
338
+ style={{
339
+ left: TIMELINE_PADDING_PX,
340
+ right: TIMELINE_PADDING_PX,
341
+ }}
342
+ >
343
+ <div
344
+ className="absolute top-0 bottom-0 w-px bg-gray-alpha-400"
345
+ style={{ left: `${hoverFraction * 100}%` }}
346
+ />
347
+ </div>
352
348
  )}
353
349
  {spans.map((span) => (
354
350
  <TimelineBar
355
351
  key={span.spanId}
356
352
  span={span}
357
- compression={compression}
358
- containerWidth={containerWidth}
353
+ viewStart={viewStart}
354
+ viewDuration={viewDuration}
355
+ containerWidth={timelineWidth}
359
356
  isSelected={selectedId === span.spanId}
360
357
  onClick={() => onSelect(span.spanId)}
361
358
  />
362
359
  ))}
363
360
  {altHeld && (
364
- <div aria-hidden className="absolute inset-0 pointer-events-none">
361
+ <div
362
+ aria-hidden
363
+ className="absolute inset-y-0 pointer-events-none"
364
+ style={{
365
+ left: TIMELINE_PADDING_PX,
366
+ right: TIMELINE_PADDING_PX,
367
+ }}
368
+ >
365
369
  {gaps.map((gap) => (
366
370
  <DeltaIndicator
367
371
  key={gap.rowIndex}
@@ -34,20 +34,18 @@ export function ActiveSpanProvider({
34
34
 
35
35
  useEffect(() => {
36
36
  setActiveSpanId((currentSpanId) => {
37
- if (spans.length === 0) {
37
+ if (!currentSpanId) {
38
38
  return null;
39
39
  }
40
40
 
41
- if (currentSpanId) {
42
- const hasCurrentSpan = spans.some(
43
- (span) => span.spanId === currentSpanId
44
- );
45
- if (hasCurrentSpan) {
46
- return currentSpanId;
47
- }
41
+ const hasCurrentSpan = spans.some(
42
+ (span) => span.spanId === currentSpanId
43
+ );
44
+ if (hasCurrentSpan) {
45
+ return currentSpanId;
48
46
  }
49
47
 
50
- return spans[0].spanId;
48
+ return null;
51
49
  });
52
50
  }, [spans]);
53
51
 
@@ -4,6 +4,7 @@ import { X } from 'lucide-react';
4
4
  import type { ReactNode } from 'react';
5
5
  import type { Span } from '../trace-viewer/types';
6
6
  import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
7
+ import { getSpanDurationMs } from './utils';
7
8
 
8
9
  interface DetailPanelProps {
9
10
  span: Span;
@@ -17,7 +18,7 @@ export function DetailPanel({
17
18
  onClose,
18
19
  }: DetailPanelProps): ReactNode {
19
20
  const startMs = getHighResInMs(span.startTime);
20
- const durationMs = getHighResInMs(span.duration);
21
+ const durationMs = getSpanDurationMs(span);
21
22
  const offsetMs = startMs - rootStart;
22
23
 
23
24
  return (