@workflow/web-shared 5.0.0-beta.10 → 5.0.0-beta.12
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/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -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 +9 -4
- package/dist/components/new-trace-viewer/components/split-pane.d.ts +3 -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 +10 -4
- package/dist/components/new-trace-viewer/components/timeline.d.ts +1 -1
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +6 -8
- package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts +2 -0
- package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.js +19 -0
- package/dist/components/new-trace-viewer/components/use-row-window.d.ts +20 -0
- package/dist/components/new-trace-viewer/components/use-row-window.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/use-row-window.js +69 -0
- package/dist/components/new-trace-viewer/context.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/context.js +2 -2
- package/dist/components/new-trace-viewer/trace-viewer.d.ts +4 -1
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +19 -6
- package/dist/components/trace-viewer-new.d.ts +5 -1
- package/dist/components/trace-viewer-new.d.ts.map +1 -1
- package/dist/components/trace-viewer-new.js +7 -8
- package/dist/components/ui/icon-button.d.ts +8 -2
- package/dist/components/ui/icon-button.d.ts.map +1 -1
- package/dist/components/ui/icon-button.js +4 -4
- package/dist/components/ui/kbd.d.ts +6 -0
- package/dist/components/ui/kbd.d.ts.map +1 -0
- package/dist/components/ui/kbd.js +6 -0
- package/dist/components/ui/tooltip.d.ts +10 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +12 -0
- package/dist/hooks/use-intersection-observer.d.ts +17 -0
- package/dist/hooks/use-intersection-observer.d.ts.map +1 -0
- package/dist/hooks/use-intersection-observer.js +29 -0
- package/dist/hooks/use-load-more-on-scroll.d.ts +21 -0
- package/dist/hooks/use-load-more-on-scroll.d.ts.map +1 -0
- package/dist/hooks/use-load-more-on-scroll.js +28 -0
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +26 -2
- package/package.json +4 -4
- package/src/components/index.ts +1 -0
- package/src/components/new-trace-viewer/components/event-list.tsx +19 -11
- package/src/components/new-trace-viewer/components/split-pane.tsx +15 -2
- package/src/components/new-trace-viewer/components/timeline.tsx +27 -19
- package/src/components/new-trace-viewer/components/trace-viewer-skeleton.tsx +94 -0
- package/src/components/new-trace-viewer/components/use-row-window.ts +93 -0
- package/src/components/new-trace-viewer/context.tsx +1 -3
- package/src/components/new-trace-viewer/trace-viewer.tsx +90 -22
- package/src/components/trace-viewer-new.tsx +20 -12
- package/src/components/ui/icon-button.tsx +7 -11
- package/src/components/ui/kbd.tsx +21 -0
- package/src/components/ui/tooltip.tsx +47 -0
- package/src/hooks/use-intersection-observer.ts +57 -0
- package/src/hooks/use-load-more-on-scroll.ts +49 -0
- package/src/lib/hydration.ts +28 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Children,
|
|
5
5
|
type ReactNode,
|
|
6
6
|
type PointerEvent as ReactPointerEvent,
|
|
7
|
+
type RefObject,
|
|
7
8
|
useCallback,
|
|
8
9
|
useEffect,
|
|
9
10
|
useRef,
|
|
@@ -28,6 +29,7 @@ export interface SplitPaneProps {
|
|
|
28
29
|
startHeader?: ReactNode;
|
|
29
30
|
/** Fixed (non-scrolling) header rendered above the end pane. */
|
|
30
31
|
endHeader?: ReactNode;
|
|
32
|
+
scrollContainerRef?: RefObject<HTMLDivElement | null>;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export function SplitPane({
|
|
@@ -36,6 +38,7 @@ export function SplitPane({
|
|
|
36
38
|
defaultStartWidth = DEFAULT_START_PX,
|
|
37
39
|
startHeader,
|
|
38
40
|
endHeader,
|
|
41
|
+
scrollContainerRef,
|
|
39
42
|
}: SplitPaneProps) {
|
|
40
43
|
const parts = Children.toArray(children);
|
|
41
44
|
if (parts.length !== 2) {
|
|
@@ -57,6 +60,16 @@ export function SplitPane({
|
|
|
57
60
|
};
|
|
58
61
|
}, []);
|
|
59
62
|
|
|
63
|
+
const setContainerRef = useCallback(
|
|
64
|
+
(node: HTMLDivElement | null) => {
|
|
65
|
+
containerRef.current = node;
|
|
66
|
+
if (scrollContainerRef) {
|
|
67
|
+
scrollContainerRef.current = node;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[scrollContainerRef]
|
|
71
|
+
);
|
|
72
|
+
|
|
60
73
|
const clampPx = useCallback((px: number) => {
|
|
61
74
|
const el = containerRef.current;
|
|
62
75
|
if (!el) return px;
|
|
@@ -152,7 +165,7 @@ export function SplitPane({
|
|
|
152
165
|
<div>{endHeader}</div>
|
|
153
166
|
</div>
|
|
154
167
|
<div
|
|
155
|
-
ref={
|
|
168
|
+
ref={setContainerRef}
|
|
156
169
|
className={cn(
|
|
157
170
|
'grid flex-1 min-h-0 overflow-x-hidden overflow-y-auto',
|
|
158
171
|
isDragging && 'select-none'
|
|
@@ -169,7 +182,7 @@ export function SplitPane({
|
|
|
169
182
|
|
|
170
183
|
return (
|
|
171
184
|
<div
|
|
172
|
-
ref={
|
|
185
|
+
ref={setContainerRef}
|
|
173
186
|
className={cn(
|
|
174
187
|
'grid h-full min-h-0 content-start overflow-x-hidden overflow-y-auto',
|
|
175
188
|
isDragging && 'select-none',
|
|
@@ -6,14 +6,15 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
6
6
|
import { cn } from '../../../lib/utils';
|
|
7
7
|
import type { Span } from '../../trace-viewer/types';
|
|
8
8
|
import { formatDuration, getHighResInMs } from '../../trace-viewer/util/timing';
|
|
9
|
-
import type { Segment, SegmentStatus, TimeMarker } from '../utils';
|
|
10
9
|
import { isSpanDimmedBySearch, type SpanSearchResult } from '../search';
|
|
10
|
+
import type { Segment, SegmentStatus, TimeMarker } from '../utils';
|
|
11
11
|
import {
|
|
12
12
|
computeSpanGaps,
|
|
13
13
|
computeSpanSegments,
|
|
14
14
|
getResourceColor,
|
|
15
15
|
getSpanDurationMs,
|
|
16
16
|
} from '../utils';
|
|
17
|
+
import { ROW_HEIGHT_PX, useRowWindow } from './use-row-window';
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Constants
|
|
@@ -365,10 +366,6 @@ export { TimelineBar };
|
|
|
365
366
|
// DeltaIndicator (Alt-key gap overlay)
|
|
366
367
|
// ---------------------------------------------------------------------------
|
|
367
368
|
|
|
368
|
-
// Row and indicator sizes are local to DeltaIndicator since it's the only
|
|
369
|
-
// place that needs to compute Y positions from a row index. ROW_HEIGHT must
|
|
370
|
-
// match the `h-10` (40px) row used in TimelineBar.
|
|
371
|
-
const DELTA_ROW_HEIGHT_PX = 40;
|
|
372
369
|
const DELTA_CAP_HEIGHT_PX = 8;
|
|
373
370
|
// Vertical offset to sit the indicator inside the gap between row N and N+1,
|
|
374
371
|
// aligned with where the bar starts in the next row (rows center a 24px bar
|
|
@@ -386,7 +383,7 @@ const DeltaIndicator = memo(function DeltaIndicator({
|
|
|
386
383
|
label: string;
|
|
387
384
|
rowIndex: number;
|
|
388
385
|
}) {
|
|
389
|
-
const centerY = DELTA_ROW_OFFSET_PX + (rowIndex + 1) *
|
|
386
|
+
const centerY = DELTA_ROW_OFFSET_PX + (rowIndex + 1) * ROW_HEIGHT_PX;
|
|
390
387
|
|
|
391
388
|
return (
|
|
392
389
|
<div
|
|
@@ -473,6 +470,11 @@ export function Timeline({
|
|
|
473
470
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
474
471
|
const viewDuration = viewEnd - viewStart;
|
|
475
472
|
const timelineWidth = Math.max(0, containerWidth - TIMELINE_PADDING_PX * 2);
|
|
473
|
+
const { start, end } = useRowWindow(
|
|
474
|
+
containerRef,
|
|
475
|
+
spans.length,
|
|
476
|
+
ROW_HEIGHT_PX
|
|
477
|
+
);
|
|
476
478
|
|
|
477
479
|
useEffect(() => {
|
|
478
480
|
const el = containerRef.current;
|
|
@@ -490,7 +492,11 @@ export function Timeline({
|
|
|
490
492
|
);
|
|
491
493
|
|
|
492
494
|
return (
|
|
493
|
-
<div
|
|
495
|
+
<div
|
|
496
|
+
ref={containerRef}
|
|
497
|
+
className="relative h-full overflow-hidden"
|
|
498
|
+
style={{ minHeight: spans.length * ROW_HEIGHT_PX }}
|
|
499
|
+
>
|
|
494
500
|
<div
|
|
495
501
|
aria-hidden
|
|
496
502
|
className="absolute inset-y-0 pointer-events-none"
|
|
@@ -518,18 +524,20 @@ export function Timeline({
|
|
|
518
524
|
/>
|
|
519
525
|
</div>
|
|
520
526
|
)}
|
|
521
|
-
{
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
527
|
+
<div style={{ transform: `translateY(${start * ROW_HEIGHT_PX}px)` }}>
|
|
528
|
+
{spans.slice(start, end).map((span) => (
|
|
529
|
+
<TimelineBar
|
|
530
|
+
key={span.spanId}
|
|
531
|
+
span={span}
|
|
532
|
+
viewStart={viewStart}
|
|
533
|
+
viewDuration={viewDuration}
|
|
534
|
+
containerWidth={timelineWidth}
|
|
535
|
+
isSelected={selectedId === span.spanId}
|
|
536
|
+
isDimmed={isSpanDimmedBySearch(span.spanId, searchResult)}
|
|
537
|
+
onSelect={onSelect}
|
|
538
|
+
/>
|
|
539
|
+
))}
|
|
540
|
+
</div>
|
|
533
541
|
{altHeld && (
|
|
534
542
|
<div
|
|
535
543
|
aria-hidden
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Skeleton } from '../../ui/skeleton';
|
|
2
|
+
|
|
3
|
+
const COL_TEMPLATE = '340px 1px minmax(50px, 1fr)';
|
|
4
|
+
|
|
5
|
+
const ROWS: { id: string; name: number; off: number; bar: number }[] = [
|
|
6
|
+
{ id: 'r0', name: 62, off: 0, bar: 72 },
|
|
7
|
+
{ id: 'r1', name: 78, off: 6, bar: 48 },
|
|
8
|
+
{ id: 'r2', name: 50, off: 10, bar: 55 },
|
|
9
|
+
{ id: 'r3', name: 84, off: 18, bar: 30 },
|
|
10
|
+
{ id: 'r4', name: 45, off: 18, bar: 42 },
|
|
11
|
+
{ id: 'r5', name: 66, off: 34, bar: 38 },
|
|
12
|
+
{ id: 'r6', name: 55, off: 41, bar: 25 },
|
|
13
|
+
{ id: 'r7', name: 40, off: 50, bar: 30 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const HEADER_MARKERS = ['m0', 'm1', 'm2', 'm3'];
|
|
17
|
+
|
|
18
|
+
const HeaderDivider = () => (
|
|
19
|
+
<div className="flex justify-center">
|
|
20
|
+
<span aria-hidden className="h-full w-px bg-gray-alpha-400" />
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export function TraceViewerSkeleton() {
|
|
25
|
+
return (
|
|
26
|
+
<output
|
|
27
|
+
aria-busy="true"
|
|
28
|
+
aria-label="Loading trace"
|
|
29
|
+
className="flex flex-col w-full h-full min-h-0 bg-background-100"
|
|
30
|
+
>
|
|
31
|
+
<span className="sr-only">Loading trace…</span>
|
|
32
|
+
|
|
33
|
+
{/* Header row: search header | divider | timeline header */}
|
|
34
|
+
<div
|
|
35
|
+
className="shrink-0 grid"
|
|
36
|
+
style={{ gridTemplateColumns: COL_TEMPLATE }}
|
|
37
|
+
>
|
|
38
|
+
<div className="h-10 min-h-10 flex items-center border-b border-gray-alpha-400 pl-4 pr-2 gap-1.5">
|
|
39
|
+
<Skeleton className="w-3.5 h-3.5 shrink-0 rounded-sm" />
|
|
40
|
+
<Skeleton className="h-3.5 w-40" />
|
|
41
|
+
</div>
|
|
42
|
+
<HeaderDivider />
|
|
43
|
+
<div className="h-10 min-h-10 flex items-end border-b border-gray-alpha-400 px-4 pb-1 gap-2">
|
|
44
|
+
<div className="relative flex-1 flex items-end justify-between">
|
|
45
|
+
{HEADER_MARKERS.map((id) => (
|
|
46
|
+
<Skeleton key={id} className="h-3.5 w-9" />
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Content row: event list | gutter | timeline */}
|
|
53
|
+
<div
|
|
54
|
+
className="grid flex-1 min-h-0 overflow-hidden"
|
|
55
|
+
style={{ gridTemplateColumns: COL_TEMPLATE }}
|
|
56
|
+
>
|
|
57
|
+
{/* Sidebar event rows */}
|
|
58
|
+
<div className="block overflow-visible">
|
|
59
|
+
<ul className="block divide-y divide-gray-alpha-400 border-b border-gray-alpha-400">
|
|
60
|
+
{ROWS.map((row) => (
|
|
61
|
+
<li key={row.id} className="h-10 flex items-center pl-4 pr-2">
|
|
62
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
63
|
+
<Skeleton className="w-4 h-4 shrink-0 rounded-sm" />
|
|
64
|
+
<Skeleton
|
|
65
|
+
className="h-3.5"
|
|
66
|
+
style={{ width: `${row.name}%` }}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<Skeleton className="ml-2 h-3.5 w-10 shrink-0" />
|
|
70
|
+
</li>
|
|
71
|
+
))}
|
|
72
|
+
</ul>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Gutter */}
|
|
76
|
+
<span aria-hidden className="h-full w-px bg-gray-alpha-400" />
|
|
77
|
+
|
|
78
|
+
{/* Timeline bars */}
|
|
79
|
+
<div className="relative min-h-0">
|
|
80
|
+
{ROWS.map((row) => (
|
|
81
|
+
<div key={row.id} className="relative h-10">
|
|
82
|
+
<div className="absolute inset-x-4 inset-y-0">
|
|
83
|
+
<Skeleton
|
|
84
|
+
className="absolute top-1/2 -translate-y-1/2 h-6 rounded-[0.25rem]"
|
|
85
|
+
style={{ left: `${row.off}%`, width: `${row.bar}%` }}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</output>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type RefObject, useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
/** Rows rendered on the first paint, before the container has been measured. */
|
|
6
|
+
const INITIAL_VISIBLE_ROWS = 60;
|
|
7
|
+
|
|
8
|
+
export interface RowWindow {
|
|
9
|
+
/** First visible row index (inclusive), already padded by overscan. */
|
|
10
|
+
start: number;
|
|
11
|
+
/** Last visible row index (exclusive), already padded by overscan. */
|
|
12
|
+
end: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Nearest scrollable ancestor of `el` (the shared `SplitPane` container). */
|
|
16
|
+
function getScrollParent(el: HTMLElement | null): HTMLElement | null {
|
|
17
|
+
let node = el?.parentElement ?? null;
|
|
18
|
+
while (node) {
|
|
19
|
+
const overflowY = getComputedStyle(node).overflowY;
|
|
20
|
+
if (overflowY === 'auto' || overflowY === 'scroll') return node;
|
|
21
|
+
node = node.parentElement;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fixed-height windowing over a shared scroll container.
|
|
28
|
+
*
|
|
29
|
+
* The events list and timeline both render one `rowHeight`-tall row per span
|
|
30
|
+
* and scroll together inside the same `SplitPane`. Each caller passes a ref to
|
|
31
|
+
* its own root; the hook walks up to the shared scrollable ancestor, so the
|
|
32
|
+
* scroll container doesn't have to be threaded through props. Same scroll
|
|
33
|
+
* position + same row height means both panes window to the same range.
|
|
34
|
+
*/
|
|
35
|
+
export function useRowWindow(
|
|
36
|
+
ref: RefObject<HTMLElement | null>,
|
|
37
|
+
rowCount: number,
|
|
38
|
+
rowHeight: number,
|
|
39
|
+
overscan = 12
|
|
40
|
+
): RowWindow {
|
|
41
|
+
// Bound the pre-measurement window so the first paint of a large trace
|
|
42
|
+
// doesn't render every row before the effect narrows it down.
|
|
43
|
+
const [range, setRange] = useState<RowWindow>(() => ({
|
|
44
|
+
start: 0,
|
|
45
|
+
end: Math.min(rowCount, INITIAL_VISIBLE_ROWS),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const self = ref.current;
|
|
50
|
+
const el = getScrollParent(self);
|
|
51
|
+
if (!self || !el) return;
|
|
52
|
+
|
|
53
|
+
let raf = 0;
|
|
54
|
+
const measure = () => {
|
|
55
|
+
raf = 0;
|
|
56
|
+
// Offset of the list's top within the scroll container's content, so the
|
|
57
|
+
// window stays correct even if the list isn't flush with the top.
|
|
58
|
+
const offset =
|
|
59
|
+
self.getBoundingClientRect().top -
|
|
60
|
+
el.getBoundingClientRect().top +
|
|
61
|
+
el.scrollTop;
|
|
62
|
+
const top = el.scrollTop - offset;
|
|
63
|
+
const start = Math.max(0, Math.floor(top / rowHeight) - overscan);
|
|
64
|
+
const end = Math.min(
|
|
65
|
+
rowCount,
|
|
66
|
+
Math.ceil((top + el.clientHeight) / rowHeight) + overscan
|
|
67
|
+
);
|
|
68
|
+
setRange((prev) =>
|
|
69
|
+
prev.start === start && prev.end === end ? prev : { start, end }
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const onScroll = () => {
|
|
74
|
+
if (!raf) raf = requestAnimationFrame(measure);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
measure();
|
|
78
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
79
|
+
const ro = new ResizeObserver(measure);
|
|
80
|
+
ro.observe(el);
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
el.removeEventListener('scroll', onScroll);
|
|
84
|
+
ro.disconnect();
|
|
85
|
+
if (raf) cancelAnimationFrame(raf);
|
|
86
|
+
};
|
|
87
|
+
}, [ref, rowCount, rowHeight, overscan]);
|
|
88
|
+
|
|
89
|
+
return range;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Row height (px) shared by the events list and timeline rows (`h-10`). */
|
|
93
|
+
export const ROW_HEIGHT_PX = 40;
|
|
@@ -28,9 +28,7 @@ export function ActiveSpanProvider({
|
|
|
28
28
|
spans: Span[];
|
|
29
29
|
children: ReactNode;
|
|
30
30
|
}) {
|
|
31
|
-
const [activeSpanId, setActiveSpanId] = useState<string | null>(
|
|
32
|
-
spans[0]?.spanId ?? null
|
|
33
|
-
);
|
|
31
|
+
const [activeSpanId, setActiveSpanId] = useState<string | null>(null);
|
|
34
32
|
|
|
35
33
|
useEffect(() => {
|
|
36
34
|
setActiveSpanId((currentSpanId) => {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
useRef,
|
|
20
20
|
useState,
|
|
21
21
|
} from 'react';
|
|
22
|
+
import { useLoadMoreOnScroll } from '../../hooks/use-load-more-on-scroll';
|
|
22
23
|
import { useReducedMotion } from '../../hooks/use-reduced-motion';
|
|
23
24
|
import { ErrorBoundary } from '../error-boundary';
|
|
24
25
|
import {
|
|
@@ -29,6 +30,8 @@ import { useSidebarDataOptional } from '../sidebar/sidebar-data-context';
|
|
|
29
30
|
import type { Trace } from '../trace-viewer/types';
|
|
30
31
|
import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
|
|
31
32
|
import { IconButton } from '../ui/icon-button';
|
|
33
|
+
import { Spinner } from '../ui/spinner';
|
|
34
|
+
import { Kbd } from '../ui/kbd';
|
|
32
35
|
import EventList from './components/event-list';
|
|
33
36
|
import { SplitPane } from './components/split-pane';
|
|
34
37
|
import {
|
|
@@ -36,6 +39,12 @@ import {
|
|
|
36
39
|
Timeline,
|
|
37
40
|
TimelineHeader,
|
|
38
41
|
} from './components/timeline';
|
|
42
|
+
import {
|
|
43
|
+
Tooltip,
|
|
44
|
+
TooltipContent,
|
|
45
|
+
TooltipProvider,
|
|
46
|
+
TooltipTrigger,
|
|
47
|
+
} from '../ui/tooltip';
|
|
39
48
|
import { ActiveSpanProvider, useActiveSpan } from './context';
|
|
40
49
|
import { DetailPanel } from './detail-panel';
|
|
41
50
|
import { searchSpans } from './search';
|
|
@@ -43,6 +52,9 @@ import { computeRootBounds, computeTimeMarkers } from './utils';
|
|
|
43
52
|
|
|
44
53
|
interface NewTraceViewerProps {
|
|
45
54
|
trace: Trace;
|
|
55
|
+
onLoadMore?: () => void | Promise<void>;
|
|
56
|
+
hasMore?: boolean;
|
|
57
|
+
isLoadingMore?: boolean;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
const MIN_VIEWPORT_MS = 0.001;
|
|
@@ -152,15 +164,32 @@ function useSelectedSpanInfo(): SelectedSpanInfo | null {
|
|
|
152
164
|
// Root component
|
|
153
165
|
// ---------------------------------------------------------------------------
|
|
154
166
|
|
|
155
|
-
export function NewTraceViewer({
|
|
167
|
+
export function NewTraceViewer({
|
|
168
|
+
trace,
|
|
169
|
+
onLoadMore,
|
|
170
|
+
hasMore,
|
|
171
|
+
isLoadingMore,
|
|
172
|
+
}: NewTraceViewerProps): ReactNode {
|
|
156
173
|
return (
|
|
157
|
-
<
|
|
158
|
-
<
|
|
159
|
-
|
|
174
|
+
<TooltipProvider delayDuration={300}>
|
|
175
|
+
<ActiveSpanProvider spans={trace.spans}>
|
|
176
|
+
<NewTraceViewerContent
|
|
177
|
+
trace={trace}
|
|
178
|
+
onLoadMore={onLoadMore}
|
|
179
|
+
hasMore={hasMore}
|
|
180
|
+
isLoadingMore={isLoadingMore}
|
|
181
|
+
/>
|
|
182
|
+
</ActiveSpanProvider>
|
|
183
|
+
</TooltipProvider>
|
|
160
184
|
);
|
|
161
185
|
}
|
|
162
186
|
|
|
163
|
-
function NewTraceViewerContent({
|
|
187
|
+
function NewTraceViewerContent({
|
|
188
|
+
trace,
|
|
189
|
+
onLoadMore,
|
|
190
|
+
hasMore,
|
|
191
|
+
isLoadingMore,
|
|
192
|
+
}: NewTraceViewerProps): ReactNode {
|
|
164
193
|
const { activeSpan, activeSpanId, setActiveSpan, clearActiveSpan } =
|
|
165
194
|
useActiveSpan();
|
|
166
195
|
|
|
@@ -177,6 +206,16 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
177
206
|
|
|
178
207
|
const root = useMemo(() => computeRootBounds(trace.spans), [trace.spans]);
|
|
179
208
|
|
|
209
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
210
|
+
const loadMore = useCallback(() => {
|
|
211
|
+
void onLoadMore?.();
|
|
212
|
+
}, [onLoadMore]);
|
|
213
|
+
const loadMoreSentinelRef = useLoadMoreOnScroll(loadMore, {
|
|
214
|
+
hasMore: Boolean(onLoadMore && hasMore),
|
|
215
|
+
isLoadingMore: Boolean(isLoadingMore),
|
|
216
|
+
rootRef: scrollContainerRef,
|
|
217
|
+
});
|
|
218
|
+
|
|
180
219
|
const { viewport, setViewport, animateTo } = useAnimatedViewport({
|
|
181
220
|
start: root.startTime,
|
|
182
221
|
end: root.startTime + root.duration,
|
|
@@ -523,13 +562,14 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
523
562
|
<div
|
|
524
563
|
data-pane="pane-root"
|
|
525
564
|
data-has-detail={activeSpan ? '' : undefined}
|
|
526
|
-
className="grid w-full h-full max-h-full grid-cols-[minmax(100px,1fr)] data-[has-detail]:grid-cols-[minmax(100px,1fr)_clamp(280px,
|
|
565
|
+
className="grid w-full h-full max-h-full grid-cols-[minmax(100px,1fr)] data-[has-detail]:grid-cols-[minmax(100px,1fr)_clamp(280px,360px,100%)]"
|
|
527
566
|
>
|
|
528
567
|
<div
|
|
529
568
|
id="trace-parent"
|
|
530
569
|
className="grid grid-rows-[1fr] h-full min-h-0 overflow-hidden relative bg-background-100"
|
|
531
570
|
>
|
|
532
571
|
<SplitPane
|
|
572
|
+
scrollContainerRef={scrollContainerRef}
|
|
533
573
|
startHeader={
|
|
534
574
|
<div className="bg-background-100 border-b border-gray-alpha-400 h-10 min-h-10 flex items-center pl-4 pr-2 gap-1.5">
|
|
535
575
|
<Search className="w-3.5 h-3.5 shrink-0 text-gray-800" />
|
|
@@ -575,6 +615,14 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
575
615
|
searchResult={searchResult}
|
|
576
616
|
onSelectSpan={handleSelectSpan}
|
|
577
617
|
/>
|
|
618
|
+
<div ref={loadMoreSentinelRef} className="flex justify-center">
|
|
619
|
+
{isLoadingMore ? (
|
|
620
|
+
<div className="flex items-center justify-center gap-2 py-3 text-sm text-gray-800">
|
|
621
|
+
<Spinner size={14} />
|
|
622
|
+
<span>Loading spans…</span>
|
|
623
|
+
</div>
|
|
624
|
+
) : null}
|
|
625
|
+
</div>
|
|
578
626
|
</div>
|
|
579
627
|
{/* biome-ignore lint/a11y/noStaticElementInteractions: timeline hover and wheel gestures are pointer-only annotations */}
|
|
580
628
|
<div
|
|
@@ -634,22 +682,42 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
634
682
|
{selectedSpanName}
|
|
635
683
|
</span>
|
|
636
684
|
<div className="flex items-center gap-0.5 shrink-0">
|
|
637
|
-
<
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
685
|
+
<Tooltip>
|
|
686
|
+
<TooltipTrigger asChild>
|
|
687
|
+
<IconButton
|
|
688
|
+
aria-label="Navigate up"
|
|
689
|
+
aria-keyshortcuts="K"
|
|
690
|
+
onClick={handleSelectPrevSpan}
|
|
691
|
+
disabled={!prevSpanId}
|
|
692
|
+
>
|
|
693
|
+
<ChevronUp className="w-4 h-4" />
|
|
694
|
+
</IconButton>
|
|
695
|
+
</TooltipTrigger>
|
|
696
|
+
{prevSpanId ? (
|
|
697
|
+
<TooltipContent>
|
|
698
|
+
Navigate up
|
|
699
|
+
<Kbd>K</Kbd>
|
|
700
|
+
</TooltipContent>
|
|
701
|
+
) : null}
|
|
702
|
+
</Tooltip>
|
|
703
|
+
<Tooltip>
|
|
704
|
+
<TooltipTrigger asChild>
|
|
705
|
+
<IconButton
|
|
706
|
+
aria-label="Navigate down"
|
|
707
|
+
aria-keyshortcuts="J"
|
|
708
|
+
onClick={handleSelectNextSpan}
|
|
709
|
+
disabled={!nextSpanId}
|
|
710
|
+
>
|
|
711
|
+
<ChevronDown className="w-4 h-4" />
|
|
712
|
+
</IconButton>
|
|
713
|
+
</TooltipTrigger>
|
|
714
|
+
{nextSpanId ? (
|
|
715
|
+
<TooltipContent>
|
|
716
|
+
Navigate down
|
|
717
|
+
<Kbd>J</Kbd>
|
|
718
|
+
</TooltipContent>
|
|
719
|
+
) : null}
|
|
720
|
+
</Tooltip>
|
|
653
721
|
<div aria-hidden className="w-px h-4 bg-gray-alpha-400 mx-1" />
|
|
654
722
|
<IconButton
|
|
655
723
|
aria-label="Close span details"
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
1
|
import type { WorkflowRun } from '@workflow/core/runtime';
|
|
3
2
|
import type { Event } from '@workflow/world';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
4
|
import { buildTrace, type TraceWithMeta } from '../lib/trace-builder';
|
|
5
|
+
import { TraceViewerSkeleton } from './new-trace-viewer/components/trace-viewer-skeleton';
|
|
5
6
|
import { NewTraceViewer as NewTraceViewerComponent } from './new-trace-viewer/trace-viewer';
|
|
6
7
|
import {
|
|
7
|
-
SidebarDataProvider,
|
|
8
8
|
type SidebarDataContextValue,
|
|
9
|
+
SidebarDataProvider,
|
|
9
10
|
} from './sidebar/sidebar-data-context';
|
|
10
11
|
import type { Trace } from './trace-viewer/types';
|
|
11
12
|
|
|
@@ -13,34 +14,41 @@ const NewTraceViewer = ({
|
|
|
13
14
|
run,
|
|
14
15
|
events,
|
|
15
16
|
sidebarData,
|
|
17
|
+
onLoadMore,
|
|
18
|
+
hasMore,
|
|
19
|
+
isLoadingMore,
|
|
20
|
+
loading = false,
|
|
16
21
|
}: {
|
|
17
22
|
run: WorkflowRun;
|
|
18
23
|
events: Event[];
|
|
19
24
|
sidebarData: SidebarDataContextValue;
|
|
25
|
+
onLoadMore?: () => void | Promise<void>;
|
|
26
|
+
hasMore?: boolean;
|
|
27
|
+
isLoadingMore?: boolean;
|
|
28
|
+
loading?: boolean;
|
|
20
29
|
}) => {
|
|
21
|
-
// Build trace only when actual data changes — no timer-driven rebuilds.
|
|
22
|
-
// Active span widths are animated imperatively by useLiveTick at 60fps.
|
|
23
30
|
const traceWithMeta: TraceWithMeta | undefined = useMemo(() => {
|
|
24
31
|
if (!run?.runId) {
|
|
25
32
|
return undefined;
|
|
26
33
|
}
|
|
27
34
|
return buildTrace(run, events, new Date());
|
|
28
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- `new Date()` is intentionally not a dep
|
|
35
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- `new Date()` is intentionally not a dep
|
|
29
36
|
}, [run, events]);
|
|
30
37
|
const trace = traceWithMeta;
|
|
31
38
|
|
|
32
|
-
if (!trace) {
|
|
33
|
-
return
|
|
34
|
-
<div className="relative w-full h-full flex items-center justify-center">
|
|
35
|
-
<div className="text-gray-500 text-sm">Loading trace…</div>
|
|
36
|
-
</div>
|
|
37
|
-
);
|
|
39
|
+
if (!trace || (loading && events.length === 0)) {
|
|
40
|
+
return <TraceViewerSkeleton />;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
return (
|
|
41
44
|
<SidebarDataProvider value={sidebarData}>
|
|
42
45
|
<div className="relative w-full h-full flex">
|
|
43
|
-
<NewTraceViewerComponent
|
|
46
|
+
<NewTraceViewerComponent
|
|
47
|
+
trace={trace as Trace}
|
|
48
|
+
onLoadMore={onLoadMore}
|
|
49
|
+
hasMore={hasMore}
|
|
50
|
+
isLoadingMore={isLoadingMore}
|
|
51
|
+
/>
|
|
44
52
|
</div>
|
|
45
53
|
</SidebarDataProvider>
|
|
46
54
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
-
import
|
|
2
|
+
import * as React from 'react';
|
|
3
3
|
import { cn } from '../../lib/utils';
|
|
4
4
|
|
|
5
5
|
const iconButtonVariants = cva(
|
|
@@ -38,20 +38,16 @@ export type IconButtonProps = Omit<
|
|
|
38
38
|
type?: 'button' | 'submit' | 'reset';
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
export
|
|
42
|
-
className,
|
|
43
|
-
variant,
|
|
44
|
-
size,
|
|
45
|
-
type = 'button',
|
|
46
|
-
...props
|
|
47
|
-
}: IconButtonProps) {
|
|
48
|
-
return (
|
|
41
|
+
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
42
|
+
({ className, variant, size, type = 'button', ...props }, ref) => (
|
|
49
43
|
<button
|
|
44
|
+
ref={ref}
|
|
50
45
|
type={type}
|
|
51
46
|
className={cn(iconButtonVariants({ variant, size, className }))}
|
|
52
47
|
{...props}
|
|
53
48
|
/>
|
|
54
|
-
)
|
|
55
|
-
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
IconButton.displayName = 'IconButton';
|
|
56
52
|
|
|
57
53
|
export { iconButtonVariants };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
|
|
4
|
+
export function Kbd({
|
|
5
|
+
children,
|
|
6
|
+
className,
|
|
7
|
+
}: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}): ReactNode {
|
|
11
|
+
return (
|
|
12
|
+
<kbd
|
|
13
|
+
className={cn(
|
|
14
|
+
'inline-flex h-5 min-w-5 items-center justify-center rounded bg-background-100/20 px-1 text-center font-sans text-xs leading-none',
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</kbd>
|
|
20
|
+
);
|
|
21
|
+
}
|