@workflow/web-shared 5.0.0-beta.27 → 5.0.0-beta.29
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.
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +13 -1
- package/dist/components/new-trace-viewer/components/detail-panel.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/detail-panel.js +2 -2
- package/dist/components/new-trace-viewer/components/event-list.d.ts +1 -1
- package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/event-list.js +4 -6
- package/dist/components/new-trace-viewer/components/minimap.d.ts +14 -0
- package/dist/components/new-trace-viewer/components/minimap.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/minimap.js +337 -0
- package/dist/components/new-trace-viewer/components/split-pane.d.ts +0 -2
- package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/split-pane.js +9 -75
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +5 -7
- package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.js +6 -2
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +48 -96
- package/dist/components/new-trace-viewer/utils.d.ts +21 -0
- package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/utils.js +44 -3
- package/dist/components/sidebar/attribute-panel.d.ts +4 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +32 -24
- package/dist/components/sidebar/attributes-block.d.ts +1 -0
- package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
- package/dist/components/sidebar/attributes-block.js +6 -5
- package/dist/components/sidebar/entity-detail-panel.d.ts +5 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +18 -3
- package/dist/components/sidebar/sidebar-data-context.d.ts +4 -0
- package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -1
- package/dist/components/sidebar/sidebar-data-context.js +1 -1
- package/dist/lib/utils.d.ts +8 -5
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +13 -16
- package/package.json +5 -5
- package/src/components/event-list-view.tsx +21 -0
- package/src/components/new-trace-viewer/components/detail-panel.tsx +1 -0
- package/src/components/new-trace-viewer/components/event-list.tsx +4 -6
- package/src/components/new-trace-viewer/components/minimap.tsx +559 -0
- package/src/components/new-trace-viewer/components/split-pane.tsx +39 -95
- package/src/components/new-trace-viewer/components/timeline.tsx +3 -5
- package/src/components/new-trace-viewer/components/trace-viewer-skeleton.tsx +17 -0
- package/src/components/new-trace-viewer/trace-viewer.tsx +79 -117
- package/src/components/new-trace-viewer/utils.test.ts +65 -0
- package/src/components/new-trace-viewer/utils.ts +76 -2
- package/src/components/sidebar/attribute-panel.tsx +77 -22
- package/src/components/sidebar/attributes-block.tsx +43 -10
- package/src/components/sidebar/entity-detail-panel.tsx +22 -0
- package/src/components/sidebar/sidebar-data-context.tsx +4 -0
- package/src/lib/utils.test.ts +33 -0
- package/src/lib/utils.ts +12 -16
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
import {
|
|
4
4
|
Children,
|
|
5
5
|
type ReactNode,
|
|
6
|
-
type PointerEvent as ReactPointerEvent,
|
|
7
6
|
type RefObject,
|
|
8
7
|
useCallback,
|
|
9
|
-
useEffect,
|
|
10
8
|
useRef,
|
|
11
9
|
useState,
|
|
12
10
|
} from 'react';
|
|
13
11
|
import { cn } from '../../../lib/cn';
|
|
12
|
+
import { DraggableBorder } from './draggable-border';
|
|
14
13
|
import {
|
|
15
14
|
DEFAULT_START_PX,
|
|
16
15
|
GUTTER_PX,
|
|
@@ -19,12 +18,12 @@ import {
|
|
|
19
18
|
} from './pane-constants';
|
|
20
19
|
import { useElementWidth } from './use-element-width';
|
|
21
20
|
|
|
21
|
+
const SPLIT_PANE_START_ID = 'trace-split-start';
|
|
22
|
+
|
|
22
23
|
export interface SplitPaneProps {
|
|
23
24
|
children: ReactNode;
|
|
24
25
|
className?: string;
|
|
25
|
-
/** Fixed (non-scrolling) header rendered above the start pane. */
|
|
26
26
|
startHeader?: ReactNode;
|
|
27
|
-
/** Fixed (non-scrolling) header rendered above the end pane. */
|
|
28
27
|
endHeader?: ReactNode;
|
|
29
28
|
scrollContainerRef?: RefObject<HTMLDivElement | null>;
|
|
30
29
|
}
|
|
@@ -42,25 +41,11 @@ export function SplitPane({
|
|
|
42
41
|
}
|
|
43
42
|
const [start, end] = parts;
|
|
44
43
|
|
|
45
|
-
// `startPx` is the user's preferred width; the rendered width is derived by
|
|
46
|
-
// clamping against the live container width (same model as the detail
|
|
47
|
-
// panel), so shrinking the container compresses the pane without destroying
|
|
48
|
-
// the preference, and re-growing restores it.
|
|
49
44
|
const [startPx, setStartPx] = useState(DEFAULT_START_PX);
|
|
50
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
51
45
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
52
|
-
const
|
|
53
|
-
const rafRef = useRef<number | null>(null);
|
|
54
|
-
const pendingPx = useRef(DEFAULT_START_PX);
|
|
55
|
-
const pointerIdRef = useRef<number | null>(null);
|
|
46
|
+
const startRef = useRef<HTMLDivElement>(null);
|
|
56
47
|
const containerWidth = useElementWidth(containerRef);
|
|
57
48
|
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
return () => {
|
|
60
|
-
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
61
|
-
};
|
|
62
|
-
}, []);
|
|
63
|
-
|
|
64
49
|
const setContainerRef = useCallback(
|
|
65
50
|
(node: HTMLDivElement | null) => {
|
|
66
51
|
containerRef.current = node;
|
|
@@ -78,65 +63,15 @@ export function SplitPane({
|
|
|
78
63
|
return Math.min(maxPx, Math.max(MIN_PX, px));
|
|
79
64
|
}, []);
|
|
80
65
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
pendingPx.current = clampPx(e.clientX - rect.left);
|
|
90
|
-
if (!rafRef.current) {
|
|
91
|
-
rafRef.current = requestAnimationFrame(() => {
|
|
92
|
-
rafRef.current = null;
|
|
93
|
-
setStartPx(pendingPx.current);
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const onPointerUp = (e: globalThis.PointerEvent) => {
|
|
99
|
-
if (e.pointerId !== pointerIdRef.current) return;
|
|
100
|
-
const gutter = gutterRef.current;
|
|
101
|
-
if (gutter?.hasPointerCapture(e.pointerId)) {
|
|
102
|
-
gutter.releasePointerCapture(e.pointerId);
|
|
103
|
-
}
|
|
104
|
-
if (rafRef.current) {
|
|
105
|
-
cancelAnimationFrame(rafRef.current);
|
|
106
|
-
rafRef.current = null;
|
|
107
|
-
}
|
|
108
|
-
pointerIdRef.current = null;
|
|
109
|
-
setIsDragging(false);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
document.addEventListener('pointermove', onPointerMove);
|
|
113
|
-
document.addEventListener('pointerup', onPointerUp);
|
|
114
|
-
document.addEventListener('pointercancel', onPointerUp);
|
|
115
|
-
|
|
116
|
-
return () => {
|
|
117
|
-
document.removeEventListener('pointermove', onPointerMove);
|
|
118
|
-
document.removeEventListener('pointerup', onPointerUp);
|
|
119
|
-
document.removeEventListener('pointercancel', onPointerUp);
|
|
120
|
-
};
|
|
121
|
-
}, [isDragging, clampPx]);
|
|
122
|
-
|
|
123
|
-
const handlePointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
|
|
124
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
125
|
-
pointerIdRef.current = e.pointerId;
|
|
126
|
-
setIsDragging(true);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const handleLostPointerCapture = () => {
|
|
130
|
-
pointerIdRef.current = null;
|
|
131
|
-
setIsDragging(false);
|
|
132
|
-
if (rafRef.current) {
|
|
133
|
-
cancelAnimationFrame(rafRef.current);
|
|
134
|
-
rafRef.current = null;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
66
|
+
const handleWidthChange = useCallback(
|
|
67
|
+
(next: number) => setStartPx(clampPx(next)),
|
|
68
|
+
[clampPx]
|
|
69
|
+
);
|
|
70
|
+
const handleReset = useCallback(
|
|
71
|
+
() => setStartPx(clampPx(DEFAULT_START_PX)),
|
|
72
|
+
[clampPx]
|
|
73
|
+
);
|
|
137
74
|
|
|
138
|
-
// Floor applied last so the start column can never collapse below MIN_PX
|
|
139
|
-
// (which would produce an invalid negative grid track on tiny containers).
|
|
140
75
|
const effectiveStartPx =
|
|
141
76
|
containerWidth > 0
|
|
142
77
|
? Math.max(MIN_PX, Math.min(containerWidth - MIN_PX - GUTTER_PX, startPx))
|
|
@@ -144,18 +79,9 @@ export function SplitPane({
|
|
|
144
79
|
|
|
145
80
|
const colTemplate = paneColTemplate(effectiveStartPx);
|
|
146
81
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
className="relative z-20 isolate flex shrink-0 cursor-col-resize justify-center"
|
|
151
|
-
onPointerDown={handlePointerDown}
|
|
152
|
-
onLostPointerCapture={handleLostPointerCapture}
|
|
153
|
-
>
|
|
154
|
-
<span
|
|
155
|
-
className="pointer-events-none relative z-10 h-full w-px shrink-0 bg-gray-alpha-400"
|
|
156
|
-
aria-hidden
|
|
157
|
-
/>
|
|
158
|
-
</div>
|
|
82
|
+
const maxStartPx = Math.max(
|
|
83
|
+
MIN_PX,
|
|
84
|
+
containerWidth > 0 ? containerWidth - MIN_PX - GUTTER_PX : startPx
|
|
159
85
|
);
|
|
160
86
|
|
|
161
87
|
return (
|
|
@@ -172,14 +98,32 @@ export function SplitPane({
|
|
|
172
98
|
</div>
|
|
173
99
|
<div
|
|
174
100
|
ref={setContainerRef}
|
|
175
|
-
className=
|
|
176
|
-
'grid flex-1 min-h-0 overflow-x-hidden overflow-y-auto',
|
|
177
|
-
isDragging && 'select-none'
|
|
178
|
-
)}
|
|
101
|
+
className="grid flex-1 min-h-0 overflow-x-hidden overflow-y-auto"
|
|
179
102
|
style={{ gridTemplateColumns: colTemplate }}
|
|
180
103
|
>
|
|
181
|
-
{
|
|
182
|
-
|
|
104
|
+
<div ref={startRef} id={SPLIT_PANE_START_ID} className="min-w-0">
|
|
105
|
+
{start}
|
|
106
|
+
</div>
|
|
107
|
+
<div className="relative z-20 isolate flex shrink-0 justify-center">
|
|
108
|
+
<span
|
|
109
|
+
className="pointer-events-none relative z-10 h-full w-px shrink-0 bg-gray-alpha-400"
|
|
110
|
+
aria-hidden
|
|
111
|
+
/>
|
|
112
|
+
<DraggableBorder
|
|
113
|
+
element={startRef}
|
|
114
|
+
position="right"
|
|
115
|
+
onWidthChange={handleWidthChange}
|
|
116
|
+
onReset={handleReset}
|
|
117
|
+
aria-label="Resize event list"
|
|
118
|
+
aria-controls={SPLIT_PANE_START_ID}
|
|
119
|
+
aria-valuemin={MIN_PX}
|
|
120
|
+
aria-valuemax={maxStartPx}
|
|
121
|
+
aria-valuenow={Math.min(
|
|
122
|
+
Math.max(Math.round(effectiveStartPx), MIN_PX),
|
|
123
|
+
maxStartPx
|
|
124
|
+
)}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
183
127
|
{end}
|
|
184
128
|
</div>
|
|
185
129
|
</div>
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
} from 'react';
|
|
14
14
|
import { cn } from '../../../lib/cn';
|
|
15
15
|
import {
|
|
16
|
-
formatDuration,
|
|
17
16
|
formatDurationPrecise,
|
|
18
17
|
getHighResInMs,
|
|
19
18
|
} from '../../trace-viewer/util/timing';
|
|
@@ -32,6 +31,7 @@ import {
|
|
|
32
31
|
computeSpanSegments,
|
|
33
32
|
getResourceColor,
|
|
34
33
|
getSpanDurationMs,
|
|
34
|
+
isSpanErrored,
|
|
35
35
|
} from '../utils';
|
|
36
36
|
import {
|
|
37
37
|
cullCollidingMarkers,
|
|
@@ -451,9 +451,7 @@ const TimelineBar = memo(function TimelineBar({
|
|
|
451
451
|
const hasMarkers =
|
|
452
452
|
markers.length > 0 || offscreen.left !== null || offscreen.right !== null;
|
|
453
453
|
|
|
454
|
-
const
|
|
455
|
-
?.status as string | undefined;
|
|
456
|
-
const isErrored = span.status.code === 2 || workflowStatus === 'failed';
|
|
454
|
+
const isErrored = isSpanErrored(span);
|
|
457
455
|
const colors = getResourceColor(span.resource);
|
|
458
456
|
const fallbackBg = isErrored
|
|
459
457
|
? (colors.errorBg ?? 'var(--ds-red-200)')
|
|
@@ -733,7 +731,7 @@ export function Timeline({
|
|
|
733
731
|
key={gap.rowIndex}
|
|
734
732
|
leftFrac={gap.leftFrac}
|
|
735
733
|
rightFrac={gap.rightFrac}
|
|
736
|
-
label={
|
|
734
|
+
label={formatDurationPrecise(gap.gapMs)}
|
|
737
735
|
rowIndex={gap.rowIndex}
|
|
738
736
|
/>
|
|
739
737
|
))}
|
|
@@ -33,6 +33,23 @@ export function TraceViewerSkeleton() {
|
|
|
33
33
|
>
|
|
34
34
|
<span className="sr-only">Loading trace…</span>
|
|
35
35
|
|
|
36
|
+
{/* Minimap strip: thin density lines tracing the same shape as the bars */}
|
|
37
|
+
<div className="relative h-10 min-h-10 shrink-0 border-b border-gray-alpha-400">
|
|
38
|
+
<div className="absolute inset-x-4 top-[6px]">
|
|
39
|
+
{ROWS.map((row, index) => (
|
|
40
|
+
<Skeleton
|
|
41
|
+
key={row.id}
|
|
42
|
+
className="absolute h-[3px] rounded-full"
|
|
43
|
+
style={{
|
|
44
|
+
left: `${row.off}%`,
|
|
45
|
+
width: `${row.bar}%`,
|
|
46
|
+
top: index * 3.5,
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
36
53
|
{/* Header row: search header | divider | timeline header */}
|
|
37
54
|
<div
|
|
38
55
|
className="shrink-0 grid"
|
|
@@ -12,13 +12,17 @@ import {
|
|
|
12
12
|
} from 'react';
|
|
13
13
|
import { useLoadMoreOnScroll } from '../../hooks/use-load-more-on-scroll';
|
|
14
14
|
import { useReducedMotion } from '../../hooks/use-reduced-motion';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
formatDurationPrecise,
|
|
17
|
+
getHighResInMs,
|
|
18
|
+
} from '../trace-viewer/util/timing';
|
|
16
19
|
import { IconButton } from '../ui/icon-button';
|
|
17
20
|
import { Kbd } from '../ui/kbd';
|
|
18
21
|
import { Spinner } from '../ui/spinner';
|
|
19
22
|
import { TooltipProvider } from '../ui/tooltip';
|
|
20
23
|
import { TraceDetailPanel } from './components/detail-panel';
|
|
21
24
|
import EventList from './components/event-list';
|
|
25
|
+
import { Minimap } from './components/minimap';
|
|
22
26
|
import { SplitPane } from './components/split-pane';
|
|
23
27
|
import {
|
|
24
28
|
TIMELINE_PADDING_PX,
|
|
@@ -30,7 +34,14 @@ import { ROW_HEIGHT_PX, scrollRowIntoView } from './components/use-row-window';
|
|
|
30
34
|
import { ActiveSpanProvider, useActiveSpan } from './context';
|
|
31
35
|
import { searchSpans } from './search';
|
|
32
36
|
import type { TraceWithMeta } from './types';
|
|
33
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
clampViewportToRoot,
|
|
39
|
+
computeRootBounds,
|
|
40
|
+
computeTimeMarkers,
|
|
41
|
+
type ViewportRange,
|
|
42
|
+
wheelDeltaToPixels,
|
|
43
|
+
wheelZoomScaleFactor,
|
|
44
|
+
} from './utils';
|
|
34
45
|
|
|
35
46
|
interface NewTraceViewerProps {
|
|
36
47
|
trace: TraceWithMeta;
|
|
@@ -43,17 +54,12 @@ const MIN_VIEWPORT_MS = 0.001;
|
|
|
43
54
|
|
|
44
55
|
const ZOOM_DEBOUNCE_MS = 150;
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
end: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function useAnimatedViewport(initial: Viewport) {
|
|
52
|
-
const [viewport, setViewportState] = useState<Viewport>(initial);
|
|
57
|
+
function useAnimatedViewport(initial: ViewportRange) {
|
|
58
|
+
const [viewport, setViewportState] = useState<ViewportRange>(initial);
|
|
53
59
|
const animRef = useRef<{
|
|
54
60
|
raf: number;
|
|
55
|
-
from:
|
|
56
|
-
to:
|
|
61
|
+
from: ViewportRange;
|
|
62
|
+
to: ViewportRange;
|
|
57
63
|
start: number;
|
|
58
64
|
} | null>(null);
|
|
59
65
|
const currentRef = useRef(initial);
|
|
@@ -68,7 +74,7 @@ function useAnimatedViewport(initial: Viewport) {
|
|
|
68
74
|
}, []);
|
|
69
75
|
|
|
70
76
|
const animateTo = useCallback(
|
|
71
|
-
(target:
|
|
77
|
+
(target: ViewportRange) => {
|
|
72
78
|
cancel();
|
|
73
79
|
|
|
74
80
|
if (reducedMotion) {
|
|
@@ -81,8 +87,8 @@ function useAnimatedViewport(initial: Viewport) {
|
|
|
81
87
|
const anim = { raf: 0, from, to: target, start: performance.now() };
|
|
82
88
|
|
|
83
89
|
const tick = () => {
|
|
84
|
-
const t = Math.min((performance.now() - anim.start) /
|
|
85
|
-
const e =
|
|
90
|
+
const t = Math.min((performance.now() - anim.start) / 240, 1);
|
|
91
|
+
const e = t < 0.5 ? 8 * t * t * t * t : 1 - (-2 * t + 2) ** 4 / 2;
|
|
86
92
|
setViewportState({
|
|
87
93
|
start: anim.from.start + (anim.to.start - anim.from.start) * e,
|
|
88
94
|
end: anim.from.end + (anim.to.end - anim.from.end) * e,
|
|
@@ -98,7 +104,7 @@ function useAnimatedViewport(initial: Viewport) {
|
|
|
98
104
|
);
|
|
99
105
|
|
|
100
106
|
const setViewport = useCallback(
|
|
101
|
-
(update:
|
|
107
|
+
(update: ViewportRange | ((prev: ViewportRange) => ViewportRange)) => {
|
|
102
108
|
cancel();
|
|
103
109
|
if (typeof update === 'function') {
|
|
104
110
|
setViewportState((prev) => {
|
|
@@ -180,7 +186,7 @@ function NewTraceViewerContent({
|
|
|
180
186
|
end: root.startTime + root.duration,
|
|
181
187
|
});
|
|
182
188
|
|
|
183
|
-
const prevRootRef = useRef<
|
|
189
|
+
const prevRootRef = useRef<ViewportRange>({
|
|
184
190
|
start: root.startTime,
|
|
185
191
|
end: root.startTime + root.duration,
|
|
186
192
|
});
|
|
@@ -221,60 +227,45 @@ function NewTraceViewerContent({
|
|
|
221
227
|
animateTo({ start: root.startTime, end: root.startTime + root.duration });
|
|
222
228
|
}, [animateTo, root.startTime, root.duration]);
|
|
223
229
|
|
|
230
|
+
const clampToRoot = useCallback(
|
|
231
|
+
(next: ViewportRange): ViewportRange =>
|
|
232
|
+
clampViewportToRoot(next, root.startTime, root.endTime, MIN_VIEWPORT_MS),
|
|
233
|
+
[root.startTime, root.endTime]
|
|
234
|
+
);
|
|
235
|
+
|
|
224
236
|
// Pan (keeping the current zoom) so `timeMs` is centered in view — used by the
|
|
225
237
|
// off-screen marker indicators to scroll their marker into view.
|
|
226
238
|
const handleRevealTime = useCallback(
|
|
227
239
|
(timeMs: number) => {
|
|
228
|
-
const rootS = root.startTime;
|
|
229
|
-
const rootE = root.startTime + root.duration;
|
|
230
240
|
const { start, end } = viewportRef.current;
|
|
231
241
|
const duration = end - start;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (newEnd > rootE) {
|
|
239
|
-
newEnd = rootE;
|
|
240
|
-
newStart = Math.max(rootS, rootE - duration);
|
|
241
|
-
}
|
|
242
|
-
animateTo({ start: newStart, end: newEnd });
|
|
242
|
+
animateTo(
|
|
243
|
+
clampToRoot({
|
|
244
|
+
start: timeMs - duration / 2,
|
|
245
|
+
end: timeMs + duration / 2,
|
|
246
|
+
})
|
|
247
|
+
);
|
|
243
248
|
},
|
|
244
|
-
[animateTo,
|
|
249
|
+
[animateTo, clampToRoot]
|
|
245
250
|
);
|
|
246
251
|
|
|
247
252
|
const ZOOM_FACTOR = 0.5;
|
|
248
253
|
|
|
249
254
|
const zoomBy = useCallback(
|
|
250
255
|
(factor: number) => {
|
|
251
|
-
const rootS = root.startTime;
|
|
252
|
-
const rootE = root.startTime + root.duration;
|
|
253
|
-
const rootD = root.duration;
|
|
254
|
-
|
|
255
256
|
setViewport((prev) => {
|
|
256
|
-
const prevDuration = prev.end - prev.start;
|
|
257
257
|
const center = (prev.start + prev.end) / 2;
|
|
258
258
|
const newDuration = Math.max(
|
|
259
259
|
MIN_VIEWPORT_MS,
|
|
260
|
-
|
|
260
|
+
(prev.end - prev.start) * factor
|
|
261
261
|
);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
newStart = rootS;
|
|
267
|
-
newEnd = rootS + newDuration;
|
|
268
|
-
}
|
|
269
|
-
if (newEnd > rootE) {
|
|
270
|
-
newEnd = rootE;
|
|
271
|
-
newStart = Math.max(rootS, rootE - newDuration);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return { start: newStart, end: newEnd };
|
|
262
|
+
return clampToRoot({
|
|
263
|
+
start: center - newDuration / 2,
|
|
264
|
+
end: center + newDuration / 2,
|
|
265
|
+
});
|
|
275
266
|
});
|
|
276
267
|
},
|
|
277
|
-
[setViewport,
|
|
268
|
+
[setViewport, clampToRoot]
|
|
278
269
|
);
|
|
279
270
|
|
|
280
271
|
const zoomIn = useCallback(() => zoomBy(ZOOM_FACTOR), [zoomBy]);
|
|
@@ -291,12 +282,8 @@ function NewTraceViewerContent({
|
|
|
291
282
|
const spanEnd = getHighResInMs(span.endTime);
|
|
292
283
|
const spanDuration = spanEnd - spanStart;
|
|
293
284
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const rootD = root.duration;
|
|
297
|
-
|
|
298
|
-
if (spanDuration > rootD * 0.8) {
|
|
299
|
-
animateTo({ start: rootS, end: rootE });
|
|
285
|
+
if (spanDuration > root.duration * 0.8) {
|
|
286
|
+
animateTo({ start: root.startTime, end: root.endTime });
|
|
300
287
|
return;
|
|
301
288
|
}
|
|
302
289
|
|
|
@@ -310,20 +297,16 @@ function NewTraceViewerContent({
|
|
|
310
297
|
newEnd = center + MIN_VIEWPORT_MS / 2;
|
|
311
298
|
}
|
|
312
299
|
|
|
313
|
-
|
|
314
|
-
const duration = newEnd - newStart;
|
|
315
|
-
newStart = rootS;
|
|
316
|
-
newEnd = Math.min(rootE, rootS + duration);
|
|
317
|
-
}
|
|
318
|
-
if (newEnd > rootE) {
|
|
319
|
-
const duration = newEnd - newStart;
|
|
320
|
-
newEnd = rootE;
|
|
321
|
-
newStart = Math.max(rootS, rootE - duration);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
animateTo({ start: newStart, end: newEnd });
|
|
300
|
+
animateTo(clampToRoot({ start: newStart, end: newEnd }));
|
|
325
301
|
},
|
|
326
|
-
[
|
|
302
|
+
[
|
|
303
|
+
animateTo,
|
|
304
|
+
trace.spans,
|
|
305
|
+
root.startTime,
|
|
306
|
+
root.endTime,
|
|
307
|
+
root.duration,
|
|
308
|
+
clampToRoot,
|
|
309
|
+
]
|
|
327
310
|
);
|
|
328
311
|
|
|
329
312
|
// Bring a row into view when keyboard/button navigation lands on a span that
|
|
@@ -423,7 +406,7 @@ function NewTraceViewerContent({
|
|
|
423
406
|
if (hoverFraction == null) return null;
|
|
424
407
|
const absTime = viewport.start + hoverFraction * viewDuration;
|
|
425
408
|
const offset = absTime - root.startTime;
|
|
426
|
-
return { fraction: hoverFraction, label:
|
|
409
|
+
return { fraction: hoverFraction, label: formatDurationPrecise(offset) };
|
|
427
410
|
}, [hoverFraction, viewport.start, viewDuration, root.startTime]);
|
|
428
411
|
|
|
429
412
|
const handleTimelineMouseMove = useCallback(
|
|
@@ -451,12 +434,7 @@ function NewTraceViewerContent({
|
|
|
451
434
|
|
|
452
435
|
useEffect(() => {
|
|
453
436
|
const el = timelineRef.current;
|
|
454
|
-
if (!el) return;
|
|
455
|
-
|
|
456
|
-
const rootS = root.startTime;
|
|
457
|
-
const rootE = root.startTime + root.duration;
|
|
458
|
-
const rootD = root.duration;
|
|
459
|
-
if (rootD <= 0) return;
|
|
437
|
+
if (!el || root.duration <= 0) return;
|
|
460
438
|
|
|
461
439
|
const onWheel = (e: WheelEvent): void => {
|
|
462
440
|
const isZoomGesture = e.ctrlKey || e.metaKey;
|
|
@@ -470,9 +448,6 @@ function NewTraceViewerContent({
|
|
|
470
448
|
if (contentWidth <= 0) return;
|
|
471
449
|
|
|
472
450
|
if (isZoomGesture) {
|
|
473
|
-
let dy = e.deltaY;
|
|
474
|
-
if (e.deltaMode === 1) dy *= 16;
|
|
475
|
-
|
|
476
451
|
const cursorFraction = Math.max(
|
|
477
452
|
0,
|
|
478
453
|
Math.min(
|
|
@@ -480,59 +455,37 @@ function NewTraceViewerContent({
|
|
|
480
455
|
(e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
|
|
481
456
|
)
|
|
482
457
|
);
|
|
483
|
-
const
|
|
484
|
-
const scaleFactor = 2 ** (dy / (isMouseWheel ? 200 : 60));
|
|
458
|
+
const scaleFactor = wheelZoomScaleFactor(e);
|
|
485
459
|
|
|
486
460
|
setViewport((prev) => {
|
|
487
461
|
const prevDuration = prev.end - prev.start;
|
|
488
462
|
const cursorTime = prev.start + cursorFraction * prevDuration;
|
|
463
|
+
// Clamp the duration before anchoring so the cursor keeps its
|
|
464
|
+
// fraction even when the zoom hits the min/max bounds.
|
|
489
465
|
const newDuration = Math.max(
|
|
490
466
|
MIN_VIEWPORT_MS,
|
|
491
|
-
Math.min(
|
|
467
|
+
Math.min(root.duration, prevDuration * scaleFactor)
|
|
492
468
|
);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (newStart < rootS) {
|
|
498
|
-
newStart = rootS;
|
|
499
|
-
newEnd = rootS + newDuration;
|
|
500
|
-
}
|
|
501
|
-
if (newEnd > rootE) {
|
|
502
|
-
newEnd = rootE;
|
|
503
|
-
newStart = Math.max(rootS, rootE - newDuration);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
return { start: newStart, end: newEnd };
|
|
469
|
+
return clampToRoot({
|
|
470
|
+
start: cursorTime - cursorFraction * newDuration,
|
|
471
|
+
end: cursorTime + (1 - cursorFraction) * newDuration,
|
|
472
|
+
});
|
|
507
473
|
});
|
|
508
474
|
} else {
|
|
509
|
-
|
|
510
|
-
if (e.deltaMode === 1) dx *= 16;
|
|
511
|
-
|
|
475
|
+
const dx = wheelDeltaToPixels(e.deltaX, e.deltaMode);
|
|
512
476
|
setViewport((prev) => {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (newStart < rootS) {
|
|
520
|
-
newStart = rootS;
|
|
521
|
-
newEnd = rootS + prevDuration;
|
|
522
|
-
}
|
|
523
|
-
if (newEnd > rootE) {
|
|
524
|
-
newEnd = rootE;
|
|
525
|
-
newStart = Math.max(rootS, rootE - prevDuration);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return { start: newStart, end: newEnd };
|
|
477
|
+
const panAmount = (dx / contentWidth) * (prev.end - prev.start);
|
|
478
|
+
return clampToRoot({
|
|
479
|
+
start: prev.start + panAmount,
|
|
480
|
+
end: prev.end + panAmount,
|
|
481
|
+
});
|
|
529
482
|
});
|
|
530
483
|
}
|
|
531
484
|
};
|
|
532
485
|
|
|
533
486
|
el.addEventListener('wheel', onWheel, { passive: false });
|
|
534
487
|
return () => el.removeEventListener('wheel', onWheel);
|
|
535
|
-
}, [root.
|
|
488
|
+
}, [root.duration, setViewport, clampToRoot]);
|
|
536
489
|
|
|
537
490
|
return (
|
|
538
491
|
<div
|
|
@@ -542,8 +495,16 @@ function NewTraceViewerContent({
|
|
|
542
495
|
>
|
|
543
496
|
<div
|
|
544
497
|
id="trace-parent"
|
|
545
|
-
className="flex-1 min-w-0 grid grid-rows-[
|
|
498
|
+
className="flex-1 min-w-0 grid grid-rows-[auto_1fr] h-full min-h-0 overflow-hidden relative bg-background-100"
|
|
546
499
|
>
|
|
500
|
+
<Minimap
|
|
501
|
+
spans={trace.spans}
|
|
502
|
+
root={root}
|
|
503
|
+
viewport={viewport}
|
|
504
|
+
minViewportMs={MIN_VIEWPORT_MS}
|
|
505
|
+
onViewportChange={setViewport}
|
|
506
|
+
onAnimateTo={animateTo}
|
|
507
|
+
/>
|
|
547
508
|
<SplitPane
|
|
548
509
|
scrollContainerRef={scrollContainerRef}
|
|
549
510
|
startHeader={
|
|
@@ -603,6 +564,7 @@ function NewTraceViewerContent({
|
|
|
603
564
|
{/* biome-ignore lint/a11y/noStaticElementInteractions: timeline hover and wheel gestures are pointer-only annotations */}
|
|
604
565
|
<div
|
|
605
566
|
ref={timelineRef}
|
|
567
|
+
id="trace-timeline"
|
|
606
568
|
className="block min-h-0 overflow-visible relative"
|
|
607
569
|
onDoubleClick={resetZoom}
|
|
608
570
|
onMouseMove={handleTimelineMouseMove}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { Span, SpanEvent } from './types';
|
|
3
3
|
import {
|
|
4
|
+
clampViewportToRoot,
|
|
4
5
|
computeOffscreenMarkers,
|
|
5
6
|
computeSpanMarkers,
|
|
6
7
|
computeSpanSegments,
|
|
8
|
+
computeTimeMarkers,
|
|
7
9
|
} from './utils';
|
|
8
10
|
|
|
9
11
|
/** Build a high-res timestamp tuple ([seconds, nanoseconds]) for a given ms. */
|
|
@@ -180,3 +182,66 @@ describe('computeOffscreenMarkers', () => {
|
|
|
180
182
|
});
|
|
181
183
|
});
|
|
182
184
|
});
|
|
185
|
+
|
|
186
|
+
describe('clampViewportToRoot', () => {
|
|
187
|
+
const clamp = (next: { start: number; end: number }) =>
|
|
188
|
+
clampViewportToRoot(next, 100, 1100, 10);
|
|
189
|
+
|
|
190
|
+
it('passes through a window already inside the root', () => {
|
|
191
|
+
expect(clamp({ start: 200, end: 400 })).toEqual({ start: 200, end: 400 });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('shifts a window past the left edge without changing its duration', () => {
|
|
195
|
+
expect(clamp({ start: 50, end: 250 })).toEqual({ start: 100, end: 300 });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('shifts a window past the right edge without changing its duration', () => {
|
|
199
|
+
expect(clamp({ start: 1000, end: 1200 })).toEqual({
|
|
200
|
+
start: 900,
|
|
201
|
+
end: 1100,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('clamps a window wider than the root to the full extent', () => {
|
|
206
|
+
expect(clamp({ start: 0, end: 5000 })).toEqual({ start: 100, end: 1100 });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('enforces the minimum duration', () => {
|
|
210
|
+
expect(clamp({ start: 500, end: 502 })).toEqual({ start: 500, end: 510 });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('keeps a minimum-duration window inside the root near the right edge', () => {
|
|
214
|
+
expect(clamp({ start: 1098, end: 1099 })).toEqual({
|
|
215
|
+
start: 1090,
|
|
216
|
+
end: 1100,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('computeTimeMarkers', () => {
|
|
222
|
+
it('emits distinct, precise labels across a sub-second-step window', () => {
|
|
223
|
+
// A ~3s window drops the tick step to 500ms. Before the fix this rendered
|
|
224
|
+
// duplicate "2s, 2s, 3s, 3s" labels; now each tick is distinct.
|
|
225
|
+
const labels = computeTimeMarkers(3000, 0).map((m) => m.label);
|
|
226
|
+
expect(labels).toEqual(['0s', '500ms', '1s', '1.5s', '2s', '2.5s', '3s']);
|
|
227
|
+
expect(new Set(labels).size).toBe(labels.length);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('keeps clean whole-second labels when the step is >=1s', () => {
|
|
231
|
+
const labels = computeTimeMarkers(10_000, 0).map((m) => m.label);
|
|
232
|
+
expect(labels).toEqual(['0s', '2s', '4s', '6s', '8s', '10s']);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('still reads in ms when super zoomed in', () => {
|
|
236
|
+
const labels = computeTimeMarkers(120, 0).map((m) => m.label);
|
|
237
|
+
expect(labels).toEqual([
|
|
238
|
+
'0s',
|
|
239
|
+
'20ms',
|
|
240
|
+
'40ms',
|
|
241
|
+
'60ms',
|
|
242
|
+
'80ms',
|
|
243
|
+
'100ms',
|
|
244
|
+
'120ms',
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
});
|