@workflow/web-shared 5.0.0-beta.3 → 5.0.0-beta.5
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/new-trace-viewer/components/event-list.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/event-list.js +7 -4
- package/dist/components/new-trace-viewer/components/event-list.js.map +1 -1
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js.map +1 -0
- package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/split-pane.js +2 -3
- package/dist/components/new-trace-viewer/components/split-pane.js.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.d.ts +10 -11
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +57 -53
- package/dist/components/new-trace-viewer/components/timeline.js.map +1 -1
- package/dist/components/new-trace-viewer/context.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/context.js +5 -7
- package/dist/components/new-trace-viewer/context.js.map +1 -1
- package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/detail-panel.js +2 -1
- package/dist/components/new-trace-viewer/detail-panel.js.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +44 -18
- package/dist/components/new-trace-viewer/trace-viewer.js.map +1 -1
- package/dist/components/new-trace-viewer/utils.d.ts +3 -8
- package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/utils.js +12 -65
- package/dist/components/new-trace-viewer/utils.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +6 -0
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/trace-viewer/util/timing.d.ts +1 -1
- package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
- package/dist/components/trace-viewer/util/timing.js +5 -8
- package/dist/components/trace-viewer/util/timing.js.map +1 -1
- package/dist/components/ui/error-stack-block.d.ts.map +1 -1
- package/dist/components/ui/error-stack-block.js +24 -2
- package/dist/components/ui/error-stack-block.js.map +1 -1
- package/dist/components/workflow-trace-view.js +1 -1
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +17 -4
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/lib/hydration.d.ts +6 -0
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +124 -1
- package/dist/lib/hydration.js.map +1 -1
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +2 -1
- package/dist/lib/trace-builder.js.map +1 -1
- package/dist/lib/utils.d.ts +9 -7
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +27 -27
- package/dist/lib/utils.js.map +1 -1
- package/package.json +5 -4
- package/src/components/new-trace-viewer/components/event-list.tsx +26 -19
- package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
- package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
- package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
- package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
- package/src/components/new-trace-viewer/components/split-pane.tsx +2 -3
- package/src/components/new-trace-viewer/components/timeline.tsx +119 -115
- package/src/components/new-trace-viewer/context.tsx +7 -9
- package/src/components/new-trace-viewer/detail-panel.tsx +2 -1
- package/src/components/new-trace-viewer/trace-viewer.tsx +100 -32
- package/src/components/new-trace-viewer/utils.ts +19 -93
- package/src/components/sidebar/attribute-panel.tsx +5 -0
- package/src/components/trace-viewer/util/timing.ts +5 -9
- package/src/components/ui/error-stack-block.tsx +32 -4
- package/src/components/workflow-trace-view.tsx +1 -1
- package/src/components/workflow-traces/trace-span-construction.ts +23 -5
- package/src/lib/hydration.ts +132 -1
- package/src/lib/trace-builder.ts +2 -1
- 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,
|
|
9
|
+
import type { SegmentStatus, TimeMarker } from '../utils';
|
|
9
10
|
import {
|
|
10
|
-
computeCompressedTimeMarkers,
|
|
11
11
|
computeSpanGaps,
|
|
12
12
|
computeSpanSegments,
|
|
13
|
-
|
|
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: {
|
|
21
|
+
queued: { className: 'bg-gray-500' },
|
|
28
22
|
retrying: {
|
|
29
23
|
className: 'box-border bg-gray-500',
|
|
30
24
|
},
|
|
31
|
-
waiting: {
|
|
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-
|
|
29
|
+
sleeping: { className: 'bg-gray-500' },
|
|
36
30
|
received: { className: 'bg-blue-700' },
|
|
37
31
|
};
|
|
38
32
|
|
|
39
|
-
const
|
|
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 =
|
|
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
|
-
|
|
79
|
+
viewStart,
|
|
80
|
+
viewDuration,
|
|
82
81
|
containerWidth,
|
|
83
82
|
isSelected,
|
|
84
83
|
onClick,
|
|
85
84
|
}: {
|
|
86
85
|
span: Span;
|
|
87
|
-
|
|
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 =
|
|
94
|
+
const totalDurationMs = getSpanDurationMs(span);
|
|
95
95
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
const
|
|
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 =
|
|
102
|
+
const leftPct = leftFracRaw * 100;
|
|
101
103
|
const widthPct = widthFrac * 100;
|
|
102
104
|
|
|
103
105
|
const pixelWidth = widthFrac * containerWidth;
|
|
104
|
-
const
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
213
|
+
className="absolute inset-y-0"
|
|
229
214
|
style={{
|
|
230
|
-
left:
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
viewDuration,
|
|
249
|
-
rootStart,
|
|
250
|
-
compression,
|
|
251
|
-
isZoomed,
|
|
252
|
-
onResetZoom,
|
|
245
|
+
markers,
|
|
253
246
|
hoverInfo,
|
|
254
247
|
}: {
|
|
255
|
-
|
|
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-
|
|
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
|
|
254
|
+
{markers.map((m) => (
|
|
282
255
|
<span
|
|
283
|
-
key={
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
342
|
-
[spans,
|
|
311
|
+
() => computeSpanGaps(spans, viewStart, viewEnd),
|
|
312
|
+
[spans, viewStart, viewEnd]
|
|
343
313
|
);
|
|
344
314
|
|
|
345
315
|
return (
|
|
346
|
-
<div ref={containerRef} className="relative
|
|
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
|
|
350
|
-
style={{
|
|
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
|
-
|
|
358
|
-
|
|
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
|
|
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 (
|
|
37
|
+
if (!currentSpanId) {
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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 =
|
|
21
|
+
const durationMs = getSpanDurationMs(span);
|
|
21
22
|
const offsetMs = startMs - rootStart;
|
|
22
23
|
|
|
23
24
|
return (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
|
|
4
|
-
import { Search, X } from 'lucide-react';
|
|
4
|
+
import { RotateCcw, Search, X, ZoomIn, ZoomOut } from 'lucide-react';
|
|
5
5
|
import {
|
|
6
6
|
type ReactNode,
|
|
7
7
|
useCallback,
|
|
@@ -19,19 +19,22 @@ import { useSidebarDataOptional } from '../sidebar/sidebar-data-context';
|
|
|
19
19
|
import type { Trace } from '../trace-viewer/types';
|
|
20
20
|
import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
|
|
21
21
|
import { CopyButton } from './components/copy-button';
|
|
22
|
-
import { SplitPane } from './components/split-pane';
|
|
23
22
|
import EventList from './components/event-list';
|
|
24
|
-
import {
|
|
23
|
+
import { SplitPane } from './components/split-pane';
|
|
24
|
+
import {
|
|
25
|
+
Timeline,
|
|
26
|
+
TimelineHeader,
|
|
27
|
+
TIMELINE_PADDING_PX,
|
|
28
|
+
} from './components/timeline';
|
|
25
29
|
import { ActiveSpanProvider, useActiveSpan } from './context';
|
|
26
30
|
import { DetailPanel } from './detail-panel';
|
|
27
|
-
import {
|
|
31
|
+
import { computeRootBounds, computeTimeMarkers } from './utils';
|
|
28
32
|
|
|
29
33
|
interface NewTraceViewerProps {
|
|
30
34
|
trace: Trace;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
const MIN_VIEWPORT_MS = 0.001;
|
|
34
|
-
const TIMELINE_PADDING = 16;
|
|
35
38
|
|
|
36
39
|
interface Viewport {
|
|
37
40
|
start: number;
|
|
@@ -189,19 +192,51 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
189
192
|
|
|
190
193
|
const viewDuration = viewport.end - viewport.start;
|
|
191
194
|
|
|
192
|
-
const
|
|
193
|
-
() =>
|
|
194
|
-
[
|
|
195
|
+
const timeMarkers = useMemo(
|
|
196
|
+
() => computeTimeMarkers(viewDuration, viewport.start - root.startTime),
|
|
197
|
+
[viewDuration, viewport.start, root.startTime]
|
|
195
198
|
);
|
|
196
199
|
|
|
197
|
-
const isZoomed =
|
|
198
|
-
viewport.start > root.startTime + 0.01 ||
|
|
199
|
-
viewport.end < root.startTime + root.duration - 0.01;
|
|
200
|
-
|
|
201
200
|
const resetZoom = useCallback(() => {
|
|
202
201
|
animateTo({ start: root.startTime, end: root.startTime + root.duration });
|
|
203
202
|
}, [animateTo, root.startTime, root.duration]);
|
|
204
203
|
|
|
204
|
+
const ZOOM_FACTOR = 0.5;
|
|
205
|
+
|
|
206
|
+
const zoomBy = useCallback(
|
|
207
|
+
(factor: number) => {
|
|
208
|
+
const rootS = root.startTime;
|
|
209
|
+
const rootE = root.startTime + root.duration;
|
|
210
|
+
const rootD = root.duration;
|
|
211
|
+
|
|
212
|
+
setViewport((prev) => {
|
|
213
|
+
const prevDuration = prev.end - prev.start;
|
|
214
|
+
const center = (prev.start + prev.end) / 2;
|
|
215
|
+
const newDuration = Math.max(
|
|
216
|
+
MIN_VIEWPORT_MS,
|
|
217
|
+
Math.min(rootD, prevDuration * factor)
|
|
218
|
+
);
|
|
219
|
+
let newStart = center - newDuration / 2;
|
|
220
|
+
let newEnd = center + newDuration / 2;
|
|
221
|
+
|
|
222
|
+
if (newStart < rootS) {
|
|
223
|
+
newStart = rootS;
|
|
224
|
+
newEnd = rootS + newDuration;
|
|
225
|
+
}
|
|
226
|
+
if (newEnd > rootE) {
|
|
227
|
+
newEnd = rootE;
|
|
228
|
+
newStart = Math.max(rootS, rootE - newDuration);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { start: newStart, end: newEnd };
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
[setViewport, root.startTime, root.duration]
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const zoomIn = useCallback(() => zoomBy(ZOOM_FACTOR), [zoomBy]);
|
|
238
|
+
const zoomOut = useCallback(() => zoomBy(1 / ZOOM_FACTOR), [zoomBy]);
|
|
239
|
+
|
|
205
240
|
const handleSelectSpan = useCallback(
|
|
206
241
|
(spanId: string) => {
|
|
207
242
|
if (spanId === activeSpanId) {
|
|
@@ -292,22 +327,24 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
292
327
|
|
|
293
328
|
const hoverInfo = useMemo(() => {
|
|
294
329
|
if (hoverFraction == null) return null;
|
|
295
|
-
const absTime =
|
|
330
|
+
const absTime = viewport.start + hoverFraction * viewDuration;
|
|
296
331
|
const offset = absTime - root.startTime;
|
|
297
332
|
return { fraction: hoverFraction, label: formatDuration(offset, true) };
|
|
298
|
-
}, [hoverFraction,
|
|
333
|
+
}, [hoverFraction, viewport.start, viewDuration, root.startTime]);
|
|
299
334
|
|
|
300
335
|
const handleTimelineMouseMove = useCallback(
|
|
301
336
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
302
337
|
const el = timelineRef.current;
|
|
303
338
|
if (!el) return;
|
|
304
339
|
const rect = el.getBoundingClientRect();
|
|
305
|
-
const
|
|
306
|
-
const contentWidth = rect.width - padding * 2;
|
|
340
|
+
const contentWidth = rect.width - TIMELINE_PADDING_PX * 2;
|
|
307
341
|
if (contentWidth <= 0) return;
|
|
308
342
|
const fraction = Math.max(
|
|
309
343
|
0,
|
|
310
|
-
Math.min(
|
|
344
|
+
Math.min(
|
|
345
|
+
1,
|
|
346
|
+
(e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
|
|
347
|
+
)
|
|
311
348
|
);
|
|
312
349
|
setHoverFraction(fraction);
|
|
313
350
|
},
|
|
@@ -335,7 +372,7 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
335
372
|
e.preventDefault();
|
|
336
373
|
|
|
337
374
|
const rect = el.getBoundingClientRect();
|
|
338
|
-
const contentWidth = rect.width -
|
|
375
|
+
const contentWidth = rect.width - TIMELINE_PADDING_PX * 2;
|
|
339
376
|
if (contentWidth <= 0) return;
|
|
340
377
|
|
|
341
378
|
if (isZoomGesture) {
|
|
@@ -344,7 +381,10 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
344
381
|
|
|
345
382
|
const cursorFraction = Math.max(
|
|
346
383
|
0,
|
|
347
|
-
Math.min(
|
|
384
|
+
Math.min(
|
|
385
|
+
1,
|
|
386
|
+
(e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
|
|
387
|
+
)
|
|
348
388
|
);
|
|
349
389
|
const scaleFactor = Math.pow(2, dy / 200);
|
|
350
390
|
|
|
@@ -403,6 +443,10 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
403
443
|
const selectedSpanName = useMemo(() => {
|
|
404
444
|
if (!selectedSpan?.data) return 'Details';
|
|
405
445
|
const data = selectedSpan.data as Record<string, unknown>;
|
|
446
|
+
if (selectedSpan.resource === 'hook') {
|
|
447
|
+
return (data.token as string | undefined) ?? (data.hookId as string);
|
|
448
|
+
}
|
|
449
|
+
|
|
406
450
|
const stepName = data.stepName as string | undefined;
|
|
407
451
|
const workflowName = data.workflowName as string | undefined;
|
|
408
452
|
return (
|
|
@@ -413,19 +457,23 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
413
457
|
(data.hookId as string) ??
|
|
414
458
|
'Details'
|
|
415
459
|
);
|
|
416
|
-
}, [selectedSpan?.data]);
|
|
460
|
+
}, [selectedSpan?.data, selectedSpan?.resource]);
|
|
417
461
|
|
|
418
462
|
const selectedResource = selectedSpan?.resource as string | undefined;
|
|
419
463
|
const selectedResourceId = useMemo(() => {
|
|
420
464
|
if (!selectedSpan?.data) return undefined;
|
|
421
465
|
const data = selectedSpan.data as Record<string, unknown>;
|
|
466
|
+
if (selectedSpan.resource === 'hook') {
|
|
467
|
+
return (data.hookId as string | undefined) ?? selectedSpan.spanId;
|
|
468
|
+
}
|
|
469
|
+
|
|
422
470
|
return (
|
|
423
471
|
(data.stepId as string) ??
|
|
424
472
|
(data.runId as string) ??
|
|
425
473
|
(data.hookId as string) ??
|
|
426
474
|
selectedSpan.spanId
|
|
427
475
|
);
|
|
428
|
-
}, [selectedSpan?.data, selectedSpan?.spanId]);
|
|
476
|
+
}, [selectedSpan?.data, selectedSpan?.resource, selectedSpan?.spanId]);
|
|
429
477
|
|
|
430
478
|
return (
|
|
431
479
|
<div
|
|
@@ -463,15 +511,7 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
463
511
|
</div>
|
|
464
512
|
}
|
|
465
513
|
endHeader={
|
|
466
|
-
<TimelineHeader
|
|
467
|
-
viewStart={viewport.start}
|
|
468
|
-
viewDuration={viewDuration}
|
|
469
|
-
rootStart={root.startTime}
|
|
470
|
-
compression={compression}
|
|
471
|
-
isZoomed={isZoomed}
|
|
472
|
-
onResetZoom={resetZoom}
|
|
473
|
-
hoverInfo={hoverInfo}
|
|
474
|
-
/>
|
|
514
|
+
<TimelineHeader markers={timeMarkers} hoverInfo={hoverInfo} />
|
|
475
515
|
}
|
|
476
516
|
>
|
|
477
517
|
<div className="block overflow-visible">
|
|
@@ -483,14 +523,16 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
483
523
|
</div>
|
|
484
524
|
<div
|
|
485
525
|
ref={timelineRef}
|
|
486
|
-
className="block min-h-0 overflow-visible relative
|
|
526
|
+
className="block min-h-0 overflow-visible relative"
|
|
487
527
|
onDoubleClick={resetZoom}
|
|
488
528
|
onMouseMove={handleTimelineMouseMove}
|
|
489
529
|
onMouseLeave={handleTimelineMouseLeave}
|
|
490
530
|
>
|
|
491
531
|
<Timeline
|
|
492
532
|
spans={filteredSpans}
|
|
493
|
-
|
|
533
|
+
viewStart={viewport.start}
|
|
534
|
+
viewEnd={viewport.end}
|
|
535
|
+
markers={timeMarkers}
|
|
494
536
|
selectedId={activeSpanId}
|
|
495
537
|
onSelect={handleSelectSpan}
|
|
496
538
|
hoverFraction={hoverFraction}
|
|
@@ -498,6 +540,32 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
498
540
|
/>
|
|
499
541
|
</div>
|
|
500
542
|
</SplitPane>
|
|
543
|
+
<div className="absolute right-3 bottom-3 z-[5] flex items-center border border-gray-alpha-400 rounded-lg bg-background-100 shadow-sm overflow-hidden divide-x divide-gray-alpha-400">
|
|
544
|
+
<button
|
|
545
|
+
type="button"
|
|
546
|
+
className="flex items-center justify-center w-8 h-8 text-gray-900 cursor-pointer transition-colors duration-[120ms] ease-in-out hover:text-gray-1000 hover:bg-gray-alpha-100"
|
|
547
|
+
onClick={zoomOut}
|
|
548
|
+
aria-label="Zoom out"
|
|
549
|
+
>
|
|
550
|
+
<ZoomOut className="w-4 h-4" />
|
|
551
|
+
</button>
|
|
552
|
+
<button
|
|
553
|
+
type="button"
|
|
554
|
+
className="flex items-center justify-center w-8 h-8 text-gray-900 cursor-pointer transition-colors duration-[120ms] ease-in-out hover:text-gray-1000 hover:bg-gray-alpha-100"
|
|
555
|
+
onClick={resetZoom}
|
|
556
|
+
aria-label="Reset zoom"
|
|
557
|
+
>
|
|
558
|
+
<RotateCcw className="w-3.5 h-3.5" />
|
|
559
|
+
</button>
|
|
560
|
+
<button
|
|
561
|
+
type="button"
|
|
562
|
+
className="flex items-center justify-center w-8 h-8 text-gray-900 cursor-pointer transition-colors duration-[120ms] ease-in-out hover:text-gray-1000 hover:bg-gray-alpha-100"
|
|
563
|
+
onClick={zoomIn}
|
|
564
|
+
aria-label="Zoom in"
|
|
565
|
+
>
|
|
566
|
+
<ZoomIn className="w-4 h-4" />
|
|
567
|
+
</button>
|
|
568
|
+
</div>
|
|
501
569
|
</div>
|
|
502
570
|
|
|
503
571
|
{/* Detail panel */}
|