@workflow/web-shared 5.0.0-beta.43 → 5.0.0-beta.46
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/README.md +4 -4
- package/dist/components/event-list-view.d.ts +12 -0
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +41 -20
- package/dist/components/sidebar/attribute-panel.d.ts +6 -2
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +10 -8
- package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
- package/dist/components/sidebar/copyable-data-block.js +2 -2
- package/dist/components/sidebar/entity-detail-panel.d.ts +6 -2
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +13 -5
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +12 -5
- package/dist/components/sidebar/resolve-hook-modal.js +1 -1
- package/dist/components/sidebar/sidebar-data-context.d.ts +2 -0
- package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -1
- package/dist/components/sidebar/sidebar-data-context.js +1 -1
- package/dist/components/stream-viewer.js +2 -2
- package/dist/components/trace-viewer/components/detail-panel-width.d.ts +4 -4
- package/dist/components/trace-viewer/components/detail-panel-width.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/detail-panel-width.js +5 -5
- package/dist/components/trace-viewer/components/detail-panel.d.ts +1 -1
- package/dist/components/trace-viewer/components/detail-panel.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/detail-panel.js +3 -3
- package/dist/components/trace-viewer/components/draggable-border.d.ts +1 -1
- package/dist/components/trace-viewer/components/draggable-border.js +2 -2
- package/dist/components/trace-viewer/components/minimap.js +4 -4
- package/dist/components/trace-viewer/components/span-markers.d.ts +8 -9
- package/dist/components/trace-viewer/components/span-markers.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/span-markers.js +13 -10
- package/dist/components/trace-viewer/components/timeline.d.ts +1 -1
- package/dist/components/trace-viewer/components/timeline.js +4 -4
- package/dist/components/trace-viewer/components/use-row-window.d.ts +1 -1
- package/dist/components/trace-viewer/components/use-row-window.js +2 -2
- package/dist/components/trace-viewer/trace-viewer.js +3 -3
- package/dist/components/trace-viewer/utils.d.ts +3 -1
- package/dist/components/trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/trace-viewer/utils.js +6 -5
- package/dist/components/ui/data-inspector.d.ts +7 -1
- package/dist/components/ui/data-inspector.d.ts.map +1 -1
- package/dist/components/ui/data-inspector.js +10 -5
- package/dist/components/ui/decrypt-button.d.ts +5 -1
- package/dist/components/ui/decrypt-button.d.ts.map +1 -1
- package/dist/components/ui/decrypt-button.js +4 -4
- package/dist/components/ui/duplicate-event-tooltip.d.ts +15 -5
- package/dist/components/ui/duplicate-event-tooltip.d.ts.map +1 -1
- package/dist/components/ui/duplicate-event-tooltip.js +18 -9
- package/dist/components/ui/error-stack-block.js +4 -4
- package/dist/components/ui/timestamp-tooltip.d.ts +5 -3
- package/dist/components/ui/timestamp-tooltip.d.ts.map +1 -1
- package/dist/components/ui/timestamp-tooltip.js +9 -9
- package/dist/components/workflow-traces/event-colors.d.ts.map +1 -1
- package/dist/components/workflow-traces/event-colors.js +11 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/duplicate-events.d.ts +1 -1
- package/dist/lib/duplicate-events.js +2 -2
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +8 -13
- package/dist/lib/sealed-events.d.ts +28 -0
- package/dist/lib/sealed-events.d.ts.map +1 -0
- package/dist/lib/sealed-events.js +30 -0
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +8 -5
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.js +2 -2
- package/dist/lib/zstd-browser-decoder.js +2 -2
- package/package.json +5 -5
- package/src/components/event-list-view.tsx +57 -22
- package/src/components/sidebar/attribute-panel.tsx +15 -7
- package/src/components/sidebar/copyable-data-block.tsx +8 -1
- package/src/components/sidebar/entity-detail-panel.tsx +18 -2
- package/src/components/sidebar/events-list.tsx +16 -6
- package/src/components/sidebar/resolve-hook-modal.tsx +2 -2
- package/src/components/sidebar/sidebar-data-context.tsx +2 -0
- package/src/components/stream-viewer.tsx +1 -1
- package/src/components/trace-viewer/components/detail-panel-width.ts +4 -4
- package/src/components/trace-viewer/components/detail-panel.tsx +3 -1
- package/src/components/trace-viewer/components/draggable-border.tsx +1 -1
- package/src/components/trace-viewer/components/minimap.tsx +3 -3
- package/src/components/trace-viewer/components/span-markers.tsx +18 -13
- package/src/components/trace-viewer/components/timeline.tsx +4 -4
- package/src/components/trace-viewer/components/use-row-window.ts +1 -1
- package/src/components/trace-viewer/trace-viewer.tsx +2 -2
- package/src/components/trace-viewer/utils.test.ts +5 -3
- package/src/components/trace-viewer/utils.ts +12 -8
- package/src/components/ui/data-inspector.tsx +19 -3
- package/src/components/ui/decrypt-button.tsx +9 -2
- package/src/components/ui/duplicate-event-tooltip.tsx +32 -10
- package/src/components/ui/error-stack-block.tsx +3 -3
- package/src/components/ui/timestamp-tooltip.tsx +16 -6
- package/src/components/workflow-traces/event-colors.ts +11 -0
- package/src/components/workflow-traces/trace-span-construction.test.ts +27 -1
- package/src/components/workflow-traces/trace-span-construction.ts +5 -3
- package/src/index.ts +4 -0
- package/src/lib/duplicate-events.ts +1 -1
- package/src/lib/hydration.ts +7 -12
- package/src/lib/sealed-events.ts +32 -0
- package/src/lib/trace-builder.test.ts +29 -1
- package/src/lib/trace-builder.ts +10 -5
- package/src/lib/utils.ts +1 -1
- package/src/lib/zstd-browser-decoder.ts +1 -1
|
@@ -4,7 +4,12 @@ import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
|
4
4
|
import type { ReactNode } from 'react';
|
|
5
5
|
import { cn } from '../../../lib/cn';
|
|
6
6
|
import { TimestampTooltip } from '../../ui/timestamp-tooltip';
|
|
7
|
-
import type { SpanMarker } from '../utils';
|
|
7
|
+
import type { SpanMarker, SpanMarkerKind } from '../utils';
|
|
8
|
+
|
|
9
|
+
const MARKER_KIND_PREFIX: Record<SpanMarkerKind, string> = {
|
|
10
|
+
hook_received: 'Hook received',
|
|
11
|
+
attr_set: 'Attribute set',
|
|
12
|
+
};
|
|
8
13
|
|
|
9
14
|
// ---------------------------------------------------------------------------
|
|
10
15
|
// Marker projection
|
|
@@ -12,8 +17,9 @@ import type { SpanMarker } from '../utils';
|
|
|
12
17
|
|
|
13
18
|
export interface VisibleMarker {
|
|
14
19
|
leftPct: number;
|
|
15
|
-
/** Absolute (epoch) timestamp in ms
|
|
20
|
+
/** Absolute (epoch) timestamp in ms, for the tooltip. */
|
|
16
21
|
timeMs: number;
|
|
22
|
+
kind: SpanMarkerKind;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
/**
|
|
@@ -37,6 +43,7 @@ export function projectMarkers(
|
|
|
37
43
|
{
|
|
38
44
|
leftPct: ((m.timeMs - visibleStartMs) / visibleDurationMs) * 100,
|
|
39
45
|
timeMs: m.timeMs,
|
|
46
|
+
kind: m.kind,
|
|
40
47
|
},
|
|
41
48
|
];
|
|
42
49
|
});
|
|
@@ -48,7 +55,7 @@ const MARKER_MIN_GAP_PX = 16;
|
|
|
48
55
|
/**
|
|
49
56
|
* Thin out ticks that would visually collide: walk left-to-right and keep each
|
|
50
57
|
* one unless it sits within MARKER_MIN_GAP_PX of the last kept tick. Well-spaced
|
|
51
|
-
* markers survive even when a tight cluster elsewhere on the bar gets thinned
|
|
58
|
+
* markers survive even when a tight cluster elsewhere on the bar gets thinned;
|
|
52
59
|
* zoom in to resolve a cluster.
|
|
53
60
|
*/
|
|
54
61
|
export function cullCollidingMarkers(
|
|
@@ -80,13 +87,11 @@ function MarkerTick({ className }: { className?: string }): ReactNode {
|
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
/**
|
|
83
|
-
* Point-in-time markers overlaid on a bar
|
|
90
|
+
* Point-in-time markers overlaid on a bar: one vertical tick per event (hook
|
|
84
91
|
* resumptions and attribute writes), centered on the bar. Each tick sits inside
|
|
85
|
-
* a larger hit target and, on hover, shows the shared
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* of the product. The position is clamped a hair inside the bar so an edge
|
|
89
|
-
* marker never jams the rounded corner.
|
|
92
|
+
* a larger hit target and, on hover, shows the shared relative-time context
|
|
93
|
+
* card, prefixed with the event kind. The position is clamped a hair inside
|
|
94
|
+
* the bar so an edge marker never jams the rounded corner.
|
|
90
95
|
*/
|
|
91
96
|
export function MarkerLayer({
|
|
92
97
|
markers,
|
|
@@ -95,13 +100,13 @@ export function MarkerLayer({
|
|
|
95
100
|
}): ReactNode {
|
|
96
101
|
return (
|
|
97
102
|
<>
|
|
98
|
-
{markers.map((m) => (
|
|
103
|
+
{markers.map((m, index) => (
|
|
99
104
|
<span
|
|
100
|
-
key={m.timeMs}
|
|
105
|
+
key={`${m.timeMs}-${index}`}
|
|
101
106
|
className="pointer-events-auto absolute top-0 bottom-0 z-10 flex w-8 -translate-x-1/2 items-center justify-center"
|
|
102
107
|
style={{ left: `clamp(8px, ${m.leftPct}%, calc(100% - 8px))` }}
|
|
103
108
|
>
|
|
104
|
-
<TimestampTooltip date={m.timeMs}>
|
|
109
|
+
<TimestampTooltip date={m.timeMs} prefix={MARKER_KIND_PREFIX[m.kind]}>
|
|
105
110
|
<span className="flex h-6 w-8 items-center justify-center">
|
|
106
111
|
<MarkerTick className="h-3" />
|
|
107
112
|
</span>
|
|
@@ -147,7 +152,7 @@ export function OffscreenMarkerIndicator({
|
|
|
147
152
|
aria-label={label}
|
|
148
153
|
title={label}
|
|
149
154
|
onClick={(e) => {
|
|
150
|
-
// Don't let the row's onClick fire
|
|
155
|
+
// Don't let the row's onClick fire, since revealing shouldn't also
|
|
151
156
|
// change the span selection.
|
|
152
157
|
e.stopPropagation();
|
|
153
158
|
onReveal?.(targetMs);
|
|
@@ -432,7 +432,7 @@ const TimelineBar = memo(function TimelineBar({
|
|
|
432
432
|
);
|
|
433
433
|
|
|
434
434
|
// Markers that fall outside the visible window (scrolled off while zoomed in)
|
|
435
|
-
//
|
|
435
|
+
// are surfaced as edge indicators so they aren't silently lost.
|
|
436
436
|
const offscreen = useMemo<OffscreenMarkers>(
|
|
437
437
|
() =>
|
|
438
438
|
geometry.mode.kind === 'full'
|
|
@@ -543,7 +543,7 @@ export { TimelineBar };
|
|
|
543
543
|
// ---------------------------------------------------------------------------
|
|
544
544
|
|
|
545
545
|
// Horizontal distance between the anchor bar's measured edge and the vertical
|
|
546
|
-
// guide
|
|
546
|
+
// guide; also the width of the connector stub bridging the two.
|
|
547
547
|
const MEASURE_GUIDE_OUTSET_PX = 4;
|
|
548
548
|
|
|
549
549
|
const DeltaMeasureLine = memo(function DeltaMeasureLine({
|
|
@@ -567,7 +567,7 @@ const DeltaMeasureLine = memo(function DeltaMeasureLine({
|
|
|
567
567
|
// It sits just outside the anchor bar's measured edge (so it doesn't blend
|
|
568
568
|
// into the bar's border), joined to the bar by a short horizontal stub. The
|
|
569
569
|
// line runs from the elbow corner (the guide's x) to the hovered span's
|
|
570
|
-
// measured edge
|
|
570
|
+
// measured edge, pulled short of the edge arrow when the hovered span is
|
|
571
571
|
// fully off-screen.
|
|
572
572
|
const guideTop = Math.min(anchorCenterY, lineY);
|
|
573
573
|
const guideBottom = Math.max(anchorCenterY, lineY);
|
|
@@ -675,7 +675,7 @@ export function TimelineHeader({
|
|
|
675
675
|
export interface TimelineHover {
|
|
676
676
|
/** Pointer x as a fraction of the timeline's content width, in [0, 1]. */
|
|
677
677
|
fraction: number;
|
|
678
|
-
/** Row index under the pointer; may be past the last row
|
|
678
|
+
/** Row index under the pointer; may be past the last row (not validated). */
|
|
679
679
|
rowIndex: number;
|
|
680
680
|
}
|
|
681
681
|
|
|
@@ -96,7 +96,7 @@ export const ROW_HEIGHT_PX = 40;
|
|
|
96
96
|
* Scroll a windowed row into view by index.
|
|
97
97
|
*
|
|
98
98
|
* The list is virtualized (fixed `rowHeight` rows), so an off-screen row has no
|
|
99
|
-
* DOM node to `scrollIntoView
|
|
99
|
+
* DOM node to `scrollIntoView`; its target offset is computed from its index
|
|
100
100
|
* instead. Walks up from `listEl` to the shared scrollable ancestor (the same
|
|
101
101
|
* one `useRowWindow` measures against), only scrolls when the row sits outside
|
|
102
102
|
* the visible area, leaves a one-row `margin` of breathing room past it, and
|
|
@@ -231,7 +231,7 @@ function TraceViewerContent({
|
|
|
231
231
|
[root.startTime, root.endTime]
|
|
232
232
|
);
|
|
233
233
|
|
|
234
|
-
// Pan (keeping the current zoom) so `timeMs` is centered in view
|
|
234
|
+
// Pan (keeping the current zoom) so `timeMs` is centered in view; used by the
|
|
235
235
|
// off-screen marker indicators to scroll their marker into view.
|
|
236
236
|
const handleRevealTime = useCallback(
|
|
237
237
|
(timeMs: number) => {
|
|
@@ -307,7 +307,7 @@ function TraceViewerContent({
|
|
|
307
307
|
|
|
308
308
|
// Bring a row into view when keyboard/button navigation lands on a span that
|
|
309
309
|
// sits outside the shared scroll container's visible area. The list is
|
|
310
|
-
// windowed, so an off-screen row has no DOM node to `scrollIntoView
|
|
310
|
+
// windowed, so an off-screen row has no DOM node to `scrollIntoView`;
|
|
311
311
|
// `scrollRowIntoView` computes the target offset from the span's index.
|
|
312
312
|
const scrollSpanIntoView = useCallback(
|
|
313
313
|
(spanId: string) => {
|
|
@@ -153,8 +153,10 @@ describe('computeSpanMarkers', () => {
|
|
|
153
153
|
attrSetMs: [10_000, 70_000],
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
-
expect(computeSpanMarkers(span)
|
|
157
|
-
10_000,
|
|
156
|
+
expect(computeSpanMarkers(span)).toEqual([
|
|
157
|
+
{ timeMs: 10_000, kind: 'attr_set' },
|
|
158
|
+
{ timeMs: 50_000, kind: 'hook_received' },
|
|
159
|
+
{ timeMs: 70_000, kind: 'attr_set' },
|
|
158
160
|
]);
|
|
159
161
|
});
|
|
160
162
|
|
|
@@ -165,7 +167,7 @@ describe('computeSpanMarkers', () => {
|
|
|
165
167
|
});
|
|
166
168
|
|
|
167
169
|
describe('computeOffscreenMarkers', () => {
|
|
168
|
-
const mk = (timeMs: number) => ({ timeMs });
|
|
170
|
+
const mk = (timeMs: number) => ({ timeMs, kind: 'hook_received' as const });
|
|
169
171
|
|
|
170
172
|
it('partitions markers by side with the nearest one per side', () => {
|
|
171
173
|
const markers = [mk(5), mk(8), mk(50), mk(92), mk(99)];
|
|
@@ -78,7 +78,7 @@ export function clampViewportToRoot(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// ---------------------------------------------------------------------------
|
|
81
|
-
// Wheel gestures
|
|
81
|
+
// Wheel gestures: shared between the timeline and the minimap
|
|
82
82
|
// ---------------------------------------------------------------------------
|
|
83
83
|
|
|
84
84
|
/** Convert a wheel delta to pixel units (line-mode deltas arrive in lines). */
|
|
@@ -164,7 +164,7 @@ export function computeTimeMarkers(
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// ---------------------------------------------------------------------------
|
|
167
|
-
// Span gaps
|
|
167
|
+
// Span gaps: time deltas between consecutive spans (Alt-key overlay)
|
|
168
168
|
// ---------------------------------------------------------------------------
|
|
169
169
|
|
|
170
170
|
export interface SpanGap {
|
|
@@ -254,8 +254,8 @@ export function computeSpanDelta(
|
|
|
254
254
|
const deltaMs = laterStart - originMs;
|
|
255
255
|
|
|
256
256
|
// Entirely outside the viewport. Compared in time-space (not clamped
|
|
257
|
-
// fractions) so a zero-delta point sitting exactly on a viewport edge
|
|
258
|
-
// e.g. the root span selected at default zoom
|
|
257
|
+
// fractions) so a zero-delta point sitting exactly on a viewport edge
|
|
258
|
+
// (e.g. the root span selected at default zoom) still renders.
|
|
259
259
|
if (laterStart < viewStart || originMs > viewEnd) {
|
|
260
260
|
return null;
|
|
261
261
|
}
|
|
@@ -294,7 +294,7 @@ export const RESOURCE_CLASS_NAMES: Record<
|
|
|
294
294
|
className: 'border-green-500 bg-green-200',
|
|
295
295
|
errorClassName: 'border-red-500 bg-red-200',
|
|
296
296
|
},
|
|
297
|
-
// Passive spans (hooks) stay gray
|
|
297
|
+
// Passive spans (hooks) stay gray; matches event-list icons and the minimap.
|
|
298
298
|
hook: {
|
|
299
299
|
className: 'border-gray-500 bg-gray-200',
|
|
300
300
|
errorClassName: 'border-red-500 bg-red-200',
|
|
@@ -317,7 +317,7 @@ export function getResourceClassNames(resource: string): {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
// ---------------------------------------------------------------------------
|
|
320
|
-
// Span segments
|
|
320
|
+
// Span segments: split a timeline bar into colored sections by event state
|
|
321
321
|
// ---------------------------------------------------------------------------
|
|
322
322
|
|
|
323
323
|
export type SegmentStatus =
|
|
@@ -603,11 +603,14 @@ export function computeSpanSegments(span: Span): Segment[] {
|
|
|
603
603
|
}
|
|
604
604
|
|
|
605
605
|
// ---------------------------------------------------------------------------
|
|
606
|
-
// Span markers
|
|
606
|
+
// Span markers: point-in-time events rendered as ticks on top of a bar
|
|
607
607
|
// ---------------------------------------------------------------------------
|
|
608
608
|
|
|
609
|
+
export type SpanMarkerKind = 'hook_received' | 'attr_set';
|
|
610
|
+
|
|
609
611
|
export interface SpanMarker {
|
|
610
612
|
timeMs: number;
|
|
613
|
+
kind: SpanMarkerKind;
|
|
611
614
|
}
|
|
612
615
|
|
|
613
616
|
// `hook_received` = a resumption; `attr_set` = attributes written mid-span.
|
|
@@ -616,12 +619,13 @@ const MARKER_EVENT_NAMES = ['hook_received', 'attr_set'];
|
|
|
616
619
|
export function computeSpanMarkers(span: Span): SpanMarker[] {
|
|
617
620
|
return sortedEventMarks(span.events, MARKER_EVENT_NAMES).map((mark) => ({
|
|
618
621
|
timeMs: mark.time,
|
|
622
|
+
kind: mark.type as SpanMarkerKind,
|
|
619
623
|
}));
|
|
620
624
|
}
|
|
621
625
|
|
|
622
626
|
export interface OffscreenSide {
|
|
623
627
|
count: number;
|
|
624
|
-
/** Nearest off-screen marker
|
|
628
|
+
/** Nearest off-screen marker: the one a reveal jumps to. */
|
|
625
629
|
nearestMs: number;
|
|
626
630
|
}
|
|
627
631
|
|
|
@@ -145,6 +145,8 @@ export const StreamClickContext = createContext<
|
|
|
145
145
|
export type DecryptClickContextValue = {
|
|
146
146
|
onDecrypt: () => void;
|
|
147
147
|
isDecrypting: boolean;
|
|
148
|
+
isDecryptDisabled?: boolean;
|
|
149
|
+
decryptDisabledReason?: string;
|
|
148
150
|
hasEncryptedData?: boolean;
|
|
149
151
|
};
|
|
150
152
|
|
|
@@ -167,7 +169,8 @@ function EncryptedInlineLabel() {
|
|
|
167
169
|
<Button
|
|
168
170
|
size="xs"
|
|
169
171
|
className="align-baseline gap-x-1"
|
|
170
|
-
disabled={ctx.isDecrypting}
|
|
172
|
+
disabled={ctx.isDecrypting || ctx.isDecryptDisabled}
|
|
173
|
+
title={ctx.isDecryptDisabled ? ctx.decryptDisabledReason : undefined}
|
|
171
174
|
onClick={(e) => {
|
|
172
175
|
e.stopPropagation();
|
|
173
176
|
ctx.onDecrypt();
|
|
@@ -842,7 +845,7 @@ export function collapseRefs(data: unknown): unknown {
|
|
|
842
845
|
if (data instanceof Set) {
|
|
843
846
|
return new Set(Array.from(data.values(), collapseRefs));
|
|
844
847
|
}
|
|
845
|
-
// Only recurse into plain objects
|
|
848
|
+
// Only recurse into plain objects; leave class instances untouched
|
|
846
849
|
const proto = Object.getPrototypeOf(data);
|
|
847
850
|
if (proto !== Object.prototype && proto !== null) return data;
|
|
848
851
|
const result: Record<string, unknown> = {};
|
|
@@ -871,6 +874,10 @@ export interface DataInspectorProps {
|
|
|
871
874
|
onDecrypt?: () => void;
|
|
872
875
|
/** Whether decryption is currently in progress */
|
|
873
876
|
isDecrypting?: boolean;
|
|
877
|
+
/** Whether decryption is unavailable */
|
|
878
|
+
isDecryptDisabled?: boolean;
|
|
879
|
+
/** Explains why decryption is unavailable */
|
|
880
|
+
decryptDisabledReason?: string;
|
|
874
881
|
}
|
|
875
882
|
|
|
876
883
|
export function DataInspector({
|
|
@@ -881,6 +888,8 @@ export function DataInspector({
|
|
|
881
888
|
onRunClick,
|
|
882
889
|
onDecrypt,
|
|
883
890
|
isDecrypting = false,
|
|
891
|
+
isDecryptDisabled = false,
|
|
892
|
+
decryptDisabledReason,
|
|
884
893
|
}: DataInspectorProps) {
|
|
885
894
|
const collapsedData = useMemo(() => collapseRefs(data), [data]);
|
|
886
895
|
const stableData = useStableInspectorData(collapsedData);
|
|
@@ -912,7 +921,14 @@ export function DataInspector({
|
|
|
912
921
|
}
|
|
913
922
|
if (onDecrypt) {
|
|
914
923
|
content = (
|
|
915
|
-
<DecryptClickContext.Provider
|
|
924
|
+
<DecryptClickContext.Provider
|
|
925
|
+
value={{
|
|
926
|
+
onDecrypt,
|
|
927
|
+
isDecrypting,
|
|
928
|
+
isDecryptDisabled,
|
|
929
|
+
decryptDisabledReason,
|
|
930
|
+
}}
|
|
931
|
+
>
|
|
916
932
|
{content}
|
|
917
933
|
</DecryptClickContext.Provider>
|
|
918
934
|
);
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import { Spinner } from './spinner';
|
|
4
4
|
|
|
5
|
-
const STYLES = `.wf-decrypt-btn{appearance:none;-webkit-appearance:none;border:none;display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 12px;border-radius:6px;font-size:14px;font-weight:500;line-height:20px;cursor:pointer;white-space:nowrap;gap:6px;transition:background 150ms}.wf-decrypt-idle{color:var(--ds-gray-1000);background:var(--ds-background-100);box-shadow:0 0 0 1px var(--ds-gray-400)}.wf-decrypt-idle:hover{background:var(--ds-gray-alpha-200)}.wf-decrypt-done{color:var(--ds-green-900);background:var(--ds-green-100);box-shadow:0 0 0 1px var(--ds-green-400);cursor:default}`;
|
|
5
|
+
const STYLES = `.wf-decrypt-btn{appearance:none;-webkit-appearance:none;border:none;display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 12px;border-radius:6px;font-size:14px;font-weight:500;line-height:20px;cursor:pointer;white-space:nowrap;gap:6px;transition:background 150ms}.wf-decrypt-idle{color:var(--ds-gray-1000);background:var(--ds-background-100);box-shadow:0 0 0 1px var(--ds-gray-400)}.wf-decrypt-idle:hover{background:var(--ds-gray-alpha-200)}.wf-decrypt-btn:disabled{cursor:not-allowed;opacity:.5}.wf-decrypt-done{color:var(--ds-green-900);background:var(--ds-green-100);box-shadow:0 0 0 1px var(--ds-green-400);cursor:default}`;
|
|
6
6
|
|
|
7
7
|
interface DecryptButtonProps {
|
|
8
8
|
/** Whether an encryption key has been obtained (decryption is active). */
|
|
9
9
|
decrypted?: boolean;
|
|
10
10
|
/** Whether the key is currently being fetched. */
|
|
11
11
|
loading?: boolean;
|
|
12
|
+
/** Whether decryption is unavailable. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Explains why decryption is unavailable. */
|
|
15
|
+
disabledReason?: string;
|
|
12
16
|
/** Called when the user clicks to initiate decryption. */
|
|
13
17
|
onClick?: () => void;
|
|
14
18
|
}
|
|
@@ -20,6 +24,8 @@ interface DecryptButtonProps {
|
|
|
20
24
|
export function DecryptButton({
|
|
21
25
|
decrypted = false,
|
|
22
26
|
loading = false,
|
|
27
|
+
disabled = false,
|
|
28
|
+
disabledReason,
|
|
23
29
|
onClick,
|
|
24
30
|
}: DecryptButtonProps) {
|
|
25
31
|
return (
|
|
@@ -28,7 +34,8 @@ export function DecryptButton({
|
|
|
28
34
|
<button
|
|
29
35
|
type="button"
|
|
30
36
|
onClick={decrypted ? undefined : onClick}
|
|
31
|
-
disabled={decrypted || loading}
|
|
37
|
+
disabled={decrypted || loading || disabled}
|
|
38
|
+
title={disabled ? disabledReason : undefined}
|
|
32
39
|
className={`wf-decrypt-btn ${decrypted ? 'wf-decrypt-done' : 'wf-decrypt-idle'}`}
|
|
33
40
|
>
|
|
34
41
|
{loading ? (
|
|
@@ -10,22 +10,24 @@ import {
|
|
|
10
10
|
} from './tooltip';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Explains why an event is shown greyed out
|
|
14
|
-
*
|
|
13
|
+
* Explains why an event row is shown greyed out, whether it is a repeat the
|
|
14
|
+
* runtime read past ({@link DUPLICATE_EVENT_MESSAGE}), a backend seal for an
|
|
15
|
+
* abandoned position (`SEALED_EVENT_MESSAGE`), or any other notice a list
|
|
16
|
+
* attaches.
|
|
15
17
|
*
|
|
16
|
-
* Renders `children` untouched when `
|
|
18
|
+
* Renders `children` untouched when `notice` is absent, so a call site can
|
|
17
19
|
* wrap an event label unconditionally. Mounts its own {@link TooltipProvider}
|
|
18
|
-
* so it works in the sidebar and the events table alike; nesting one inside
|
|
19
|
-
* existing provider is harmless.
|
|
20
|
+
* so it works in the sidebar and the events table alike; nesting one inside
|
|
21
|
+
* an existing provider is harmless.
|
|
20
22
|
*/
|
|
21
|
-
export function
|
|
22
|
-
|
|
23
|
+
export function EventNoticeTooltip({
|
|
24
|
+
notice,
|
|
23
25
|
children,
|
|
24
26
|
}: {
|
|
25
|
-
|
|
27
|
+
notice?: string;
|
|
26
28
|
children: ReactNode;
|
|
27
29
|
}): ReactNode {
|
|
28
|
-
if (!
|
|
30
|
+
if (!notice) return children;
|
|
29
31
|
|
|
30
32
|
return (
|
|
31
33
|
<TooltipProvider delayDuration={300}>
|
|
@@ -36,9 +38,29 @@ export function DuplicateEventTooltip({
|
|
|
36
38
|
collisionPadding={8}
|
|
37
39
|
side="top"
|
|
38
40
|
>
|
|
39
|
-
{
|
|
41
|
+
{notice}
|
|
40
42
|
</TooltipContent>
|
|
41
43
|
</Tooltip>
|
|
42
44
|
</TooltipProvider>
|
|
43
45
|
);
|
|
44
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The duplicate-specific wrapper kept for existing call sites: greys the
|
|
50
|
+
* event out as a repeat the runtime read past.
|
|
51
|
+
*/
|
|
52
|
+
export function DuplicateEventTooltip({
|
|
53
|
+
isDuplicate = false,
|
|
54
|
+
children,
|
|
55
|
+
}: {
|
|
56
|
+
isDuplicate?: boolean;
|
|
57
|
+
children: ReactNode;
|
|
58
|
+
}): ReactNode {
|
|
59
|
+
return (
|
|
60
|
+
<EventNoticeTooltip
|
|
61
|
+
notice={isDuplicate ? DUPLICATE_EVENT_MESSAGE : undefined}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</EventNoticeTooltip>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -39,10 +39,10 @@ export function isStructuredErrorWithStack(
|
|
|
39
39
|
/**
|
|
40
40
|
* Pull a short, single-line title out of an error message.
|
|
41
41
|
*
|
|
42
|
-
* Workflow's structured error messages are multi-line
|
|
42
|
+
* Workflow's structured error messages are multi-line: the first line is
|
|
43
43
|
* the headline (`Failed to serialize step return value`) and the rest are
|
|
44
44
|
* `╰▶ hint:` / `╰▶ docs:` framed details. The full message belongs in the
|
|
45
|
-
* body of the error block; the title should
|
|
45
|
+
* body of the error block; the title should be the headline so the
|
|
46
46
|
* card stays scannable.
|
|
47
47
|
*/
|
|
48
48
|
function deriveTitle(message: string): string {
|
|
@@ -94,7 +94,7 @@ export function ErrorStackBlock({ value }: { value: StructuredErrorRecord }) {
|
|
|
94
94
|
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
95
95
|
<p
|
|
96
96
|
className="text-label-12 font-semibold m-0 truncate"
|
|
97
|
-
// The full message or stack is in the body below; the header
|
|
97
|
+
// The full message or stack is in the body below; the header
|
|
98
98
|
// shows the first line, single-line, with overflow
|
|
99
99
|
// ellipsised so a long title doesn't push the copy button or
|
|
100
100
|
// wrap into the framed hint/docs lines.
|
|
@@ -156,7 +156,13 @@ function ZoneDateTimeRow({
|
|
|
156
156
|
);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
function RelativeTimeContextCardContent({
|
|
159
|
+
function RelativeTimeContextCardContent({
|
|
160
|
+
date,
|
|
161
|
+
prefix,
|
|
162
|
+
}: {
|
|
163
|
+
date: number;
|
|
164
|
+
prefix?: string;
|
|
165
|
+
}): ReactNode {
|
|
160
166
|
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
161
167
|
const timeAgo = useTimeAgo(date);
|
|
162
168
|
|
|
@@ -164,7 +170,7 @@ function RelativeTimeContextCardContent({ date }: { date: number }): ReactNode {
|
|
|
164
170
|
<div className="flex flex-col gap-3 min-w-[300px]">
|
|
165
171
|
<div className="flex flex-col gap-3">
|
|
166
172
|
<span className="tabular-nums text-label-13 text-gray-900">
|
|
167
|
-
{timeAgo}
|
|
173
|
+
{prefix ? `${prefix} ${timeAgo}` : timeAgo}
|
|
168
174
|
</span>
|
|
169
175
|
</div>
|
|
170
176
|
<div className="flex flex-col gap-2">
|
|
@@ -196,17 +202,19 @@ type RelativeTimeCardProps = Omit<
|
|
|
196
202
|
date?: number | null;
|
|
197
203
|
/** Custom content to render instead of the default relative time text. */
|
|
198
204
|
children?: ReactNode;
|
|
205
|
+
prefix?: string;
|
|
199
206
|
};
|
|
200
207
|
|
|
201
208
|
/**
|
|
202
209
|
* Relative time label that reveals a context card with detailed UTC and local
|
|
203
210
|
* timestamps on hover. Renders a default short relative time label (e.g.
|
|
204
|
-
* "3 days ago") when `children` is omitted; renders
|
|
211
|
+
* "3 days ago") when `children` is omitted; renders only the children without
|
|
205
212
|
* the hover card when `date` is nullish.
|
|
206
213
|
*/
|
|
207
214
|
export function RelativeTimeCard({
|
|
208
215
|
date,
|
|
209
216
|
children: _children,
|
|
217
|
+
prefix,
|
|
210
218
|
...props
|
|
211
219
|
}: RelativeTimeCardProps): ReactNode {
|
|
212
220
|
const children =
|
|
@@ -216,7 +224,7 @@ export function RelativeTimeCard({
|
|
|
216
224
|
|
|
217
225
|
return (
|
|
218
226
|
<ContextCardTrigger
|
|
219
|
-
content={<RelativeTimeContextCardContent date={date} />}
|
|
227
|
+
content={<RelativeTimeContextCardContent date={date} prefix={prefix} />}
|
|
220
228
|
{...props}
|
|
221
229
|
>
|
|
222
230
|
{children}
|
|
@@ -225,7 +233,7 @@ export function RelativeTimeCard({
|
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
// ---------------------------------------------------------------------------
|
|
228
|
-
// TimestampTooltip
|
|
236
|
+
// TimestampTooltip: convenience wrapper used across the observability UI
|
|
229
237
|
// ---------------------------------------------------------------------------
|
|
230
238
|
|
|
231
239
|
/**
|
|
@@ -238,10 +246,12 @@ export function TimestampTooltip({
|
|
|
238
246
|
date,
|
|
239
247
|
children,
|
|
240
248
|
side = 'top',
|
|
249
|
+
prefix,
|
|
241
250
|
}: {
|
|
242
251
|
date: number | Date | string | null | undefined;
|
|
243
252
|
children: ReactNode;
|
|
244
253
|
side?: ContextCardTriggerProps['side'];
|
|
254
|
+
prefix?: string;
|
|
245
255
|
}): ReactNode {
|
|
246
256
|
const hasProvider = useHasContextCardProvider();
|
|
247
257
|
|
|
@@ -255,7 +265,7 @@ export function TimestampTooltip({
|
|
|
255
265
|
if (ts == null || Number.isNaN(ts)) return <>{children}</>;
|
|
256
266
|
|
|
257
267
|
const card = (
|
|
258
|
-
<RelativeTimeCard date={ts} side={side} asChild>
|
|
268
|
+
<RelativeTimeCard date={ts} side={side} prefix={prefix} asChild>
|
|
259
269
|
{children}
|
|
260
270
|
</RelativeTimeCard>
|
|
261
271
|
);
|
|
@@ -72,6 +72,17 @@ export function getEventColor(
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Sealed positions - neutral gray: backend log filler, not run activity
|
|
76
|
+
if (eventType === 'noop') {
|
|
77
|
+
return {
|
|
78
|
+
color: 'var(--ds-gray-500)',
|
|
79
|
+
background: 'var(--ds-gray-100)',
|
|
80
|
+
border: 'var(--ds-gray-400)',
|
|
81
|
+
text: 'var(--ds-gray-900)',
|
|
82
|
+
secondary: 'var(--ds-gray-700)',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
// Default - Blue
|
|
76
87
|
return {
|
|
77
88
|
color: 'var(--ds-blue-600)',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Event, WorkflowRun } from '@workflow/world';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { runToSpan } from './trace-span-construction';
|
|
3
|
+
import { runToSpan, stepToSpan } from './trace-span-construction';
|
|
4
4
|
import { otelTimeToMs } from './trace-time-utils';
|
|
5
5
|
|
|
6
6
|
const date = (ms: number) => new Date(ms);
|
|
@@ -83,3 +83,29 @@ describe('runToSpan', () => {
|
|
|
83
83
|
expect(data).not.toHaveProperty('occurredAt');
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
describe('stepToSpan', () => {
|
|
88
|
+
it('uses the short name for a workflow-prefixed step name', () => {
|
|
89
|
+
const span = stepToSpan(
|
|
90
|
+
[
|
|
91
|
+
event({
|
|
92
|
+
eventType: 'step_created',
|
|
93
|
+
correlationId: 'step_1',
|
|
94
|
+
eventData: {
|
|
95
|
+
stepName: 'workflow//src/billing.ts//fetchInvoices',
|
|
96
|
+
input: null,
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
event({
|
|
100
|
+
eventType: 'step_completed',
|
|
101
|
+
correlationId: 'step_1',
|
|
102
|
+
createdAt: date(2_000),
|
|
103
|
+
eventData: { result: null },
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
date(2_000)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(span?.name).toBe('fetchInvoices');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -175,7 +175,7 @@ export const stepEventsToStepEntity = (
|
|
|
175
175
|
return null;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
// Walk events
|
|
178
|
+
// Walk events to derive status, attempt count, and timestamps.
|
|
179
179
|
// Handles both step_retrying and consecutive step_started as retry signals.
|
|
180
180
|
let status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' =
|
|
181
181
|
'pending';
|
|
@@ -236,7 +236,9 @@ export function stepToSpan(stepEvents: Event[], maxEndTime: Date): Span | null {
|
|
|
236
236
|
if (!step) {
|
|
237
237
|
return null;
|
|
238
238
|
}
|
|
239
|
-
const parsedName =
|
|
239
|
+
const parsedName =
|
|
240
|
+
parseStepName(String(step.stepName)) ??
|
|
241
|
+
parseWorkflowName(String(step.stepName));
|
|
240
242
|
|
|
241
243
|
const attributes = {
|
|
242
244
|
resource: 'step' as const,
|
|
@@ -404,7 +406,7 @@ export function runToSpan(
|
|
|
404
406
|
getEventTimestamp(terminalEvent) ?? run.completedAt ?? undefined;
|
|
405
407
|
const endTime = completedAt ?? now;
|
|
406
408
|
|
|
407
|
-
// Only embed identification fields
|
|
409
|
+
// Only embed identification fields, not the full object with
|
|
408
410
|
// input/output/error which may contain non-cloneable types. Lifecycle
|
|
409
411
|
// timestamps are event-derived so detail rows align with the span timeline.
|
|
410
412
|
const { input: _i, output: _o, error: _e, ...runIdentity } = run;
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,10 @@ export {
|
|
|
59
59
|
STREAM_REF_TYPE,
|
|
60
60
|
truncateId,
|
|
61
61
|
} from './lib/hydration';
|
|
62
|
+
export {
|
|
63
|
+
isSealedNoopEvent,
|
|
64
|
+
SEALED_EVENT_MESSAGE,
|
|
65
|
+
} from './lib/sealed-events';
|
|
62
66
|
export type { DecodedStreamChunkSource } from './lib/stream-display';
|
|
63
67
|
export type { ToastAdapter } from './lib/toast';
|
|
64
68
|
export { ToastProvider, useToast } from './lib/toast';
|
|
@@ -162,7 +162,7 @@ function foldDuplicates(ordered: readonly Event[]): Set<string> {
|
|
|
162
162
|
* `isCompleteHistory` must be false whenever the caller holds a subset of the
|
|
163
163
|
* run's log: one page of a paginated list, or the result of a search. Which
|
|
164
164
|
* occurrence of a class came first is a property of the whole log, so on a
|
|
165
|
-
* subset the earlier event may
|
|
165
|
+
* subset the earlier event may be missing, and the fold would report
|
|
166
166
|
* the surviving one. Nothing is classified in that case.
|
|
167
167
|
*
|
|
168
168
|
* Two other things make the answer unknowable and yield the same empty result:
|