@workflow/web-shared 4.1.0-beta.52 → 4.1.0-beta.53
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/error-boundary.d.ts +15 -20
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +17 -31
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/event-list-view.d.ts +7 -6
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +492 -109
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/run-trace-view.d.ts +2 -1
- package/dist/components/run-trace-view.d.ts.map +1 -1
- package/dist/components/run-trace-view.js +2 -2
- package/dist/components/run-trace-view.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts +2 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +53 -142
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
- package/dist/components/sidebar/conversation-view.js +3 -17
- package/dist/components/sidebar/conversation-view.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +63 -10
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +4 -8
- package/dist/components/sidebar/events-list.js.map +1 -1
- package/dist/components/sidebar/resolve-hook-modal.d.ts +3 -0
- package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
- package/dist/components/sidebar/resolve-hook-modal.js +152 -3
- package/dist/components/sidebar/resolve-hook-modal.js.map +1 -1
- package/dist/components/stream-viewer.d.ts +7 -5
- package/dist/components/stream-viewer.d.ts.map +1 -1
- package/dist/components/stream-viewer.js +54 -22
- package/dist/components/stream-viewer.js.map +1 -1
- package/dist/components/trace-viewer/components/markers.d.ts +2 -1
- package/dist/components/trace-viewer/components/markers.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/markers.js +59 -20
- package/dist/components/trace-viewer/components/markers.js.map +1 -1
- package/dist/components/trace-viewer/components/node.d.ts +5 -1
- package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/node.js +250 -68
- package/dist/components/trace-viewer/components/node.js.map +1 -1
- package/dist/components/trace-viewer/components/span-content.d.ts +19 -0
- package/dist/components/trace-viewer/components/span-content.d.ts.map +1 -0
- package/dist/components/trace-viewer/components/span-content.js +137 -0
- package/dist/components/trace-viewer/components/span-content.js.map +1 -0
- package/dist/components/trace-viewer/components/span-detail-panel.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/span-detail-panel.js +3 -2
- package/dist/components/trace-viewer/components/span-detail-panel.js.map +1 -1
- package/dist/components/trace-viewer/components/span-segments.d.ts +50 -0
- package/dist/components/trace-viewer/components/span-segments.d.ts.map +1 -0
- package/dist/components/trace-viewer/components/span-segments.js +392 -0
- package/dist/components/trace-viewer/components/span-segments.js.map +1 -0
- package/dist/components/trace-viewer/components/span-strategies.d.ts +46 -0
- package/dist/components/trace-viewer/components/span-strategies.d.ts.map +1 -0
- package/dist/components/trace-viewer/components/span-strategies.js +108 -0
- package/dist/components/trace-viewer/components/span-strategies.js.map +1 -0
- package/dist/components/trace-viewer/context.d.ts +7 -6
- package/dist/components/trace-viewer/context.d.ts.map +1 -1
- package/dist/components/trace-viewer/context.js +47 -18
- package/dist/components/trace-viewer/context.js.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.d.ts +5 -1
- package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.js +87 -11
- package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.module.css +179 -6
- package/dist/components/trace-viewer/util/timing.d.ts +5 -0
- package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
- package/dist/components/trace-viewer/util/timing.js +12 -0
- package/dist/components/trace-viewer/util/timing.js.map +1 -1
- package/dist/components/trace-viewer/util/use-streaming-spans.d.ts +1 -1
- package/dist/components/trace-viewer/util/use-streaming-spans.d.ts.map +1 -1
- package/dist/components/trace-viewer/util/use-streaming-spans.js +29 -17
- package/dist/components/trace-viewer/util/use-streaming-spans.js.map +1 -1
- package/dist/components/trace-viewer/worker.js +3 -1
- package/dist/components/trace-viewer/worker.js.map +1 -1
- package/dist/components/ui/alert.js +3 -3
- package/dist/components/ui/alert.js.map +1 -1
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/card.js +2 -2
- package/dist/components/ui/card.js.map +1 -1
- package/dist/components/ui/data-inspector.d.ts +17 -0
- package/dist/components/ui/data-inspector.d.ts.map +1 -0
- package/dist/components/ui/data-inspector.js +184 -0
- package/dist/components/ui/data-inspector.js.map +1 -0
- package/dist/components/ui/error-card.d.ts.map +1 -1
- package/dist/components/ui/error-card.js +4 -1
- package/dist/components/ui/error-card.js.map +1 -1
- package/dist/components/ui/inspector-theme.d.ts +39 -24
- package/dist/components/ui/inspector-theme.d.ts.map +1 -1
- package/dist/components/ui/inspector-theme.js +90 -38
- package/dist/components/ui/inspector-theme.js.map +1 -1
- package/dist/components/ui/skeleton.d.ts +1 -1
- package/dist/components/ui/skeleton.d.ts.map +1 -1
- package/dist/components/ui/skeleton.js +2 -2
- package/dist/components/ui/skeleton.js.map +1 -1
- package/dist/components/workflow-trace-view.d.ts +3 -1
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +435 -21
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +2 -2
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +17 -3
- package/dist/lib/hydration.js.map +1 -1
- package/dist/styles.css +186 -0
- package/package.json +8 -7
- package/src/components/error-boundary.tsx +29 -40
- package/src/components/event-list-view.tsx +1000 -287
- package/src/components/index.ts +1 -0
- package/src/components/run-trace-view.tsx +3 -0
- package/src/components/sidebar/attribute-panel.tsx +58 -258
- package/src/components/sidebar/conversation-view.tsx +30 -27
- package/src/components/sidebar/entity-detail-panel.tsx +86 -20
- package/src/components/sidebar/events-list.tsx +4 -11
- package/src/components/sidebar/resolve-hook-modal.tsx +206 -47
- package/src/components/stream-viewer.tsx +138 -61
- package/src/components/trace-viewer/components/markers.tsx +69 -21
- package/src/components/trace-viewer/components/node.tsx +346 -100
- package/src/components/trace-viewer/components/span-content.tsx +247 -0
- package/src/components/trace-viewer/components/span-detail-panel.tsx +7 -2
- package/src/components/trace-viewer/components/span-segments.ts +516 -0
- package/src/components/trace-viewer/components/span-strategies.ts +205 -0
- package/src/components/trace-viewer/context.tsx +92 -40
- package/src/components/trace-viewer/trace-viewer.module.css +179 -6
- package/src/components/trace-viewer/trace-viewer.tsx +115 -11
- package/src/components/trace-viewer/util/timing.ts +13 -0
- package/src/components/trace-viewer/util/use-streaming-spans.ts +28 -17
- package/src/components/trace-viewer/worker.ts +4 -1
- package/src/components/ui/alert.tsx +3 -3
- package/src/components/ui/card.tsx +3 -5
- package/src/components/ui/data-inspector.tsx +318 -0
- package/src/components/ui/error-card.tsx +17 -6
- package/src/components/ui/inspector-theme.ts +127 -39
- package/src/components/ui/skeleton.tsx +3 -1
- package/src/components/workflow-trace-view.tsx +625 -26
- package/src/components/workflow-traces/trace-span-construction.ts +3 -2
- package/src/lib/hydration.ts +17 -8
- package/src/styles.css +186 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { clsx } from 'clsx';
|
|
4
4
|
import type { CSSProperties, MutableRefObject, ReactNode } from 'react';
|
|
5
|
-
import { memo, useRef } from 'react';
|
|
5
|
+
import { memo, useEffect, useRef, useState } from 'react';
|
|
6
6
|
import styles from '../trace-viewer.module.css';
|
|
7
7
|
import type {
|
|
8
8
|
MemoCache,
|
|
@@ -14,46 +14,76 @@ import type {
|
|
|
14
14
|
VisibleSpanEvent,
|
|
15
15
|
} from '../types';
|
|
16
16
|
import { MARKER_HEIGHT, ROW_HEIGHT, ROW_PADDING } from '../util/constants';
|
|
17
|
-
import { formatDuration } from '../util/timing';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
import { formatDuration, getHighResInMs } from '../util/timing';
|
|
18
|
+
import { SpanContent } from './span-content';
|
|
19
|
+
import { computeSegments } from './span-segments';
|
|
20
|
+
import {
|
|
21
|
+
type ResourceType,
|
|
22
|
+
type SpanLayout,
|
|
23
|
+
getResourceType,
|
|
24
|
+
getSpanLayout,
|
|
25
|
+
} from './span-strategies';
|
|
24
26
|
|
|
25
27
|
export const getSpanColorClassName = (node: SpanNode): string => {
|
|
26
28
|
if (node.isVercel) return String(styles.colorVercel);
|
|
27
29
|
return String(styles[`color${node.resourceIndex % 5}` as 'color0']);
|
|
28
30
|
};
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
isHuge && styles.huge,
|
|
37
|
-
isSpanSmall(node, scale) && styles.small,
|
|
38
|
-
node.isSelected && styles.selected,
|
|
39
|
-
isHovered && styles.xHover,
|
|
40
|
-
node.isHighlighted ? styles.colorHighlight : getSpanColorClassName(node),
|
|
41
|
-
node.isHighlighted === false && styles.unlit
|
|
42
|
-
);
|
|
43
|
-
};
|
|
32
|
+
function getTerminalTimestamp(
|
|
33
|
+
resourceType: ResourceType,
|
|
34
|
+
node: VisibleSpan
|
|
35
|
+
): number | undefined {
|
|
36
|
+
const events = node.events;
|
|
37
|
+
if (!events?.length) return undefined;
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
switch (resourceType) {
|
|
40
|
+
case 'hook':
|
|
41
|
+
for (let i = events.length; i--; ) {
|
|
42
|
+
if (events[i].event.name === 'hook_disposed') {
|
|
43
|
+
return events[i].timestamp;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
case 'sleep':
|
|
48
|
+
for (let i = events.length; i--; ) {
|
|
49
|
+
if (events[i].event.name === 'wait_completed') {
|
|
50
|
+
return events[i].timestamp;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
case 'run':
|
|
55
|
+
for (let i = events.length; i--; ) {
|
|
56
|
+
const name = events[i].event.name;
|
|
57
|
+
if (
|
|
58
|
+
name === 'run_completed' ||
|
|
59
|
+
name === 'run_failed' ||
|
|
60
|
+
name === 'run_cancelled'
|
|
61
|
+
) {
|
|
62
|
+
return events[i].timestamp;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
case 'step':
|
|
67
|
+
for (let i = events.length; i--; ) {
|
|
68
|
+
const name = events[i].event.name;
|
|
69
|
+
if (name === 'step_completed' || name === 'step_failed') {
|
|
70
|
+
return events[i].timestamp;
|
|
71
|
+
}
|
|
72
|
+
if (name === 'step_started' || name === 'step_retrying') {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
default:
|
|
78
|
+
return undefined;
|
|
48
79
|
}
|
|
49
|
-
|
|
50
|
-
return formatDuration(node.duration);
|
|
51
|
-
};
|
|
80
|
+
}
|
|
52
81
|
|
|
53
82
|
export const SpanNodes = memo(function SpanNodes({
|
|
54
83
|
root,
|
|
55
84
|
scale,
|
|
56
85
|
spans,
|
|
86
|
+
isLive = false,
|
|
57
87
|
scrollSnapshotRef,
|
|
58
88
|
cache,
|
|
59
89
|
customSpanClassNameFunc,
|
|
@@ -62,7 +92,9 @@ export const SpanNodes = memo(function SpanNodes({
|
|
|
62
92
|
root: RootNode;
|
|
63
93
|
scale: number;
|
|
64
94
|
spans: VisibleSpan[];
|
|
95
|
+
isLive?: boolean;
|
|
65
96
|
scrollSnapshotRef: MutableRefObject<ScrollSnapshot | undefined>;
|
|
97
|
+
/** Not used in the body — exists solely to bust React.memo when the global memo cache changes. */
|
|
66
98
|
cacheKey?: MemoCacheKey;
|
|
67
99
|
cache: MemoCache;
|
|
68
100
|
customSpanClassNameFunc?: (span: SpanNode) => string;
|
|
@@ -77,15 +109,54 @@ export const SpanNodes = memo(function SpanNodes({
|
|
|
77
109
|
node={x}
|
|
78
110
|
root={root}
|
|
79
111
|
scale={scale}
|
|
112
|
+
traceIsLive={isLive}
|
|
80
113
|
scrollSnapshotRef={scrollSnapshotRef}
|
|
81
114
|
/>
|
|
82
115
|
));
|
|
83
116
|
});
|
|
84
117
|
|
|
118
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
119
|
+
// Compute inline styles from a SpanLayout
|
|
120
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function getSpanStyle(
|
|
123
|
+
layout: SpanLayout,
|
|
124
|
+
node: VisibleSpan,
|
|
125
|
+
root: RootNode,
|
|
126
|
+
scale: number
|
|
127
|
+
): CSSProperties {
|
|
128
|
+
// Use isExpanded (hovered or selected) for width expansion so that
|
|
129
|
+
// selected small spans stay expanded and show their name.
|
|
130
|
+
const expanded = layout.isExpanded;
|
|
131
|
+
return {
|
|
132
|
+
// Use actualWidth for CSS variable so hover expansion is accurate
|
|
133
|
+
'--span-width': `${Math.max(layout.actualWidth, 1)}px`,
|
|
134
|
+
minWidth: expanded ? layout.width : undefined,
|
|
135
|
+
width: expanded ? undefined : layout.width,
|
|
136
|
+
height: layout.height,
|
|
137
|
+
maxWidth:
|
|
138
|
+
expanded && !layout.isNearRightSide
|
|
139
|
+
? (root.endTime - node.startTime) * scale
|
|
140
|
+
: undefined,
|
|
141
|
+
containIntrinsicWidth: expanded ? undefined : layout.width,
|
|
142
|
+
containIntrinsicHeight: layout.height,
|
|
143
|
+
left: layout.isNearRightSide ? undefined : layout.left,
|
|
144
|
+
right: layout.isNearRightSide
|
|
145
|
+
? (root.endTime - node.endTime) * scale
|
|
146
|
+
: undefined,
|
|
147
|
+
top: layout.top,
|
|
148
|
+
} as CSSProperties;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
152
|
+
// SpanComponent
|
|
153
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
85
155
|
export const SpanComponent = memo(function SpanComponent({
|
|
86
156
|
node,
|
|
87
157
|
root,
|
|
88
158
|
scale,
|
|
159
|
+
traceIsLive = false,
|
|
89
160
|
scrollSnapshotRef,
|
|
90
161
|
customSpanClassNameFunc,
|
|
91
162
|
customSpanEventClassNameFunc,
|
|
@@ -93,99 +164,179 @@ export const SpanComponent = memo(function SpanComponent({
|
|
|
93
164
|
node: VisibleSpan;
|
|
94
165
|
root: RootNode;
|
|
95
166
|
scale: number;
|
|
167
|
+
traceIsLive?: boolean;
|
|
96
168
|
scrollSnapshotRef: MutableRefObject<ScrollSnapshot | undefined>;
|
|
169
|
+
/** Not used in the body — exists solely to bust React.memo for this span. */
|
|
97
170
|
cacheKey?: MemoCacheKey;
|
|
98
171
|
customSpanClassNameFunc?: (span: SpanNode) => string;
|
|
99
172
|
customSpanEventClassNameFunc?: (event: VisibleSpanEvent) => string;
|
|
100
173
|
}): ReactNode {
|
|
101
174
|
const ref = useRef<HTMLButtonElement>(null);
|
|
102
|
-
node.ref = ref;
|
|
103
175
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Enforce minimum width so very short spans are always visible
|
|
111
|
-
const MIN_SPAN_WIDTH = 4;
|
|
112
|
-
const width = Math.max(actualWidth, MIN_SPAN_WIDTH);
|
|
113
|
-
let height = ROW_HEIGHT;
|
|
114
|
-
const isHuge = isSpanHuge(node, scale);
|
|
115
|
-
// Check if span is small based on actual width, not minimum width
|
|
116
|
-
const isSmall = actualWidth < 64;
|
|
117
|
-
const isHovered = node.isHovered && !isHuge && node.isHighlighted !== false;
|
|
118
|
-
if (isSmall && !isHovered) {
|
|
119
|
-
height *= 0.4;
|
|
120
|
-
top += (ROW_HEIGHT - height) * 0.5;
|
|
121
|
-
}
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
node.ref = ref;
|
|
178
|
+
return () => {
|
|
179
|
+
node.ref = undefined;
|
|
180
|
+
};
|
|
181
|
+
}, [node]);
|
|
122
182
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const snapshot = scrollSnapshotRef.current;
|
|
127
|
-
if (snapshot) {
|
|
128
|
-
visibleDuration = snapshot.endTime - snapshot.startTime;
|
|
129
|
-
visibleEndTime = snapshot.endTime;
|
|
130
|
-
}
|
|
131
|
-
isNearRightSide = visibleEndTime - node.startTime < 0.25 * visibleDuration;
|
|
132
|
-
}
|
|
183
|
+
const { span } = node;
|
|
184
|
+
const resourceType = getResourceType(node);
|
|
185
|
+
const [liveNow, setLiveNow] = useState(0);
|
|
133
186
|
|
|
134
187
|
// Get custom class name from callback if provided
|
|
135
188
|
const customClassName = customSpanClassNameFunc
|
|
136
189
|
? customSpanClassNameFunc(node)
|
|
137
190
|
: '';
|
|
138
191
|
|
|
192
|
+
// Workflow span types use colored segments + boundary line markers
|
|
193
|
+
// Generic OTEL spans use diamond event markers
|
|
194
|
+
const isWorkflowSpan = resourceType !== 'default';
|
|
195
|
+
|
|
196
|
+
// Determine if this span is still active (live). We require the parent run
|
|
197
|
+
// to be explicitly live to avoid completed/cancelled traces continuing to
|
|
198
|
+
// grow due to timing heuristics.
|
|
199
|
+
const data = span.attributes?.data as Record<string, unknown> | undefined;
|
|
200
|
+
const terminalTimestamp = getTerminalTimestamp(resourceType, node);
|
|
201
|
+
const hasTerminalEvent = terminalTimestamp != null;
|
|
202
|
+
const isCompletedByData =
|
|
203
|
+
resourceType === 'hook'
|
|
204
|
+
? Boolean(data?.disposedAt)
|
|
205
|
+
: Boolean(data?.completedAt);
|
|
206
|
+
const isCompleted = isCompletedByData || hasTerminalEvent;
|
|
207
|
+
const isLive = traceIsLive && isWorkflowSpan && !isCompleted;
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!isLive) return;
|
|
211
|
+
setLiveNow(Date.now());
|
|
212
|
+
const interval = setInterval(() => {
|
|
213
|
+
setLiveNow(Date.now());
|
|
214
|
+
}, 250);
|
|
215
|
+
return () => clearInterval(interval);
|
|
216
|
+
}, [isLive]);
|
|
217
|
+
|
|
218
|
+
const canonicalStartTime = getHighResInMs(span.startTime);
|
|
219
|
+
const canonicalEndTime = getHighResInMs(span.endTime);
|
|
220
|
+
const canonicalDuration = getHighResInMs(span.duration);
|
|
221
|
+
const canonicalActiveStartTime = span.activeStartTime
|
|
222
|
+
? getHighResInMs(span.activeStartTime)
|
|
223
|
+
: undefined;
|
|
224
|
+
|
|
225
|
+
const durationMs =
|
|
226
|
+
terminalTimestamp != null
|
|
227
|
+
? Math.max(0, terminalTimestamp - canonicalStartTime)
|
|
228
|
+
: isLive
|
|
229
|
+
? Math.max(0, (liveNow || Date.now()) - canonicalStartTime)
|
|
230
|
+
: canonicalDuration;
|
|
231
|
+
const duration = node.isInstrumentationHint
|
|
232
|
+
? 'Get Started'
|
|
233
|
+
: formatDuration(durationMs);
|
|
234
|
+
const baseNode = {
|
|
235
|
+
...node,
|
|
236
|
+
startTime: canonicalStartTime,
|
|
237
|
+
endTime: canonicalEndTime,
|
|
238
|
+
duration: canonicalDuration,
|
|
239
|
+
activeStartTime: canonicalActiveStartTime,
|
|
240
|
+
} as VisibleSpan;
|
|
241
|
+
const segmentNode = isLive
|
|
242
|
+
? ({
|
|
243
|
+
...baseNode,
|
|
244
|
+
duration: durationMs,
|
|
245
|
+
endTime: canonicalStartTime + durationMs,
|
|
246
|
+
} as VisibleSpan)
|
|
247
|
+
: baseNode;
|
|
248
|
+
const hasSegments =
|
|
249
|
+
isWorkflowSpan &&
|
|
250
|
+
computeSegments(resourceType, segmentNode).segments.length > 0;
|
|
251
|
+
const layout = getSpanLayout(
|
|
252
|
+
resourceType,
|
|
253
|
+
segmentNode,
|
|
254
|
+
root,
|
|
255
|
+
scale,
|
|
256
|
+
scrollSnapshotRef
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// For live spans the data-level `node.duration` can be stale (from the last
|
|
260
|
+
// fetch) while the visual width is growing via rAF. Recompute from real
|
|
261
|
+
// elapsed time so that:
|
|
262
|
+
// - height/isSmall classification stays correct (not shrunken to 40%)
|
|
263
|
+
// - React-rendered width matches the rAF-driven width, preventing a brief
|
|
264
|
+
// shrink→expand flash on re-render that breaks cursor hit-testing
|
|
265
|
+
if (isLive) {
|
|
266
|
+
const elapsed = durationMs;
|
|
267
|
+
const liveActualWidth = elapsed * scale;
|
|
268
|
+
const liveWidth = Math.max(liveActualWidth, 4);
|
|
269
|
+
layout.actualWidth = liveActualWidth;
|
|
270
|
+
layout.width = liveWidth;
|
|
271
|
+
layout.isSmall = liveActualWidth < 64;
|
|
272
|
+
layout.isHuge = liveActualWidth >= 500;
|
|
273
|
+
if (!layout.isSmall) {
|
|
274
|
+
layout.height = ROW_HEIGHT;
|
|
275
|
+
layout.top = MARKER_HEIGHT + (ROW_HEIGHT + ROW_PADDING) * segmentNode.row;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Smoothly grow active span width at 60fps using wall clock time
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (!isLive || !ref.current) return;
|
|
282
|
+
|
|
283
|
+
let rafId = 0;
|
|
284
|
+
const tick = (): void => {
|
|
285
|
+
const $el = ref.current;
|
|
286
|
+
if (!$el) return;
|
|
287
|
+
const elapsed = Date.now() - canonicalStartTime;
|
|
288
|
+
const w = Math.max(elapsed * scale, 2);
|
|
289
|
+
$el.style.width = `${w}px`;
|
|
290
|
+
$el.style.setProperty('--span-width', `${w}px`);
|
|
291
|
+
rafId = requestAnimationFrame(tick);
|
|
292
|
+
};
|
|
293
|
+
rafId = requestAnimationFrame(tick);
|
|
294
|
+
return () => cancelAnimationFrame(rafId);
|
|
295
|
+
}, [isLive, canonicalStartTime, scale]);
|
|
296
|
+
|
|
297
|
+
const renderNode = segmentNode;
|
|
298
|
+
|
|
139
299
|
return (
|
|
140
300
|
<>
|
|
141
301
|
<button
|
|
142
302
|
aria-label={`${span.name} - ${duration}`}
|
|
143
|
-
className={clsx(
|
|
303
|
+
className={clsx(
|
|
304
|
+
styles.spanNode,
|
|
305
|
+
layout.isHuge && styles.huge,
|
|
306
|
+
layout.isSmall && styles.small,
|
|
307
|
+
layout.isExpanded && styles.xHover,
|
|
308
|
+
node.isHighlighted
|
|
309
|
+
? styles.colorHighlight
|
|
310
|
+
: getSpanColorClassName(node),
|
|
311
|
+
node.isHighlighted === false && styles.unlit,
|
|
312
|
+
customClassName,
|
|
313
|
+
hasSegments && styles.hasSegments
|
|
314
|
+
)}
|
|
144
315
|
data-span-id={span.spanId}
|
|
145
|
-
data-start-time={
|
|
146
|
-
data-right-side={isNearRightSide}
|
|
316
|
+
data-start-time={segmentNode.startTime - root.startTime}
|
|
317
|
+
data-right-side={layout.isNearRightSide}
|
|
318
|
+
data-selected={node.isSelected ? '' : undefined}
|
|
147
319
|
ref={ref}
|
|
148
|
-
style={
|
|
149
|
-
{
|
|
150
|
-
// Use actualWidth for CSS variable so hover expansion is accurate
|
|
151
|
-
'--span-width': `${Math.max(actualWidth, 1)}px`,
|
|
152
|
-
minWidth: isHovered ? width : undefined,
|
|
153
|
-
width: isHovered ? undefined : width,
|
|
154
|
-
height,
|
|
155
|
-
maxWidth:
|
|
156
|
-
isHovered && !isNearRightSide
|
|
157
|
-
? (root.endTime - node.startTime) * scale
|
|
158
|
-
: undefined,
|
|
159
|
-
containIntrinsicWidth: isHovered ? undefined : width,
|
|
160
|
-
containIntrinsicHeight: height,
|
|
161
|
-
left: isNearRightSide ? undefined : left,
|
|
162
|
-
right: isNearRightSide
|
|
163
|
-
? (root.endTime - node.endTime) * scale
|
|
164
|
-
: undefined,
|
|
165
|
-
top,
|
|
166
|
-
} as CSSProperties
|
|
167
|
-
}
|
|
320
|
+
style={getSpanStyle(layout, segmentNode, root, scale)}
|
|
168
321
|
type="button"
|
|
169
322
|
>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
) : null}
|
|
177
|
-
</>
|
|
178
|
-
)}
|
|
323
|
+
<SpanContent
|
|
324
|
+
durationMs={durationMs}
|
|
325
|
+
resourceType={resourceType}
|
|
326
|
+
node={renderNode}
|
|
327
|
+
layout={layout}
|
|
328
|
+
/>
|
|
179
329
|
</button>
|
|
180
|
-
{
|
|
181
|
-
?
|
|
330
|
+
{segmentNode.events && !layout.isSmall
|
|
331
|
+
? segmentNode.events.map((x) => (
|
|
182
332
|
<SpanEventComponent
|
|
183
333
|
customSpanEventClassNameFunc={customSpanEventClassNameFunc}
|
|
184
334
|
event={x}
|
|
185
335
|
key={x.key}
|
|
186
|
-
node={
|
|
336
|
+
node={segmentNode}
|
|
187
337
|
root={root}
|
|
188
338
|
scale={scale}
|
|
339
|
+
asBoundary={isWorkflowSpan}
|
|
189
340
|
/>
|
|
190
341
|
))
|
|
191
342
|
: null}
|
|
@@ -193,29 +344,102 @@ export const SpanComponent = memo(function SpanComponent({
|
|
|
193
344
|
);
|
|
194
345
|
});
|
|
195
346
|
|
|
347
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
348
|
+
// SpanEventComponent
|
|
349
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
/** Human-readable labels for workflow event types */
|
|
352
|
+
const BOUNDARY_LABELS: Record<string, string> = {
|
|
353
|
+
step_started: 'Started',
|
|
354
|
+
step_retrying: 'Retrying',
|
|
355
|
+
step_failed: 'Failed',
|
|
356
|
+
hook_created: 'Created',
|
|
357
|
+
hook_received: 'Received',
|
|
358
|
+
hook_disposed: 'Resolved',
|
|
359
|
+
wait_created: 'Sleep started',
|
|
360
|
+
wait_completed: 'Sleep completed',
|
|
361
|
+
run_started: 'Started',
|
|
362
|
+
run_completed: 'Completed',
|
|
363
|
+
run_failed: 'Run failed',
|
|
364
|
+
step_completed: 'Completed',
|
|
365
|
+
};
|
|
366
|
+
|
|
196
367
|
export const SpanEventComponent = memo(function SpanEventComponent({
|
|
197
368
|
event,
|
|
198
369
|
node,
|
|
199
370
|
root,
|
|
200
371
|
scale,
|
|
201
372
|
customSpanEventClassNameFunc,
|
|
373
|
+
asBoundary = false,
|
|
202
374
|
}: {
|
|
203
375
|
event: VisibleSpanEvent;
|
|
204
376
|
node: VisibleSpan;
|
|
205
377
|
root: RootNode;
|
|
206
378
|
scale: number;
|
|
207
379
|
customSpanEventClassNameFunc?: (event: VisibleSpanEvent) => string;
|
|
380
|
+
asBoundary?: boolean;
|
|
208
381
|
}): ReactNode {
|
|
209
382
|
const ref = useRef<HTMLDivElement>(null);
|
|
210
|
-
|
|
383
|
+
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
event.ref = ref;
|
|
386
|
+
return () => {
|
|
387
|
+
event.ref = undefined;
|
|
388
|
+
};
|
|
389
|
+
}, [event]);
|
|
211
390
|
|
|
212
391
|
const {
|
|
213
392
|
event: { name },
|
|
214
393
|
} = event;
|
|
215
394
|
const timestamp = formatDuration(event.timestamp - root.startTime);
|
|
395
|
+
const displayLabel = asBoundary ? (BOUNDARY_LABELS[name] ?? name) : name;
|
|
396
|
+
|
|
397
|
+
// For boundary events, compute the duration of the phase.
|
|
398
|
+
// "Forward" events (started, retrying) measure until the next event.
|
|
399
|
+
// "Terminal" events (completed) measure from the previous started event.
|
|
400
|
+
const isForwardEvent =
|
|
401
|
+
asBoundary &&
|
|
402
|
+
(name === 'step_started' ||
|
|
403
|
+
name === 'run_started' ||
|
|
404
|
+
name === 'step_retrying');
|
|
405
|
+
const isTerminalEvent =
|
|
406
|
+
asBoundary && (name === 'step_completed' || name === 'run_completed');
|
|
407
|
+
|
|
408
|
+
let phaseDuration: string | null = null;
|
|
409
|
+
let phaseLabel: string | null = null;
|
|
410
|
+
if ((isForwardEvent || isTerminalEvent) && node.events) {
|
|
411
|
+
const sortedNodeEvents = [...node.events].sort(
|
|
412
|
+
(a, b) => a.timestamp - b.timestamp
|
|
413
|
+
);
|
|
414
|
+
const currentIdx = sortedNodeEvents.findIndex((e) => e.key === event.key);
|
|
415
|
+
|
|
416
|
+
if (isTerminalEvent) {
|
|
417
|
+
// Look backward to find the last step_started
|
|
418
|
+
let prevStartTime: number | null = null;
|
|
419
|
+
for (let i = currentIdx - 1; i >= 0; i--) {
|
|
420
|
+
if (sortedNodeEvents[i].event.name === 'step_started') {
|
|
421
|
+
prevStartTime = sortedNodeEvents[i].timestamp;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (prevStartTime !== null) {
|
|
426
|
+
phaseDuration = formatDuration(event.timestamp - prevStartTime);
|
|
427
|
+
phaseLabel = 'Executed';
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
// Look forward to the next event
|
|
431
|
+
const nextEvent = sortedNodeEvents[currentIdx + 1];
|
|
432
|
+
const endTime = nextEvent ? nextEvent.timestamp : node.endTime;
|
|
433
|
+
phaseDuration = formatDuration(endTime - event.timestamp);
|
|
434
|
+
phaseLabel = name === 'step_retrying' ? 'Waited' : 'Executed';
|
|
435
|
+
}
|
|
436
|
+
}
|
|
216
437
|
|
|
217
438
|
const left = (event.timestamp - root.startTime) * scale;
|
|
218
439
|
const top = MARKER_HEIGHT + (ROW_HEIGHT + ROW_PADDING) * node.row;
|
|
440
|
+
const isLeftAligned =
|
|
441
|
+
node.duration <= 0 ||
|
|
442
|
+
(event.timestamp - node.startTime) / node.duration < 0.5;
|
|
219
443
|
|
|
220
444
|
// Get custom class name from callback if provided
|
|
221
445
|
const customClassName = customSpanEventClassNameFunc
|
|
@@ -224,10 +448,15 @@ export const SpanEventComponent = memo(function SpanEventComponent({
|
|
|
224
448
|
|
|
225
449
|
return (
|
|
226
450
|
<div
|
|
227
|
-
title={
|
|
451
|
+
title={
|
|
452
|
+
phaseDuration
|
|
453
|
+
? `${displayLabel} ${timestamp}\n${phaseLabel} ${phaseDuration}`
|
|
454
|
+
: `${displayLabel} ${timestamp}`
|
|
455
|
+
}
|
|
228
456
|
className={clsx(
|
|
229
457
|
styles.spanNodeEvent,
|
|
230
458
|
customClassName,
|
|
459
|
+
asBoundary && styles.boundaryMarker,
|
|
231
460
|
node.isHighlighted
|
|
232
461
|
? styles.colorHighlight
|
|
233
462
|
: getSpanColorClassName(node),
|
|
@@ -245,15 +474,32 @@ export const SpanEventComponent = memo(function SpanEventComponent({
|
|
|
245
474
|
<div
|
|
246
475
|
className={clsx(
|
|
247
476
|
styles.hoverInfo,
|
|
248
|
-
|
|
249
|
-
? styles.alignStart
|
|
250
|
-
: styles.alignEnd
|
|
477
|
+
isLeftAligned ? styles.alignStart : styles.alignEnd
|
|
251
478
|
)}
|
|
252
479
|
>
|
|
253
|
-
|
|
254
|
-
|
|
480
|
+
{asBoundary ? (
|
|
481
|
+
<>
|
|
482
|
+
<span className={styles.eventName}>{displayLabel}</span>
|
|
483
|
+
<span className={styles.eventTimestamp}>{timestamp}</span>
|
|
484
|
+
{phaseDuration ? (
|
|
485
|
+
<>
|
|
486
|
+
<span className={styles.eventName}>{phaseLabel}</span>
|
|
487
|
+
<span className={styles.eventTimestamp}>{phaseDuration}</span>
|
|
488
|
+
</>
|
|
489
|
+
) : null}
|
|
490
|
+
</>
|
|
491
|
+
) : (
|
|
492
|
+
<>
|
|
493
|
+
<span className={styles.eventName}>{name}</span>
|
|
494
|
+
<span className={styles.eventTimestamp}>{timestamp}</span>
|
|
495
|
+
</>
|
|
496
|
+
)}
|
|
255
497
|
</div>
|
|
256
|
-
|
|
498
|
+
{asBoundary ? (
|
|
499
|
+
<div className={styles.boundaryLine} />
|
|
500
|
+
) : (
|
|
501
|
+
<div className={styles.eventDiamond} />
|
|
502
|
+
)}
|
|
257
503
|
</div>
|
|
258
504
|
);
|
|
259
505
|
});
|