@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
|
@@ -33,6 +33,10 @@ interface TraceViewerProps {
|
|
|
33
33
|
withPanel?: boolean;
|
|
34
34
|
getQuickLinks?: GetQuickLinks;
|
|
35
35
|
highlightedSpans?: string[];
|
|
36
|
+
/** Render all spans immediately (no progressive streaming). */
|
|
37
|
+
eagerRender?: boolean;
|
|
38
|
+
/** Whether the trace is live (for continuous tick updates). */
|
|
39
|
+
isLive?: boolean;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
export function TraceViewerProvider({
|
|
@@ -97,25 +101,107 @@ export function TraceViewerTimeline({
|
|
|
97
101
|
height,
|
|
98
102
|
withPanel = false,
|
|
99
103
|
highlightedSpans,
|
|
104
|
+
eagerRender = false,
|
|
105
|
+
isLive = false,
|
|
100
106
|
}: Omit<TraceViewerProps, 'getQuickLinks'>): ReactNode {
|
|
101
107
|
const isSkeleton = trace === skeletonTrace;
|
|
102
108
|
const { state, dispatch } = useTraceViewer();
|
|
103
109
|
const { timelineRef, scrollSnapshotRef } = state;
|
|
104
110
|
const memoCache = state.memoCacheRef.current;
|
|
105
|
-
const hideSearchBar =
|
|
106
|
-
|
|
111
|
+
const hideSearchBar = (highlightedSpans?.length ?? 0) > 0;
|
|
112
|
+
|
|
113
|
+
const hasInitializedRef = useRef(false);
|
|
114
|
+
const prevSpanKeyRef = useRef('');
|
|
115
|
+
const prevRootRef = useRef<ReturnType<typeof parseTrace>['root'] | null>(
|
|
116
|
+
null
|
|
117
|
+
);
|
|
118
|
+
const prevSpanMapRef = useRef<ReturnType<typeof parseTrace>['map']>({});
|
|
119
|
+
const prevSizeRef = useRef<{ width: number; height: number } | null>(null);
|
|
107
120
|
|
|
108
121
|
useEffect(() => {
|
|
109
|
-
const { root, map:
|
|
122
|
+
const { root: newRoot, map: newSpanMap } = parseTrace(trace);
|
|
123
|
+
const isInitial = !hasInitializedRef.current;
|
|
124
|
+
hasInitializedRef.current = true;
|
|
125
|
+
|
|
126
|
+
// Build a structural key from span IDs + lightweight event signatures.
|
|
127
|
+
// When only timing changes (same spans and boundary events), we can skip the
|
|
128
|
+
// worker restart. When events change (step completed, etc.) we need a
|
|
129
|
+
// full update because the VisibleSpan objects rendered by React hold
|
|
130
|
+
// copies of the events from the worker — in-place mutation won't reach them.
|
|
131
|
+
const spanKey = Object.keys(newSpanMap)
|
|
132
|
+
.sort()
|
|
133
|
+
.map((id) => {
|
|
134
|
+
const events = newSpanMap[id].events;
|
|
135
|
+
const count = events?.length ?? 0;
|
|
136
|
+
const first = count ? events?.[0] : undefined;
|
|
137
|
+
const last = count ? events?.[count - 1] : undefined;
|
|
138
|
+
const eventSignature = `${first?.event.name ?? 'none'}@${first?.timestamp ?? 0}|${last?.event.name ?? 'none'}@${last?.timestamp ?? 0}`;
|
|
139
|
+
return `${id}:${count}:${eventSignature}`;
|
|
140
|
+
})
|
|
141
|
+
.join(',');
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
!isInitial &&
|
|
145
|
+
spanKey === prevSpanKeyRef.current &&
|
|
146
|
+
prevRootRef.current
|
|
147
|
+
) {
|
|
148
|
+
// Same span set, same event counts — only timing changed.
|
|
149
|
+
// Mutate in-place so the worker effect (which depends on root/spanMap
|
|
150
|
+
// references) does NOT restart.
|
|
151
|
+
const oldRoot = prevRootRef.current;
|
|
152
|
+
const oldSpanMap = prevSpanMapRef.current;
|
|
153
|
+
let didChange = false;
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
oldRoot.endTime !== newRoot.endTime ||
|
|
157
|
+
oldRoot.duration !== newRoot.duration
|
|
158
|
+
) {
|
|
159
|
+
oldRoot.endTime = newRoot.endTime;
|
|
160
|
+
oldRoot.duration = newRoot.duration;
|
|
161
|
+
didChange = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const [id, newNode] of Object.entries(newSpanMap)) {
|
|
165
|
+
const oldNode = oldSpanMap[id];
|
|
166
|
+
if (oldNode) {
|
|
167
|
+
if (
|
|
168
|
+
oldNode.endTime !== newNode.endTime ||
|
|
169
|
+
oldNode.duration !== newNode.duration
|
|
170
|
+
) {
|
|
171
|
+
oldNode.endTime = newNode.endTime;
|
|
172
|
+
oldNode.duration = newNode.duration;
|
|
173
|
+
didChange = true;
|
|
174
|
+
}
|
|
175
|
+
Object.assign(oldNode.span, newNode.span);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!didChange) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Trigger re-render (scale/markers) without restarting the worker.
|
|
184
|
+
// useLiveTick handles continuous scale recalculation for live runs.
|
|
185
|
+
dispatch({ type: 'forceRender' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Structure changed or initial load — full dispatch (worker computes new rows)
|
|
190
|
+
prevSpanKeyRef.current = spanKey;
|
|
191
|
+
prevRootRef.current = newRoot;
|
|
192
|
+
prevSpanMapRef.current = newSpanMap;
|
|
110
193
|
dispatch({
|
|
111
|
-
type: 'setRoot',
|
|
112
|
-
root,
|
|
113
|
-
spanMap,
|
|
194
|
+
type: isInitial ? 'setRoot' : 'updateRoot',
|
|
195
|
+
root: newRoot,
|
|
196
|
+
spanMap: newSpanMap,
|
|
114
197
|
resources: trace.resources || [],
|
|
115
198
|
});
|
|
116
199
|
}, [dispatch, trace]);
|
|
117
200
|
|
|
118
|
-
const { rows, spans, events, scale } = useStreamingSpans(
|
|
201
|
+
const { rows, spans, events, scale } = useStreamingSpans(
|
|
202
|
+
highlightedSpans,
|
|
203
|
+
eagerRender
|
|
204
|
+
);
|
|
119
205
|
|
|
120
206
|
const ref = useRef<HTMLDivElement>(null);
|
|
121
207
|
useLayoutEffect(() => {
|
|
@@ -125,11 +211,23 @@ export function TraceViewerTimeline({
|
|
|
125
211
|
const onResize = (): void => {
|
|
126
212
|
const padding = 2 * TIMELINE_PADDING;
|
|
127
213
|
const rect = $el.getBoundingClientRect();
|
|
214
|
+
const nextWidth = rect.width - padding;
|
|
215
|
+
const nextHeight = rect.height;
|
|
216
|
+
const prevSize = prevSizeRef.current;
|
|
217
|
+
|
|
218
|
+
if (
|
|
219
|
+
prevSize &&
|
|
220
|
+
prevSize.width === nextWidth &&
|
|
221
|
+
prevSize.height === nextHeight
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
prevSizeRef.current = { width: nextWidth, height: nextHeight };
|
|
128
226
|
|
|
129
227
|
dispatch({
|
|
130
228
|
type: 'setSize',
|
|
131
|
-
width:
|
|
132
|
-
height:
|
|
229
|
+
width: nextWidth,
|
|
230
|
+
height: nextHeight,
|
|
133
231
|
});
|
|
134
232
|
};
|
|
135
233
|
|
|
@@ -202,7 +300,11 @@ export function TraceViewerTimeline({
|
|
|
202
300
|
[dispatch]
|
|
203
301
|
);
|
|
204
302
|
|
|
205
|
-
// Zoom helper
|
|
303
|
+
// Zoom helper — apply the scroll snapshot once after a zoom operation so
|
|
304
|
+
// the anchor point stays at the same screen position, then clear it.
|
|
305
|
+
// Without clearing, live runs (where scale changes every frame via
|
|
306
|
+
// detectBaseScale) would continuously reset scrollLeft and prevent the
|
|
307
|
+
// user from scrolling horizontally.
|
|
206
308
|
useLayoutEffect(() => {
|
|
207
309
|
const $timeline = timelineRef.current;
|
|
208
310
|
if (!$timeline) return;
|
|
@@ -210,6 +312,7 @@ export function TraceViewerTimeline({
|
|
|
210
312
|
const snapshot = scrollSnapshotRef.current;
|
|
211
313
|
if (snapshot) {
|
|
212
314
|
$timeline.scrollLeft = snapshot.anchorT * scale - snapshot.anchorX;
|
|
315
|
+
scrollSnapshotRef.current = undefined;
|
|
213
316
|
}
|
|
214
317
|
}, [scrollSnapshotRef, timelineRef, scale]);
|
|
215
318
|
|
|
@@ -359,7 +462,7 @@ export function TraceViewerTimeline({
|
|
|
359
462
|
height: timelineHeight - TIMELINE_PADDING * 2,
|
|
360
463
|
}}
|
|
361
464
|
>
|
|
362
|
-
<Markers scale={scale} />
|
|
465
|
+
<Markers scale={scale} isLive={isLive} />
|
|
363
466
|
<EventMarkers events={events} root={state.root} scale={scale} />
|
|
364
467
|
<CursorMarker
|
|
365
468
|
dispatch={dispatch}
|
|
@@ -378,6 +481,7 @@ export function TraceViewerTimeline({
|
|
|
378
481
|
customSpanEventClassNameFunc={
|
|
379
482
|
state.customSpanEventClassNameFunc
|
|
380
483
|
}
|
|
484
|
+
isLive={isLive}
|
|
381
485
|
root={state.root}
|
|
382
486
|
scale={scale}
|
|
383
487
|
scrollSnapshotRef={scrollSnapshotRef}
|
|
@@ -31,3 +31,16 @@ export function formatTimeSelection(ms: number): string {
|
|
|
31
31
|
}
|
|
32
32
|
return `${timeSelectionFormatter.format(ms)}ms`;
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Format an epoch-millisecond timestamp as a local wall-clock time.
|
|
37
|
+
* Returns a compact HH:MM:SS.mmm string (24-hour format).
|
|
38
|
+
*/
|
|
39
|
+
export function formatWallClockTime(epochMs: number): string {
|
|
40
|
+
const d = new Date(epochMs);
|
|
41
|
+
const h = String(d.getHours()).padStart(2, '0');
|
|
42
|
+
const m = String(d.getMinutes()).padStart(2, '0');
|
|
43
|
+
const s = String(d.getSeconds()).padStart(2, '0');
|
|
44
|
+
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
45
|
+
return `${h}:${m}:${s}.${ms}`;
|
|
46
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { clsx } from 'clsx';
|
|
4
3
|
import { useEffect, useRef, useState } from 'react';
|
|
5
|
-
import { getSpanClassName } from '../components/node';
|
|
6
4
|
import { useTraceViewer } from '../context';
|
|
7
5
|
import type {
|
|
8
6
|
VisibleSpan,
|
|
@@ -32,7 +30,8 @@ function emptyArrayInit<T>(): T[] {
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
export const useStreamingSpans = (
|
|
35
|
-
highlightedSpans?: string[]
|
|
33
|
+
highlightedSpans?: string[],
|
|
34
|
+
eagerRender = false
|
|
36
35
|
): {
|
|
37
36
|
rows: VisibleSpan[][];
|
|
38
37
|
spans: VisibleSpan[];
|
|
@@ -50,7 +49,6 @@ export const useStreamingSpans = (
|
|
|
50
49
|
scrollSnapshotRef,
|
|
51
50
|
selected,
|
|
52
51
|
memoCacheRef,
|
|
53
|
-
customSpanClassNameFunc,
|
|
54
52
|
} = state;
|
|
55
53
|
const timelineHeight = useStableValue(state.timelineHeight);
|
|
56
54
|
const counterRef = useRef(0);
|
|
@@ -83,13 +81,16 @@ export const useStreamingSpans = (
|
|
|
83
81
|
useEffect(() => {
|
|
84
82
|
if (!root.startTime) return;
|
|
85
83
|
|
|
86
|
-
const worker = new Worker(new URL('../worker', import.meta.url)
|
|
84
|
+
const worker = new Worker(new URL('../worker', import.meta.url), {
|
|
85
|
+
type: 'module',
|
|
86
|
+
});
|
|
87
87
|
let requestId = ++counterRef.current;
|
|
88
88
|
const onMessage = (event: MessageEvent): void => {
|
|
89
89
|
const data = event.data as WorkerResponse;
|
|
90
90
|
|
|
91
91
|
switch (data.type) {
|
|
92
92
|
case 'setRowsResult': {
|
|
93
|
+
if (data?.requestId !== requestId) return;
|
|
93
94
|
if (data.isEnd) {
|
|
94
95
|
for (const row of data.rows) {
|
|
95
96
|
for (const node of row) {
|
|
@@ -168,6 +169,24 @@ export const useStreamingSpans = (
|
|
|
168
169
|
useEffect(() => {
|
|
169
170
|
if (!rows.length) return;
|
|
170
171
|
|
|
172
|
+
if (eagerRender) {
|
|
173
|
+
const visible: VisibleSpan[] = [];
|
|
174
|
+
const events: VisibleSpanEvent[] = [];
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
for (const span of row) {
|
|
177
|
+
if (!adjustSpanVisibility(span, scale)) continue;
|
|
178
|
+
visible.push(span);
|
|
179
|
+
if (span.events) {
|
|
180
|
+
events.push(...span.events);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
setVisibleSpans(visible);
|
|
185
|
+
setVisibleEvents(events);
|
|
186
|
+
setResultScale(scale);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
171
190
|
const $timeline = timelineRef.current;
|
|
172
191
|
let snapshot = scrollSnapshotRef.current;
|
|
173
192
|
let hasScrolled = false;
|
|
@@ -342,6 +361,7 @@ export const useStreamingSpans = (
|
|
|
342
361
|
timelineRef,
|
|
343
362
|
timelineWidth,
|
|
344
363
|
timelineHeight,
|
|
364
|
+
eagerRender,
|
|
345
365
|
]);
|
|
346
366
|
|
|
347
367
|
useEffect(() => {
|
|
@@ -359,22 +379,13 @@ export const useStreamingSpans = (
|
|
|
359
379
|
if (!span || !$span) return;
|
|
360
380
|
|
|
361
381
|
span.isSelected = true;
|
|
362
|
-
|
|
363
|
-
const baseClassName = getSpanClassName(span, scale);
|
|
364
|
-
const customClassName = customSpanClassNameFunc
|
|
365
|
-
? customSpanClassNameFunc(span)
|
|
366
|
-
: '';
|
|
367
|
-
$span.className = clsx(baseClassName, customClassName);
|
|
382
|
+
$span.setAttribute('data-selected', '');
|
|
368
383
|
|
|
369
384
|
return () => {
|
|
370
385
|
span.isSelected = false;
|
|
371
|
-
|
|
372
|
-
const customClassName = customSpanClassNameFunc
|
|
373
|
-
? customSpanClassNameFunc(span)
|
|
374
|
-
: '';
|
|
375
|
-
$span.className = clsx(baseClassName, customClassName);
|
|
386
|
+
$span.removeAttribute('data-selected');
|
|
376
387
|
};
|
|
377
|
-
}, [visibleSpans,
|
|
388
|
+
}, [visibleSpans, selected]);
|
|
378
389
|
|
|
379
390
|
return {
|
|
380
391
|
rows,
|
|
@@ -100,7 +100,10 @@ const filterSpans = (root: RootNode, filter: string): void => {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const match = (span: Span): boolean => {
|
|
103
|
-
|
|
103
|
+
const nameMatch =
|
|
104
|
+
span.name.toLocaleLowerCase().includes(name) ||
|
|
105
|
+
span.spanId.toLocaleLowerCase().includes(name);
|
|
106
|
+
if (!nameMatch) return false;
|
|
104
107
|
// TODO: support resource attribute filtering
|
|
105
108
|
return attrs.every(([key, value]) => {
|
|
106
109
|
const v = span.attributes[key];
|
|
@@ -4,13 +4,13 @@ import * as React from 'react';
|
|
|
4
4
|
import { cn } from '../../lib/utils';
|
|
5
5
|
|
|
6
6
|
const alertVariants = cva(
|
|
7
|
-
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4
|
|
7
|
+
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4',
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
-
default: '
|
|
11
|
+
default: '',
|
|
12
12
|
destructive:
|
|
13
|
-
'border-
|
|
13
|
+
'border-red-400 text-red-900 dark:border-red-700 [&>svg]:text-red-900',
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
defaultVariants: {
|
|
@@ -10,10 +10,7 @@ const Card = React.forwardRef<
|
|
|
10
10
|
>(({ className, ...props }, ref) => (
|
|
11
11
|
<div
|
|
12
12
|
ref={ref}
|
|
13
|
-
className={cn(
|
|
14
|
-
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
|
15
|
-
className
|
|
16
|
-
)}
|
|
13
|
+
className={cn('rounded-lg border shadow-sm', className)}
|
|
17
14
|
{...props}
|
|
18
15
|
/>
|
|
19
16
|
));
|
|
@@ -52,7 +49,8 @@ const CardDescription = React.forwardRef<
|
|
|
52
49
|
>(({ className, ...props }, ref) => (
|
|
53
50
|
<div
|
|
54
51
|
ref={ref}
|
|
55
|
-
className={cn('text-sm
|
|
52
|
+
className={cn('text-sm', className)}
|
|
53
|
+
style={{ color: 'var(--ds-gray-900)' }}
|
|
56
54
|
{...props}
|
|
57
55
|
/>
|
|
58
56
|
));
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reusable data inspector component built on react-inspector.
|
|
5
|
+
*
|
|
6
|
+
* All data rendering in the o11y UI should use this component to ensure
|
|
7
|
+
* consistent theming, custom type handling (StreamRef, ClassInstanceRef),
|
|
8
|
+
* and expand behavior.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
12
|
+
import {
|
|
13
|
+
ObjectInspector,
|
|
14
|
+
ObjectLabel,
|
|
15
|
+
ObjectName,
|
|
16
|
+
ObjectRootLabel,
|
|
17
|
+
ObjectValue,
|
|
18
|
+
} from 'react-inspector';
|
|
19
|
+
import { useDarkMode } from '../../hooks/use-dark-mode';
|
|
20
|
+
import {
|
|
21
|
+
type InspectorThemeExtended,
|
|
22
|
+
inspectorThemeDark,
|
|
23
|
+
inspectorThemeExtendedDark,
|
|
24
|
+
inspectorThemeExtendedLight,
|
|
25
|
+
inspectorThemeLight,
|
|
26
|
+
} from './inspector-theme';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// StreamRef / ClassInstanceRef type detection
|
|
30
|
+
// (inline to avoid circular deps with hydration module)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const STREAM_REF_TYPE = '__workflow_stream_ref__';
|
|
34
|
+
const CLASS_INSTANCE_REF_TYPE = '__workflow_class_instance_ref__';
|
|
35
|
+
|
|
36
|
+
interface StreamRef {
|
|
37
|
+
__type: typeof STREAM_REF_TYPE;
|
|
38
|
+
streamId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isStreamRef(value: unknown): value is StreamRef {
|
|
42
|
+
return (
|
|
43
|
+
value !== null &&
|
|
44
|
+
typeof value === 'object' &&
|
|
45
|
+
'__type' in value &&
|
|
46
|
+
(value as Record<string, unknown>).__type === STREAM_REF_TYPE
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isClassInstanceRef(value: unknown): value is {
|
|
51
|
+
__type: string;
|
|
52
|
+
className: string;
|
|
53
|
+
classId: string;
|
|
54
|
+
data: unknown;
|
|
55
|
+
} {
|
|
56
|
+
return (
|
|
57
|
+
value !== null &&
|
|
58
|
+
typeof value === 'object' &&
|
|
59
|
+
'__type' in value &&
|
|
60
|
+
(value as Record<string, unknown>).__type === CLASS_INSTANCE_REF_TYPE
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Stream click context (passed through from the panel)
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Context for passing stream click handlers down to DataInspector instances.
|
|
70
|
+
* Exported so that parent components (e.g., AttributePanel) can provide the handler.
|
|
71
|
+
*/
|
|
72
|
+
export const StreamClickContext = createContext<
|
|
73
|
+
((streamId: string) => void) | undefined
|
|
74
|
+
>(undefined);
|
|
75
|
+
|
|
76
|
+
function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
|
|
77
|
+
const onStreamClick = useContext(StreamClickContext);
|
|
78
|
+
return (
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono cursor-pointer"
|
|
82
|
+
style={{
|
|
83
|
+
backgroundColor: 'var(--ds-blue-100)',
|
|
84
|
+
color: 'var(--ds-blue-800)',
|
|
85
|
+
border: '1px solid var(--ds-blue-300)',
|
|
86
|
+
}}
|
|
87
|
+
onClick={() => onStreamClick?.(streamRef.streamId)}
|
|
88
|
+
title={`View stream: ${streamRef.streamId}`}
|
|
89
|
+
>
|
|
90
|
+
<span>📡</span>
|
|
91
|
+
<span>{streamRef.streamId}</span>
|
|
92
|
+
</button>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Extended theme context (for colors react-inspector doesn't support natively)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
const ExtendedThemeContext = createContext<InspectorThemeExtended>(
|
|
101
|
+
inspectorThemeExtendedLight
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Custom nodeRenderer
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extends the default react-inspector nodeRenderer with special handling
|
|
110
|
+
* for ClassInstanceRef, StreamRef, and Date types.
|
|
111
|
+
*
|
|
112
|
+
* react-inspector renders Date instances as unstyled plain text (no theme
|
|
113
|
+
* key exists for them), so we intercept here and apply the magenta color
|
|
114
|
+
* from our extended theme — matching Node.js util.inspect()'s date style.
|
|
115
|
+
*
|
|
116
|
+
* Default nodeRenderer reference:
|
|
117
|
+
* https://github.com/storybookjs/react-inspector/blob/main/README.md#noderenderer
|
|
118
|
+
*/
|
|
119
|
+
function NodeRenderer({
|
|
120
|
+
depth,
|
|
121
|
+
name,
|
|
122
|
+
data,
|
|
123
|
+
isNonenumerable,
|
|
124
|
+
}: {
|
|
125
|
+
depth: number;
|
|
126
|
+
name?: string;
|
|
127
|
+
data: unknown;
|
|
128
|
+
isNonenumerable?: boolean;
|
|
129
|
+
expanded?: boolean;
|
|
130
|
+
}) {
|
|
131
|
+
const extendedTheme = useContext(ExtendedThemeContext);
|
|
132
|
+
|
|
133
|
+
// StreamRef → inline clickable badge
|
|
134
|
+
if (isStreamRef(data)) {
|
|
135
|
+
return (
|
|
136
|
+
<span>
|
|
137
|
+
{name != null && <ObjectName name={name} />}
|
|
138
|
+
{name != null && <span>: </span>}
|
|
139
|
+
<StreamRefInline streamRef={data} />
|
|
140
|
+
</span>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ClassInstanceRef → show className as type, data as the inspectable value
|
|
145
|
+
if (isClassInstanceRef(data)) {
|
|
146
|
+
if (depth === 0) {
|
|
147
|
+
return <ObjectRootLabel name={data.className} data={data.data} />;
|
|
148
|
+
}
|
|
149
|
+
return (
|
|
150
|
+
<span>
|
|
151
|
+
{name != null && <ObjectName name={name} />}
|
|
152
|
+
{name != null && <span>: </span>}
|
|
153
|
+
<span style={{ fontStyle: 'italic' }}>{data.className} </span>
|
|
154
|
+
<ObjectValue object={data.data} />
|
|
155
|
+
</span>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Date → magenta color (Node.js: 'date' → 'magenta')
|
|
160
|
+
// react-inspector has no OBJECT_VALUE_DATE_COLOR theme key, so we handle it here.
|
|
161
|
+
if (data instanceof Date) {
|
|
162
|
+
const dateStr = data.toISOString();
|
|
163
|
+
if (depth === 0) {
|
|
164
|
+
return (
|
|
165
|
+
<span style={{ color: extendedTheme.OBJECT_VALUE_DATE_COLOR }}>
|
|
166
|
+
{dateStr}
|
|
167
|
+
</span>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return (
|
|
171
|
+
<span>
|
|
172
|
+
{name != null && <ObjectName name={name} />}
|
|
173
|
+
{name != null && <span>: </span>}
|
|
174
|
+
<span style={{ color: extendedTheme.OBJECT_VALUE_DATE_COLOR }}>
|
|
175
|
+
{dateStr}
|
|
176
|
+
</span>
|
|
177
|
+
</span>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Default rendering (same as react-inspector's built-in)
|
|
182
|
+
if (depth === 0) {
|
|
183
|
+
return <ObjectRootLabel name={name} data={data} />;
|
|
184
|
+
}
|
|
185
|
+
return (
|
|
186
|
+
<ObjectLabel name={name} data={data} isNonenumerable={isNonenumerable} />
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Public component
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
export interface DataInspectorProps {
|
|
195
|
+
/** The data to inspect */
|
|
196
|
+
data: unknown;
|
|
197
|
+
/** Initial expand depth (default: 2) */
|
|
198
|
+
expandLevel?: number;
|
|
199
|
+
/** Optional name for the root node */
|
|
200
|
+
name?: string;
|
|
201
|
+
/** Callback when a stream reference is clicked */
|
|
202
|
+
onStreamClick?: (streamId: string) => void;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function DataInspector({
|
|
206
|
+
data,
|
|
207
|
+
expandLevel = 2,
|
|
208
|
+
name,
|
|
209
|
+
onStreamClick,
|
|
210
|
+
}: DataInspectorProps) {
|
|
211
|
+
const stableData = useStableInspectorData(data);
|
|
212
|
+
const [initialExpandLevel, setInitialExpandLevel] = useState(expandLevel);
|
|
213
|
+
const isDark = useDarkMode();
|
|
214
|
+
const extendedTheme = isDark
|
|
215
|
+
? inspectorThemeExtendedDark
|
|
216
|
+
: inspectorThemeExtendedLight;
|
|
217
|
+
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
// react-inspector reapplies expandLevel on every data change, which can
|
|
220
|
+
// reopen paths the user manually collapsed. Apply it only on mount.
|
|
221
|
+
setInitialExpandLevel(0);
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
224
|
+
const content = (
|
|
225
|
+
<ExtendedThemeContext.Provider value={extendedTheme}>
|
|
226
|
+
<ObjectInspector
|
|
227
|
+
data={stableData}
|
|
228
|
+
name={name}
|
|
229
|
+
// @ts-expect-error react-inspector accepts theme objects at runtime despite
|
|
230
|
+
// types declaring string only — see https://github.com/storybookjs/react-inspector/blob/main/README.md#theme
|
|
231
|
+
theme={isDark ? inspectorThemeDark : inspectorThemeLight}
|
|
232
|
+
expandLevel={initialExpandLevel}
|
|
233
|
+
nodeRenderer={NodeRenderer}
|
|
234
|
+
/>
|
|
235
|
+
</ExtendedThemeContext.Provider>
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Wrap in StreamClickContext if a handler is provided
|
|
239
|
+
if (onStreamClick) {
|
|
240
|
+
return (
|
|
241
|
+
<StreamClickContext.Provider value={onStreamClick}>
|
|
242
|
+
{content}
|
|
243
|
+
</StreamClickContext.Provider>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return content;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function useStableInspectorData<T>(next: T): T {
|
|
251
|
+
const previousRef = useRef<T>(next);
|
|
252
|
+
if (!isDeepEqual(previousRef.current, next)) {
|
|
253
|
+
previousRef.current = next;
|
|
254
|
+
}
|
|
255
|
+
return previousRef.current;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isObjectLike(value: unknown): value is Record<string, unknown> {
|
|
259
|
+
return typeof value === 'object' && value !== null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isDeepEqual(a: unknown, b: unknown, seen = new WeakMap()): boolean {
|
|
263
|
+
if (Object.is(a, b)) return true;
|
|
264
|
+
|
|
265
|
+
if (a instanceof Date && b instanceof Date) {
|
|
266
|
+
return a.getTime() === b.getTime();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (a instanceof RegExp && b instanceof RegExp) {
|
|
270
|
+
return a.source === b.source && a.flags === b.flags;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (a instanceof Map && b instanceof Map) {
|
|
274
|
+
if (a.size !== b.size) return false;
|
|
275
|
+
for (const [key, value] of a.entries()) {
|
|
276
|
+
if (!b.has(key) || !isDeepEqual(value, b.get(key), seen)) return false;
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (a instanceof Set && b instanceof Set) {
|
|
282
|
+
if (a.size !== b.size) return false;
|
|
283
|
+
for (const value of a.values()) {
|
|
284
|
+
if (!b.has(value)) return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!isObjectLike(a) || !isObjectLike(b)) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (seen.get(a) === b) return true;
|
|
294
|
+
seen.set(a, b);
|
|
295
|
+
|
|
296
|
+
const aIsArray = Array.isArray(a);
|
|
297
|
+
const bIsArray = Array.isArray(b);
|
|
298
|
+
if (aIsArray !== bIsArray) return false;
|
|
299
|
+
|
|
300
|
+
if (aIsArray && bIsArray) {
|
|
301
|
+
if (a.length !== b.length) return false;
|
|
302
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
303
|
+
if (!isDeepEqual(a[i], b[i], seen)) return false;
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const aKeys = Object.keys(a);
|
|
309
|
+
const bKeys = Object.keys(b);
|
|
310
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
311
|
+
|
|
312
|
+
for (const key of aKeys) {
|
|
313
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
314
|
+
if (!isDeepEqual(a[key], b[key], seen)) return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return true;
|
|
318
|
+
}
|