@workflow/web-shared 5.0.0-beta.23 → 5.0.0-beta.25
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 +2 -10
- package/dist/components/event-list-view.d.ts +30 -0
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +27 -12
- package/dist/components/index.d.ts +4 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -3
- package/dist/components/new-trace-viewer/components/copy-button.js +2 -2
- package/dist/components/new-trace-viewer/components/event-list.js +2 -2
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +2 -2
- package/dist/components/new-trace-viewer/components/span-markers.js +3 -3
- package/dist/components/new-trace-viewer/components/split-pane.js +2 -2
- package/dist/components/new-trace-viewer/components/timeline.js +2 -2
- package/dist/components/new-trace-viewer/trace-viewer.js +4 -4
- package/dist/components/run-trace-view.d.ts +5 -7
- package/dist/components/run-trace-view.d.ts.map +1 -1
- package/dist/components/run-trace-view.js +3 -3
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +3 -5
- package/dist/components/sidebar/detail-card.js +2 -2
- package/dist/components/sidebar/entity-detail-panel.d.ts +6 -10
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +8 -60
- package/dist/components/sidebar/events-list.d.ts +3 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +31 -9
- package/dist/components/sidebar/sidebar-data-context.d.ts +5 -6
- 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/sidebar/span-detail-merge.d.ts +15 -0
- package/dist/components/sidebar/span-detail-merge.d.ts.map +1 -1
- package/dist/components/sidebar/span-detail-merge.js +40 -1
- package/dist/components/sidebar/use-selected-span-detail.d.ts +16 -0
- package/dist/components/sidebar/use-selected-span-detail.d.ts.map +1 -0
- package/dist/components/sidebar/use-selected-span-detail.js +93 -0
- package/dist/components/ui/alert.js +2 -2
- package/dist/components/ui/button.js +2 -2
- package/dist/components/ui/card.js +2 -2
- package/dist/components/ui/context-card.js +2 -2
- package/dist/components/ui/error-card.js +2 -2
- package/dist/components/ui/icon-button.js +2 -2
- package/dist/components/ui/kbd.js +2 -2
- package/dist/components/ui/skeleton.js +2 -2
- package/dist/components/ui/tooltip.js +2 -2
- package/dist/components/workflow-trace-view.d.ts +6 -7
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +3 -6
- package/dist/components/workflow-traces/trace-span-construction.d.ts +4 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +69 -29
- package/dist/lib/cn.d.ts +10 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +83 -0
- package/dist/lib/event-materialization.d.ts.map +1 -1
- package/dist/lib/event-materialization.js +22 -15
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +3 -3
- package/dist/lib/utils.d.ts +0 -2
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +1 -6
- package/package.json +3 -3
- package/src/components/event-list-view.tsx +76 -13
- package/src/components/index.ts +10 -6
- package/src/components/new-trace-viewer/components/copy-button.tsx +1 -1
- package/src/components/new-trace-viewer/components/event-list.tsx +1 -1
- package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +1 -1
- package/src/components/new-trace-viewer/components/span-markers.tsx +2 -2
- package/src/components/new-trace-viewer/components/split-pane.tsx +1 -1
- package/src/components/new-trace-viewer/components/timeline.tsx +1 -1
- package/src/components/new-trace-viewer/trace-viewer.tsx +6 -6
- package/src/components/run-trace-view.tsx +10 -14
- package/src/components/sidebar/attribute-panel.tsx +2 -4
- package/src/components/sidebar/detail-card.tsx +1 -1
- package/src/components/sidebar/entity-detail-panel.tsx +23 -94
- package/src/components/sidebar/events-list.tsx +58 -8
- package/src/components/sidebar/sidebar-data-context.tsx +6 -7
- package/src/components/sidebar/span-detail-merge.ts +69 -0
- package/src/components/sidebar/use-selected-span-detail.ts +146 -0
- package/src/components/ui/alert.tsx +1 -1
- package/src/components/ui/button.tsx +1 -1
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/context-card.tsx +1 -1
- package/src/components/ui/error-card.tsx +1 -1
- package/src/components/ui/icon-button.tsx +1 -1
- package/src/components/ui/kbd.tsx +1 -1
- package/src/components/ui/skeleton.tsx +1 -1
- package/src/components/ui/tooltip.tsx +1 -1
- package/src/components/workflow-trace-view.tsx +11 -21
- package/src/components/workflow-traces/trace-span-construction.test.ts +85 -0
- package/src/components/workflow-traces/trace-span-construction.ts +93 -29
- package/src/lib/cn.ts +93 -0
- package/src/lib/event-materialization.ts +22 -14
- package/src/lib/trace-builder.ts +2 -1
- package/src/lib/utils.ts +0 -6
|
@@ -768,10 +768,8 @@ export const AttributePanel = ({
|
|
|
768
768
|
|
|
769
769
|
if (resource === 'sleep') return present;
|
|
770
770
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const loadingDefaults = ['input', 'output'];
|
|
774
|
-
for (const key of loadingDefaults) {
|
|
771
|
+
const loadingPlaceholders = ['input', 'output'];
|
|
772
|
+
for (const key of loadingPlaceholders) {
|
|
775
773
|
if (!present.includes(key)) {
|
|
776
774
|
present.push(key);
|
|
777
775
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type { Event, Hook,
|
|
3
|
+
import type { Event, Hook, WorkflowRun } from '@workflow/world';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import { Send, Zap } from 'lucide-react';
|
|
6
|
-
import { useCallback, useEffect, useMemo,
|
|
6
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
7
|
import { useToast } from '../../lib/toast';
|
|
8
8
|
import { DecryptClickContext } from '../ui/data-inspector';
|
|
9
9
|
import { AttributePanel } from './attribute-panel';
|
|
@@ -11,19 +11,11 @@ import { EventsList } from './events-list';
|
|
|
11
11
|
import { ResolveHookModal } from './resolve-hook-modal';
|
|
12
12
|
import { useSidebarDataOptional } from './sidebar-data-context';
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from './span-detail
|
|
17
|
-
|
|
18
|
-
// Type guards for runtime validation of span attribute data
|
|
19
|
-
function isStep(data: unknown): data is Step {
|
|
20
|
-
return data !== null && typeof data === 'object' && 'stepId' in data;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function isWorkflowRun(data: unknown): data is WorkflowRun {
|
|
24
|
-
return data !== null && typeof data === 'object' && 'runId' in data;
|
|
25
|
-
}
|
|
14
|
+
type FetchSpanDetail,
|
|
15
|
+
useSelectedSpanDetail,
|
|
16
|
+
} from './use-selected-span-detail';
|
|
26
17
|
|
|
18
|
+
// Type guard for runtime validation of span attribute data
|
|
27
19
|
function isHook(data: unknown): data is Hook {
|
|
28
20
|
return data !== null && typeof data === 'object' && 'hookId' in data;
|
|
29
21
|
}
|
|
@@ -61,10 +53,7 @@ export function EntityDetailPanel({
|
|
|
61
53
|
run,
|
|
62
54
|
onStreamClick,
|
|
63
55
|
onRunClick,
|
|
64
|
-
|
|
65
|
-
spanDetailError,
|
|
66
|
-
spanDetailLoading,
|
|
67
|
-
onSpanSelect,
|
|
56
|
+
fetchSpanDetail,
|
|
68
57
|
onWakeUpSleep,
|
|
69
58
|
onLoadEventData,
|
|
70
59
|
onResolveHook,
|
|
@@ -72,20 +61,14 @@ export function EntityDetailPanel({
|
|
|
72
61
|
onDecrypt,
|
|
73
62
|
isDecrypting = false,
|
|
74
63
|
selectedSpan,
|
|
64
|
+
showSeparateEventOccurrenceTimestamps = false,
|
|
75
65
|
}: {
|
|
76
66
|
run: WorkflowRun;
|
|
77
67
|
/** Callback when a stream reference is clicked */
|
|
78
68
|
onStreamClick?: (streamId: string) => void;
|
|
79
69
|
/** Callback when a run reference is clicked */
|
|
80
70
|
onRunClick?: (runId: string) => void;
|
|
81
|
-
|
|
82
|
-
spanDetailData: WorkflowRun | Step | Hook | Event | null;
|
|
83
|
-
/** Error from external span detail fetch. */
|
|
84
|
-
spanDetailError?: Error | null;
|
|
85
|
-
/** Loading state from external span detail fetch. */
|
|
86
|
-
spanDetailLoading?: boolean;
|
|
87
|
-
/** Callback when a span is selected. Use this to fetch data externally and pass via spanDetailData. */
|
|
88
|
-
onSpanSelect: (info: SpanSelectionInfo) => void;
|
|
71
|
+
fetchSpanDetail: FetchSpanDetail;
|
|
89
72
|
/** Callback to wake up a pending sleep call. */
|
|
90
73
|
onWakeUpSleep?: (
|
|
91
74
|
runId: string,
|
|
@@ -110,6 +93,8 @@ export function EntityDetailPanel({
|
|
|
110
93
|
isDecrypting?: boolean;
|
|
111
94
|
/** Info about the currently selected span from the trace viewer */
|
|
112
95
|
selectedSpan: SelectedSpanInfo | null;
|
|
96
|
+
/** Show occurredAt separately instead of folding it into the Created timestamp. */
|
|
97
|
+
showSeparateEventOccurrenceTimestamps?: boolean;
|
|
113
98
|
}): React.JSX.Element | null {
|
|
114
99
|
const toast = useToast();
|
|
115
100
|
const [stoppingSleep, setStoppingSleep] = useState(false);
|
|
@@ -128,53 +113,15 @@ export function EntityDetailPanel({
|
|
|
128
113
|
const rawEvents = selectedSpan?.rawEvents;
|
|
129
114
|
const rawEventsLength = rawEvents?.length ?? 0;
|
|
130
115
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
if (res === 'run' && isWorkflowRun(data)) {
|
|
142
|
-
return { resource: 'run', resourceId: data.runId, runId: undefined };
|
|
143
|
-
}
|
|
144
|
-
if (res === 'hook' && isHook(data)) {
|
|
145
|
-
return { resource: 'hook', resourceId: data.hookId, runId: undefined };
|
|
146
|
-
}
|
|
147
|
-
if (res === 'sleep') {
|
|
148
|
-
const waitData = data as { runId?: string } | undefined;
|
|
149
|
-
return {
|
|
150
|
-
resource: 'sleep',
|
|
151
|
-
resourceId: selectedSpan.spanId,
|
|
152
|
-
runId: waitData?.runId,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
return { resource: undefined, resourceId: undefined, runId: undefined };
|
|
156
|
-
}, [selectedSpan, data]);
|
|
157
|
-
|
|
158
|
-
// Notify parent when span selection changes.
|
|
159
|
-
// Use a ref for the callback so the effect only fires when the actual
|
|
160
|
-
// selection values change, not when the callback identity changes due to
|
|
161
|
-
// parent re-renders from polling.
|
|
162
|
-
const onSpanSelectRef = useRef(onSpanSelect);
|
|
163
|
-
onSpanSelectRef.current = onSpanSelect;
|
|
164
|
-
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
if (
|
|
167
|
-
resource &&
|
|
168
|
-
resourceId &&
|
|
169
|
-
['run', 'step', 'hook', 'sleep'].includes(resource)
|
|
170
|
-
) {
|
|
171
|
-
onSpanSelectRef.current({
|
|
172
|
-
resource: resource as 'run' | 'step' | 'hook' | 'sleep',
|
|
173
|
-
resourceId,
|
|
174
|
-
runId,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}, [resource, resourceId, runId]);
|
|
116
|
+
const {
|
|
117
|
+
status,
|
|
118
|
+
resource,
|
|
119
|
+
resourceId,
|
|
120
|
+
displayData,
|
|
121
|
+
detail: matchedSpanDetailData,
|
|
122
|
+
error,
|
|
123
|
+
} = useSelectedSpanDetail(selectedSpan, fetchSpanDetail);
|
|
124
|
+
const loading = status === 'loading';
|
|
178
125
|
|
|
179
126
|
// Check if this sleep is still pending and can be woken up
|
|
180
127
|
const canWakeUp = useMemo(() => {
|
|
@@ -216,17 +163,6 @@ export function EntityDetailPanel({
|
|
|
216
163
|
resolvedHookIds,
|
|
217
164
|
]);
|
|
218
165
|
|
|
219
|
-
const error = spanDetailError ?? undefined;
|
|
220
|
-
const loading = spanDetailLoading ?? false;
|
|
221
|
-
|
|
222
|
-
const matchedSpanDetailData = useMemo(
|
|
223
|
-
() =>
|
|
224
|
-
spanDetailMatchesSelection(spanDetailData, resource, resourceId)
|
|
225
|
-
? spanDetailData
|
|
226
|
-
: null,
|
|
227
|
-
[spanDetailData, resource, resourceId]
|
|
228
|
-
);
|
|
229
|
-
|
|
230
166
|
// Get the hook token for resolving (prefer fetched data, then hooks array fallback)
|
|
231
167
|
const hookToken = useMemo(() => {
|
|
232
168
|
if (resource !== 'hook' || !resourceId) return undefined;
|
|
@@ -325,16 +261,6 @@ export function EntityDetailPanel({
|
|
|
325
261
|
[onResolveHook, hookToken, resolvingHook, matchedSpanDetailData, data]
|
|
326
262
|
);
|
|
327
263
|
|
|
328
|
-
const displayData = useMemo(
|
|
329
|
-
() =>
|
|
330
|
-
mergeSpanDetail(data, matchedSpanDetailData) as
|
|
331
|
-
| WorkflowRun
|
|
332
|
-
| Step
|
|
333
|
-
| Hook
|
|
334
|
-
| Event,
|
|
335
|
-
[data, matchedSpanDetailData]
|
|
336
|
-
);
|
|
337
|
-
|
|
338
264
|
const moduleSpecifier = useMemo(() => {
|
|
339
265
|
const displayRecord = displayData as Record<string, unknown>;
|
|
340
266
|
const displayStepName = displayRecord.stepName;
|
|
@@ -451,6 +377,9 @@ export function EntityDetailPanel({
|
|
|
451
377
|
onStreamClick={onStreamClick}
|
|
452
378
|
onRunClick={onRunClick}
|
|
453
379
|
encryptionKey={encryptionKey}
|
|
380
|
+
showSeparateEventOccurrenceTimestamps={
|
|
381
|
+
showSeparateEventOccurrenceTimestamps
|
|
382
|
+
}
|
|
454
383
|
/>
|
|
455
384
|
)}
|
|
456
385
|
</div>
|
|
@@ -7,6 +7,7 @@ import { RunClickContext, StreamClickContext } from '../ui/data-inspector';
|
|
|
7
7
|
import { ErrorCard } from '../ui/error-card';
|
|
8
8
|
import { ErrorStackBlock, isStructuredError } from '../ui/error-stack-block';
|
|
9
9
|
import { Skeleton } from '../ui/skeleton';
|
|
10
|
+
import { TimestampTooltip } from '../ui/timestamp-tooltip';
|
|
10
11
|
import { AttrSetEventBlock } from './attributes-block';
|
|
11
12
|
import { CopyableDataBlock, EncryptedDataBlock } from './copyable-data-block';
|
|
12
13
|
import { DetailCard } from './detail-card';
|
|
@@ -34,6 +35,28 @@ const DATA_EVENT_TYPES = new Set([
|
|
|
34
35
|
'attr_set',
|
|
35
36
|
]);
|
|
36
37
|
|
|
38
|
+
const parseDateValue = (value: unknown): Date | null => {
|
|
39
|
+
if (value == null) return null;
|
|
40
|
+
|
|
41
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
42
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getEffectiveEventDate = (
|
|
46
|
+
event: Pick<Event, 'createdAt' | 'occurredAt'>
|
|
47
|
+
): Date => parseDateValue(event.occurredAt) ?? new Date(event.createdAt);
|
|
48
|
+
|
|
49
|
+
const formatEventTimestamp = (date: Date): string =>
|
|
50
|
+
date.toLocaleString(undefined, {
|
|
51
|
+
month: 'short',
|
|
52
|
+
day: 'numeric',
|
|
53
|
+
hour: 'numeric',
|
|
54
|
+
minute: 'numeric',
|
|
55
|
+
second: 'numeric',
|
|
56
|
+
fractionalSecondDigits: 3,
|
|
57
|
+
timeZoneName: 'short',
|
|
58
|
+
});
|
|
59
|
+
|
|
37
60
|
/**
|
|
38
61
|
* A single event row that can lazy-load its eventData when expanded.
|
|
39
62
|
*/
|
|
@@ -41,6 +64,7 @@ function EventItem({
|
|
|
41
64
|
event,
|
|
42
65
|
onLoadEventData,
|
|
43
66
|
encryptionKey,
|
|
67
|
+
showSeparateEventOccurrenceTimestamps = false,
|
|
44
68
|
}: {
|
|
45
69
|
event: Event;
|
|
46
70
|
onLoadEventData?: (
|
|
@@ -49,6 +73,8 @@ function EventItem({
|
|
|
49
73
|
) => Promise<unknown | null>;
|
|
50
74
|
/** When this changes (e.g., Decrypt was clicked), invalidate cached data */
|
|
51
75
|
encryptionKey?: Uint8Array;
|
|
76
|
+
/** Show occurredAt separately instead of folding it into the Created timestamp. */
|
|
77
|
+
showSeparateEventOccurrenceTimestamps?: boolean;
|
|
52
78
|
}) {
|
|
53
79
|
const [loadedData, setLoadedData] = useState<unknown | null>(null);
|
|
54
80
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -101,11 +127,18 @@ function EventItem({
|
|
|
101
127
|
}, [encryptionKey, loadEventData]);
|
|
102
128
|
|
|
103
129
|
const createdAt = new Date(event.createdAt);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
130
|
+
const occurredAt = parseDateValue(event.occurredAt);
|
|
131
|
+
const displayedCreatedAt = showSeparateEventOccurrenceTimestamps
|
|
132
|
+
? createdAt
|
|
133
|
+
: getEffectiveEventDate(event);
|
|
134
|
+
const displayedCreatedAtTime = displayedCreatedAt.toLocaleTimeString(
|
|
135
|
+
undefined,
|
|
136
|
+
{
|
|
137
|
+
hour: 'numeric',
|
|
138
|
+
minute: 'numeric',
|
|
139
|
+
second: 'numeric',
|
|
140
|
+
}
|
|
141
|
+
);
|
|
109
142
|
|
|
110
143
|
const displayPayload = isLoading ? loadedData : mergedDisplay;
|
|
111
144
|
|
|
@@ -119,7 +152,7 @@ function EventItem({
|
|
|
119
152
|
{event.eventType}
|
|
120
153
|
</span>
|
|
121
154
|
<span className="shrink-0 text-label-13 text-gray-900">
|
|
122
|
-
{
|
|
155
|
+
{displayedCreatedAtTime}
|
|
123
156
|
</span>
|
|
124
157
|
</div>
|
|
125
158
|
}
|
|
@@ -133,6 +166,16 @@ function EventItem({
|
|
|
133
166
|
>
|
|
134
167
|
{/* Event attributes */}
|
|
135
168
|
<div className="flex flex-col bg-background-200 [&:has(+_*)]:border-b [&:has(+_*)]:border-gray-alpha-400">
|
|
169
|
+
{showSeparateEventOccurrenceTimestamps && occurredAt && (
|
|
170
|
+
<div className="flex items-center justify-between gap-2 py-2 px-3">
|
|
171
|
+
<span className="text-label-12 text-gray-900">Occurred</span>
|
|
172
|
+
<TimestampTooltip date={occurredAt}>
|
|
173
|
+
<span className="max-w-[70%] truncate text-right text-label-12 font-mono">
|
|
174
|
+
{formatEventTimestamp(occurredAt)}
|
|
175
|
+
</span>
|
|
176
|
+
</TimestampTooltip>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
136
179
|
<div className="flex items-center justify-between gap-2 py-2 px-3">
|
|
137
180
|
<span className="text-label-12 text-gray-900">Event ID</span>
|
|
138
181
|
<span className="max-w-[70%] truncate text-right text-label-12 font-mono">
|
|
@@ -268,6 +311,7 @@ export function EventsList({
|
|
|
268
311
|
onStreamClick,
|
|
269
312
|
onRunClick,
|
|
270
313
|
encryptionKey,
|
|
314
|
+
showSeparateEventOccurrenceTimestamps = false,
|
|
271
315
|
}: {
|
|
272
316
|
events: Event[];
|
|
273
317
|
isLoading?: boolean;
|
|
@@ -280,13 +324,16 @@ export function EventsList({
|
|
|
280
324
|
onRunClick?: (runId: string) => void;
|
|
281
325
|
/** When provided, signals that decryption is active (triggers re-load of expanded events) */
|
|
282
326
|
encryptionKey?: Uint8Array;
|
|
327
|
+
/** Show occurredAt separately instead of folding it into the Created timestamp. */
|
|
328
|
+
showSeparateEventOccurrenceTimestamps?: boolean;
|
|
283
329
|
}) {
|
|
284
|
-
// Sort by
|
|
330
|
+
// Sort by the timestamp shown as Created by default.
|
|
285
331
|
const sortedEvents = useMemo(
|
|
286
332
|
() =>
|
|
287
333
|
[...events].sort(
|
|
288
334
|
(a, b) =>
|
|
289
|
-
|
|
335
|
+
getEffectiveEventDate(a).getTime() -
|
|
336
|
+
getEffectiveEventDate(b).getTime()
|
|
290
337
|
),
|
|
291
338
|
[events]
|
|
292
339
|
);
|
|
@@ -321,6 +368,9 @@ export function EventsList({
|
|
|
321
368
|
event={event}
|
|
322
369
|
onLoadEventData={onLoadEventData}
|
|
323
370
|
encryptionKey={encryptionKey}
|
|
371
|
+
showSeparateEventOccurrenceTimestamps={
|
|
372
|
+
showSeparateEventOccurrenceTimestamps
|
|
373
|
+
}
|
|
324
374
|
/>
|
|
325
375
|
))}
|
|
326
376
|
</div>
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import type {
|
|
3
|
+
import type { Event, Hook, WorkflowRun } from '@workflow/world';
|
|
4
|
+
import { createContext, type ReactNode, useContext } from 'react';
|
|
5
|
+
import type { FetchSpanDetail } from './use-selected-span-detail';
|
|
6
6
|
|
|
7
7
|
export interface SidebarDataContextValue {
|
|
8
8
|
run: WorkflowRun;
|
|
9
9
|
events: Event[];
|
|
10
|
-
|
|
11
|
-
spanDetailError?: Error | null;
|
|
12
|
-
spanDetailLoading?: boolean;
|
|
13
|
-
onSpanSelect: (info: SpanSelectionInfo) => void;
|
|
10
|
+
fetchSpanDetail: FetchSpanDetail;
|
|
14
11
|
onStreamClick?: (streamId: string) => void;
|
|
15
12
|
onRunClick?: (runId: string) => void;
|
|
16
13
|
onWakeUpSleep?: (
|
|
@@ -30,6 +27,8 @@ export interface SidebarDataContextValue {
|
|
|
30
27
|
onDecrypt?: () => void;
|
|
31
28
|
isDecrypting?: boolean;
|
|
32
29
|
hasEncryptedData?: boolean;
|
|
30
|
+
/** Show occurredAt separately instead of folding it into the Created timestamp. */
|
|
31
|
+
showSeparateEventOccurrenceTimestamps?: boolean;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
const SidebarDataContext = createContext<SidebarDataContextValue | null>(null);
|
|
@@ -3,6 +3,75 @@ const hasField = (
|
|
|
3
3
|
key: string
|
|
4
4
|
): value is Record<string, unknown> => key in value;
|
|
5
5
|
|
|
6
|
+
const RESOURCES_WITH_FETCHED_DETAIL = new Set(['run', 'step', 'sleep']);
|
|
7
|
+
|
|
8
|
+
export function resourceNeedsFetchedDetail(
|
|
9
|
+
resource: string | undefined
|
|
10
|
+
): boolean {
|
|
11
|
+
return resource !== undefined && RESOURCES_WITH_FETCHED_DETAIL.has(resource);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SpanDetailStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
15
|
+
|
|
16
|
+
export interface SpanDetailView {
|
|
17
|
+
status: SpanDetailStatus;
|
|
18
|
+
displayData: Record<string, unknown>;
|
|
19
|
+
detail: unknown;
|
|
20
|
+
error: Error | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function deriveSpanDetailView(args: {
|
|
24
|
+
resource: string | undefined;
|
|
25
|
+
resourceId: string | undefined;
|
|
26
|
+
inlineData: unknown;
|
|
27
|
+
fetchedDetail: unknown;
|
|
28
|
+
fetchedError: Error | null;
|
|
29
|
+
}): SpanDetailView {
|
|
30
|
+
const { resource, resourceId, inlineData, fetchedDetail, fetchedError } =
|
|
31
|
+
args;
|
|
32
|
+
|
|
33
|
+
const matchedDetail = spanDetailMatchesSelection(
|
|
34
|
+
fetchedDetail,
|
|
35
|
+
resource,
|
|
36
|
+
resourceId
|
|
37
|
+
)
|
|
38
|
+
? fetchedDetail
|
|
39
|
+
: null;
|
|
40
|
+
const displayData = mergeSpanDetail(inlineData, matchedDetail) as Record<
|
|
41
|
+
string,
|
|
42
|
+
unknown
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
if (!resource || !resourceId) {
|
|
46
|
+
return { status: 'idle', displayData, detail: null, error: undefined };
|
|
47
|
+
}
|
|
48
|
+
if (fetchedError) {
|
|
49
|
+
return {
|
|
50
|
+
status: 'error',
|
|
51
|
+
displayData,
|
|
52
|
+
detail: matchedDetail,
|
|
53
|
+
error: fetchedError,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!resourceNeedsFetchedDetail(resource)) {
|
|
57
|
+
return {
|
|
58
|
+
status: 'ready',
|
|
59
|
+
displayData,
|
|
60
|
+
detail: matchedDetail,
|
|
61
|
+
error: undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (matchedDetail !== null) {
|
|
65
|
+
return {
|
|
66
|
+
status: 'ready',
|
|
67
|
+
displayData,
|
|
68
|
+
detail: matchedDetail,
|
|
69
|
+
error: undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { status: 'loading', displayData, detail: null, error: undefined };
|
|
73
|
+
}
|
|
74
|
+
|
|
6
75
|
/**
|
|
7
76
|
* Returns true when the fetched `detail` belongs to the current selection.
|
|
8
77
|
* The fetch lags selection, so it can briefly hold a previously selected span
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import type {
|
|
6
|
+
SelectedSpanInfo,
|
|
7
|
+
SpanSelectionInfo,
|
|
8
|
+
} from './entity-detail-panel';
|
|
9
|
+
import {
|
|
10
|
+
deriveSpanDetailView,
|
|
11
|
+
resourceNeedsFetchedDetail,
|
|
12
|
+
type SpanDetailStatus,
|
|
13
|
+
} from './span-detail-merge';
|
|
14
|
+
|
|
15
|
+
export type DetailResource = WorkflowRun | Step | Hook | Event;
|
|
16
|
+
|
|
17
|
+
export type FetchSpanDetail = (
|
|
18
|
+
selection: SpanSelectionInfo
|
|
19
|
+
) => Promise<DetailResource>;
|
|
20
|
+
|
|
21
|
+
function deriveSpanSelection(
|
|
22
|
+
selectedSpan: SelectedSpanInfo | null
|
|
23
|
+
): SpanSelectionInfo | null {
|
|
24
|
+
if (!selectedSpan) return null;
|
|
25
|
+
const { resource, data } = selectedSpan;
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
resource === 'step' &&
|
|
29
|
+
data &&
|
|
30
|
+
typeof data === 'object' &&
|
|
31
|
+
'stepId' in data
|
|
32
|
+
) {
|
|
33
|
+
const step = data as Step;
|
|
34
|
+
return { resource: 'step', resourceId: step.stepId, runId: step.runId };
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
resource === 'run' &&
|
|
38
|
+
data &&
|
|
39
|
+
typeof data === 'object' &&
|
|
40
|
+
'runId' in data
|
|
41
|
+
) {
|
|
42
|
+
return { resource: 'run', resourceId: (data as WorkflowRun).runId };
|
|
43
|
+
}
|
|
44
|
+
if (
|
|
45
|
+
resource === 'hook' &&
|
|
46
|
+
data &&
|
|
47
|
+
typeof data === 'object' &&
|
|
48
|
+
'hookId' in data
|
|
49
|
+
) {
|
|
50
|
+
return { resource: 'hook', resourceId: (data as Hook).hookId };
|
|
51
|
+
}
|
|
52
|
+
if (resource === 'sleep') {
|
|
53
|
+
if (!selectedSpan.spanId) return null;
|
|
54
|
+
const waitData = data as { runId?: string } | undefined;
|
|
55
|
+
return {
|
|
56
|
+
resource: 'sleep',
|
|
57
|
+
resourceId: selectedSpan.spanId,
|
|
58
|
+
runId: waitData?.runId,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SelectedSpanDetailResult {
|
|
65
|
+
status: SpanDetailStatus;
|
|
66
|
+
resource: SpanSelectionInfo['resource'] | undefined;
|
|
67
|
+
resourceId: string | undefined;
|
|
68
|
+
runId: string | undefined;
|
|
69
|
+
displayData: Record<string, unknown>;
|
|
70
|
+
detail: DetailResource | null;
|
|
71
|
+
error: Error | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useSelectedSpanDetail(
|
|
75
|
+
selectedSpan: SelectedSpanInfo | null,
|
|
76
|
+
fetchSpanDetail: FetchSpanDetail
|
|
77
|
+
): SelectedSpanDetailResult {
|
|
78
|
+
const selection = useMemo(
|
|
79
|
+
() => deriveSpanSelection(selectedSpan),
|
|
80
|
+
[selectedSpan]
|
|
81
|
+
);
|
|
82
|
+
const resource = selection?.resource;
|
|
83
|
+
const resourceId = selection?.resourceId;
|
|
84
|
+
const runId = selection?.runId;
|
|
85
|
+
const selectionKey =
|
|
86
|
+
resource && resourceId ? `${resource}:${resourceId}` : null;
|
|
87
|
+
const needsFetch = resourceNeedsFetchedDetail(resource);
|
|
88
|
+
|
|
89
|
+
const [fetched, setFetched] = useState<{
|
|
90
|
+
detail: DetailResource | null;
|
|
91
|
+
error: Error | null;
|
|
92
|
+
errorKey: string | null;
|
|
93
|
+
}>({ detail: null, error: null, errorKey: null });
|
|
94
|
+
|
|
95
|
+
const tokenRef = useRef(0);
|
|
96
|
+
const selectionRef = useRef(selection);
|
|
97
|
+
selectionRef.current = selection;
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!selectionKey || !needsFetch) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const activeSelection = selectionRef.current;
|
|
104
|
+
if (!activeSelection) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const token = ++tokenRef.current;
|
|
108
|
+
fetchSpanDetail(activeSelection)
|
|
109
|
+
.then((detail) => {
|
|
110
|
+
if (tokenRef.current !== token) return;
|
|
111
|
+
setFetched({ detail, error: null, errorKey: null });
|
|
112
|
+
})
|
|
113
|
+
.catch((err: unknown) => {
|
|
114
|
+
if (tokenRef.current !== token) return;
|
|
115
|
+
setFetched({
|
|
116
|
+
detail: null,
|
|
117
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
118
|
+
errorKey: selectionKey,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}, [selectionKey, runId, needsFetch, fetchSpanDetail]);
|
|
122
|
+
|
|
123
|
+
const scopedError = fetched.errorKey === selectionKey ? fetched.error : null;
|
|
124
|
+
|
|
125
|
+
const view = useMemo(
|
|
126
|
+
() =>
|
|
127
|
+
deriveSpanDetailView({
|
|
128
|
+
resource,
|
|
129
|
+
resourceId,
|
|
130
|
+
inlineData: selectedSpan?.data,
|
|
131
|
+
fetchedDetail: fetched.detail,
|
|
132
|
+
fetchedError: scopedError,
|
|
133
|
+
}),
|
|
134
|
+
[resource, resourceId, selectedSpan?.data, fetched.detail, scopedError]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
status: view.status,
|
|
139
|
+
resource,
|
|
140
|
+
resourceId,
|
|
141
|
+
runId,
|
|
142
|
+
displayData: view.displayData,
|
|
143
|
+
detail: view.detail as DetailResource | null,
|
|
144
|
+
error: view.error,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
|
-
import { cn } from '../../lib/
|
|
4
|
+
import { cn } from '../../lib/cn';
|
|
5
5
|
|
|
6
6
|
const alertVariants = cva(
|
|
7
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',
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { createPortal } from 'react-dom';
|
|
15
15
|
import useMeasure from 'react-use-measure';
|
|
16
16
|
import { useReducedMotion } from '../../hooks/use-reduced-motion';
|
|
17
|
-
import { cn } from '../../lib/
|
|
17
|
+
import { cn } from '../../lib/cn';
|
|
18
18
|
|
|
19
19
|
const INACTIVE_TIMEOUT_MS = 250;
|
|
20
20
|
const ENTER_DELAY_MS = 0;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
2
|
import type { ComponentProps, ReactNode } from 'react';
|
|
3
|
-
import { cn } from '../../lib/
|
|
3
|
+
import { cn } from '../../lib/cn';
|
|
4
4
|
|
|
5
5
|
const kbdVariants = cva(
|
|
6
6
|
'inline-flex items-center justify-center rounded px-1 text-center font-sans',
|