@workflow/web-shared 5.0.0-beta.24 → 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 -4
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +74 -53
- 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 +0 -3
- package/dist/lib/event-materialization.d.ts.map +1 -1
- package/dist/lib/event-materialization.js +28 -27
- 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 +113 -89
- package/src/lib/cn.ts +93 -0
- package/src/lib/event-materialization.ts +56 -76
- package/src/lib/trace-builder.ts +2 -1
- package/src/lib/utils.ts +0 -6
|
@@ -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',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
|
|
4
|
-
import type { Event, Hook,
|
|
4
|
+
import type { Event, Hook, WorkflowRun } from '@workflow/world';
|
|
5
5
|
import {
|
|
6
6
|
ChevronDown,
|
|
7
7
|
ChevronUp,
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type SpanSelectionInfo,
|
|
30
30
|
} from './sidebar/entity-detail-panel';
|
|
31
31
|
import { ResolveHookModal } from './sidebar/resolve-hook-modal';
|
|
32
|
+
import type { FetchSpanDetail } from './sidebar/use-selected-span-detail';
|
|
32
33
|
import {
|
|
33
34
|
TraceViewerContextProvider,
|
|
34
35
|
TraceViewerTimeline,
|
|
@@ -764,15 +765,12 @@ export const WorkflowTraceViewer = ({
|
|
|
764
765
|
events,
|
|
765
766
|
isLoading,
|
|
766
767
|
error,
|
|
767
|
-
|
|
768
|
-
spanDetailLoading,
|
|
769
|
-
spanDetailError,
|
|
768
|
+
fetchSpanDetail,
|
|
770
769
|
onWakeUpSleep,
|
|
771
770
|
onResolveHook,
|
|
772
771
|
onCancelRun,
|
|
773
772
|
onStreamClick,
|
|
774
773
|
onRunClick,
|
|
775
|
-
onSpanSelect,
|
|
776
774
|
onLoadEventData,
|
|
777
775
|
onLoadMoreSpans,
|
|
778
776
|
hasMoreSpans = false,
|
|
@@ -780,14 +778,13 @@ export const WorkflowTraceViewer = ({
|
|
|
780
778
|
encryptionKey,
|
|
781
779
|
onDecrypt,
|
|
782
780
|
isDecrypting = false,
|
|
781
|
+
showSeparateEventOccurrenceTimestamps = false,
|
|
783
782
|
}: {
|
|
784
783
|
run: WorkflowRun;
|
|
785
784
|
events: Event[];
|
|
786
785
|
isLoading?: boolean;
|
|
787
786
|
error?: Error | null;
|
|
788
|
-
|
|
789
|
-
spanDetailLoading?: boolean;
|
|
790
|
-
spanDetailError?: Error | null;
|
|
787
|
+
fetchSpanDetail: FetchSpanDetail;
|
|
791
788
|
onWakeUpSleep?: (
|
|
792
789
|
runId: string,
|
|
793
790
|
correlationId: string
|
|
@@ -803,8 +800,6 @@ export const WorkflowTraceViewer = ({
|
|
|
803
800
|
onStreamClick?: (streamId: string) => void;
|
|
804
801
|
/** Callback when a run reference is clicked in the detail panel */
|
|
805
802
|
onRunClick?: (runId: string) => void;
|
|
806
|
-
/** Callback when a span is selected. */
|
|
807
|
-
onSpanSelect?: (info: SpanSelectionInfo) => void;
|
|
808
803
|
/** Callback to load event data for a specific event (lazy loading in sidebar) */
|
|
809
804
|
onLoadEventData?: (
|
|
810
805
|
correlationId: string,
|
|
@@ -822,6 +817,8 @@ export const WorkflowTraceViewer = ({
|
|
|
822
817
|
onDecrypt?: () => void;
|
|
823
818
|
/** Whether the encryption key is currently being fetched */
|
|
824
819
|
isDecrypting?: boolean;
|
|
820
|
+
/** Show occurredAt separately instead of folding it into the Created timestamp. */
|
|
821
|
+
showSeparateEventOccurrenceTimestamps?: boolean;
|
|
825
822
|
}) => {
|
|
826
823
|
const toast = useToast();
|
|
827
824
|
const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
|
|
@@ -856,13 +853,6 @@ export const WorkflowTraceViewer = ({
|
|
|
856
853
|
}
|
|
857
854
|
}, [error, isLoading]);
|
|
858
855
|
|
|
859
|
-
const handleSpanSelect = useCallback(
|
|
860
|
-
(info: SpanSelectionInfo) => {
|
|
861
|
-
onSpanSelect?.(info);
|
|
862
|
-
},
|
|
863
|
-
[onSpanSelect]
|
|
864
|
-
);
|
|
865
|
-
|
|
866
856
|
const handleSelectionChange = useCallback(
|
|
867
857
|
(info: SelectedSpanInfo | null) => {
|
|
868
858
|
if (info) {
|
|
@@ -1158,10 +1148,7 @@ export const WorkflowTraceViewer = ({
|
|
|
1158
1148
|
run={run}
|
|
1159
1149
|
onStreamClick={onStreamClick}
|
|
1160
1150
|
onRunClick={onRunClick}
|
|
1161
|
-
|
|
1162
|
-
spanDetailError={spanDetailError}
|
|
1163
|
-
spanDetailLoading={spanDetailLoading}
|
|
1164
|
-
onSpanSelect={handleSpanSelect}
|
|
1151
|
+
fetchSpanDetail={fetchSpanDetail}
|
|
1165
1152
|
onWakeUpSleep={onWakeUpSleep}
|
|
1166
1153
|
onLoadEventData={onLoadEventData}
|
|
1167
1154
|
onResolveHook={onResolveHook}
|
|
@@ -1169,6 +1156,9 @@ export const WorkflowTraceViewer = ({
|
|
|
1169
1156
|
onDecrypt={onDecrypt}
|
|
1170
1157
|
isDecrypting={isDecrypting}
|
|
1171
1158
|
selectedSpan={selectedSpan}
|
|
1159
|
+
showSeparateEventOccurrenceTimestamps={
|
|
1160
|
+
showSeparateEventOccurrenceTimestamps
|
|
1161
|
+
}
|
|
1172
1162
|
/>
|
|
1173
1163
|
</ErrorBoundary>
|
|
1174
1164
|
</div>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Event, WorkflowRun } from '@workflow/world';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { runToSpan } from './trace-span-construction';
|
|
4
|
+
import { otelTimeToMs } from './trace-time-utils';
|
|
5
|
+
|
|
6
|
+
const date = (ms: number) => new Date(ms);
|
|
7
|
+
|
|
8
|
+
function run(overrides: Partial<WorkflowRun> = {}): WorkflowRun {
|
|
9
|
+
return {
|
|
10
|
+
runId: 'run_1',
|
|
11
|
+
status: 'completed',
|
|
12
|
+
deploymentId: 'dpl_1',
|
|
13
|
+
workflowName: 'workflow//./workflow//testWorkflow',
|
|
14
|
+
specVersion: 2,
|
|
15
|
+
attributes: {},
|
|
16
|
+
createdAt: date(1_000),
|
|
17
|
+
updatedAt: date(6_000),
|
|
18
|
+
startedAt: date(2_000),
|
|
19
|
+
completedAt: date(6_000),
|
|
20
|
+
output: null,
|
|
21
|
+
...overrides,
|
|
22
|
+
} as WorkflowRun;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function event(overrides: Partial<Event>): Event {
|
|
26
|
+
return {
|
|
27
|
+
runId: 'run_1',
|
|
28
|
+
eventId: `evt_${overrides.eventType ?? 'unknown'}`,
|
|
29
|
+
eventType: 'run_created',
|
|
30
|
+
createdAt: date(1_000),
|
|
31
|
+
specVersion: 2,
|
|
32
|
+
eventData: {
|
|
33
|
+
deploymentId: 'dpl_1',
|
|
34
|
+
workflowName: 'workflow//./workflow//testWorkflow',
|
|
35
|
+
input: null,
|
|
36
|
+
},
|
|
37
|
+
...overrides,
|
|
38
|
+
} as Event;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('runToSpan', () => {
|
|
42
|
+
it('prefers occurredAt for run span timing when available', () => {
|
|
43
|
+
const span = runToSpan(
|
|
44
|
+
run(),
|
|
45
|
+
[
|
|
46
|
+
event({
|
|
47
|
+
eventType: 'run_created',
|
|
48
|
+
createdAt: date(1_000),
|
|
49
|
+
occurredAt: date(100),
|
|
50
|
+
}),
|
|
51
|
+
event({
|
|
52
|
+
eventType: 'run_started',
|
|
53
|
+
createdAt: date(2_000),
|
|
54
|
+
occurredAt: date(200),
|
|
55
|
+
}),
|
|
56
|
+
event({
|
|
57
|
+
eventType: 'run_completed',
|
|
58
|
+
createdAt: date(6_000),
|
|
59
|
+
occurredAt: date(500),
|
|
60
|
+
}),
|
|
61
|
+
],
|
|
62
|
+
date(10_000)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(otelTimeToMs(span.startTime)).toBe(100);
|
|
66
|
+
expect(
|
|
67
|
+
span.activeStartTime ? otelTimeToMs(span.activeStartTime) : undefined
|
|
68
|
+
).toBe(200);
|
|
69
|
+
expect(otelTimeToMs(span.endTime)).toBe(500);
|
|
70
|
+
expect(
|
|
71
|
+
span.events.find((e) => e.name === 'run_created')?.timestamp
|
|
72
|
+
).toEqual([0, 100_000_000]);
|
|
73
|
+
|
|
74
|
+
const data = span.attributes.data as {
|
|
75
|
+
createdAt: Date;
|
|
76
|
+
startedAt?: Date;
|
|
77
|
+
completedAt?: Date;
|
|
78
|
+
occurredAt?: Date;
|
|
79
|
+
};
|
|
80
|
+
expect(data.createdAt).toEqual(date(100));
|
|
81
|
+
expect(data.startedAt).toEqual(date(200));
|
|
82
|
+
expect(data.completedAt).toEqual(date(500));
|
|
83
|
+
expect(data).not.toHaveProperty('occurredAt');
|
|
84
|
+
});
|
|
85
|
+
});
|