@workflow/web-shared 4.1.0-beta.65 → 4.1.0-beta.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +52 -16
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/hook-actions.d.ts.map +1 -1
- package/dist/components/hook-actions.js +2 -1
- package/dist/components/hook-actions.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts +3 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +55 -38
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
- package/dist/components/sidebar/copyable-data-block.js +2 -1
- package/dist/components/sidebar/copyable-data-block.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +12 -6
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +7 -5
- package/dist/components/sidebar/events-list.js.map +1 -1
- package/dist/components/trace-viewer/components/span-segments.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/span-segments.js +54 -1
- package/dist/components/trace-viewer/components/span-segments.js.map +1 -1
- package/dist/components/ui/error-stack-block.d.ts.map +1 -1
- package/dist/components/ui/error-stack-block.js +2 -1
- package/dist/components/ui/error-stack-block.js.map +1 -1
- package/dist/components/ui/timestamp-tooltip.d.ts +6 -0
- package/dist/components/ui/timestamp-tooltip.d.ts.map +1 -0
- package/dist/components/ui/timestamp-tooltip.js +200 -0
- package/dist/components/ui/timestamp-tooltip.js.map +1 -0
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +8 -1
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +10 -7
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/toast.d.ts +25 -0
- package/dist/lib/toast.d.ts.map +1 -0
- package/dist/lib/toast.js +24 -0
- package/dist/lib/toast.js.map +1 -0
- package/package.json +6 -4
- package/src/components/event-list-view.tsx +125 -35
- package/src/components/hook-actions.tsx +2 -1
- package/src/components/sidebar/attribute-panel.tsx +60 -33
- package/src/components/sidebar/copyable-data-block.tsx +2 -1
- package/src/components/sidebar/entity-detail-panel.tsx +13 -5
- package/src/components/sidebar/events-list.tsx +14 -13
- package/src/components/trace-viewer/components/span-segments.ts +70 -1
- package/src/components/ui/error-stack-block.tsx +2 -1
- package/src/components/ui/timestamp-tooltip.tsx +326 -0
- package/src/components/workflow-trace-view.tsx +8 -1
- package/src/components/workflow-traces/trace-span-construction.ts +12 -7
- package/src/index.ts +2 -0
- package/src/lib/toast.tsx +42 -0
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { LoadMoreButton } from './ui/load-more-button';
|
|
18
18
|
import { MenuDropdown } from './ui/menu-dropdown';
|
|
19
19
|
import { Skeleton } from './ui/skeleton';
|
|
20
|
+
import { TimestampTooltip } from './ui/timestamp-tooltip';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Event types whose eventData contains an error field with a StructuredError.
|
|
@@ -211,6 +212,15 @@ function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
|
|
|
211
212
|
return durations;
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
/** Check if a loaded eventData object contains any encrypted marker values. */
|
|
216
|
+
function hasEncryptedValues(data: unknown): boolean {
|
|
217
|
+
if (!data || typeof data !== 'object') return false;
|
|
218
|
+
for (const val of Object.values(data as Record<string, unknown>)) {
|
|
219
|
+
if (isEncryptedMarker(val)) return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
214
224
|
function isRunLevel(eventType: string): boolean {
|
|
215
225
|
return (
|
|
216
226
|
eventType === 'run_created' ||
|
|
@@ -602,21 +612,67 @@ const SORT_OPTIONS = [
|
|
|
602
612
|
function RowsSkeleton() {
|
|
603
613
|
return (
|
|
604
614
|
<div className="flex-1 overflow-hidden">
|
|
605
|
-
{Array.from({ length:
|
|
606
|
-
<div
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
615
|
+
{Array.from({ length: 16 }, (_, i) => (
|
|
616
|
+
<div key={i} className="flex items-center gap-0" style={{ height: 40 }}>
|
|
617
|
+
{/* Gutter area */}
|
|
618
|
+
<div
|
|
619
|
+
className="relative flex-shrink-0 self-stretch flex items-center"
|
|
620
|
+
style={{ width: GUTTER_WIDTH }}
|
|
621
|
+
>
|
|
622
|
+
{/* Vertical line skeleton */}
|
|
623
|
+
<div
|
|
624
|
+
style={{
|
|
625
|
+
position: 'absolute',
|
|
626
|
+
left: 8,
|
|
627
|
+
top: i === 0 ? '50%' : 0,
|
|
628
|
+
bottom: 0,
|
|
629
|
+
width: 2,
|
|
630
|
+
}}
|
|
631
|
+
>
|
|
632
|
+
<Skeleton className="w-full h-full" style={{ borderRadius: 1 }} />
|
|
633
|
+
</div>
|
|
634
|
+
{/* Dot skeleton */}
|
|
635
|
+
<Skeleton
|
|
636
|
+
className="flex-shrink-0"
|
|
637
|
+
style={{
|
|
638
|
+
width: i % 4 === 0 ? 8 : 6,
|
|
639
|
+
height: i % 4 === 0 ? 8 : 6,
|
|
640
|
+
borderRadius: '50%',
|
|
641
|
+
marginLeft: i % 4 === 0 ? 5 : 6,
|
|
642
|
+
}}
|
|
643
|
+
/>
|
|
644
|
+
</div>
|
|
645
|
+
{/* Chevron placeholder */}
|
|
646
|
+
<div className="w-5 flex-shrink-0 flex items-center justify-center">
|
|
647
|
+
<Skeleton className="w-5 h-5" style={{ borderRadius: 4 }} />
|
|
648
|
+
</div>
|
|
649
|
+
{/* Time */}
|
|
650
|
+
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
|
|
651
|
+
<Skeleton className="h-3" style={{ width: '70%' }} />
|
|
652
|
+
</div>
|
|
653
|
+
{/* Event Type */}
|
|
654
|
+
<div
|
|
655
|
+
className="min-w-0 px-4 flex items-center gap-1.5"
|
|
656
|
+
style={{ flex: '2 1 0%' }}
|
|
657
|
+
>
|
|
658
|
+
<Skeleton
|
|
659
|
+
className="flex-shrink-0"
|
|
660
|
+
style={{ width: 6, height: 6, borderRadius: '50%' }}
|
|
661
|
+
/>
|
|
662
|
+
<Skeleton className="h-3" style={{ width: '60%' }} />
|
|
663
|
+
</div>
|
|
664
|
+
{/* Name */}
|
|
665
|
+
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
|
|
666
|
+
<Skeleton className="h-3" style={{ width: '50%' }} />
|
|
667
|
+
</div>
|
|
668
|
+
{/* Correlation ID */}
|
|
669
|
+
<div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
|
|
670
|
+
<Skeleton className="h-3" style={{ width: '75%' }} />
|
|
671
|
+
</div>
|
|
672
|
+
{/* Event ID */}
|
|
673
|
+
<div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
|
|
674
|
+
<Skeleton className="h-3" style={{ width: '75%' }} />
|
|
675
|
+
</div>
|
|
620
676
|
</div>
|
|
621
677
|
))}
|
|
622
678
|
</div>
|
|
@@ -668,6 +724,7 @@ function EventRow({
|
|
|
668
724
|
cachedEventData,
|
|
669
725
|
onCacheEventData,
|
|
670
726
|
encryptionKey,
|
|
727
|
+
onEncryptedDataDetected,
|
|
671
728
|
}: {
|
|
672
729
|
event: Event;
|
|
673
730
|
index: number;
|
|
@@ -687,6 +744,7 @@ function EventRow({
|
|
|
687
744
|
cachedEventData: unknown | null;
|
|
688
745
|
onCacheEventData: (eventId: string, data: unknown) => void;
|
|
689
746
|
encryptionKey?: Uint8Array;
|
|
747
|
+
onEncryptedDataDetected?: () => void;
|
|
690
748
|
}) {
|
|
691
749
|
const [isLoading, setIsLoading] = useState(false);
|
|
692
750
|
const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
|
|
@@ -697,6 +755,18 @@ function EventRow({
|
|
|
697
755
|
cachedEventData !== null
|
|
698
756
|
);
|
|
699
757
|
|
|
758
|
+
// Notify parent if cached data has encrypted markers on mount
|
|
759
|
+
useEffect(() => {
|
|
760
|
+
if (
|
|
761
|
+
cachedEventData !== null &&
|
|
762
|
+
!encryptionKey &&
|
|
763
|
+
hasEncryptedValues(cachedEventData)
|
|
764
|
+
) {
|
|
765
|
+
onEncryptedDataDetected?.();
|
|
766
|
+
}
|
|
767
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
768
|
+
}, []);
|
|
769
|
+
|
|
700
770
|
const rowGroupKey = isRunLevel(event.eventType)
|
|
701
771
|
? '__run__'
|
|
702
772
|
: (event.correlationId ?? undefined);
|
|
@@ -745,6 +815,9 @@ function EventRow({
|
|
|
745
815
|
if (data !== null && data !== undefined) {
|
|
746
816
|
setLoadedEventData(data);
|
|
747
817
|
onCacheEventData(event.eventId, data);
|
|
818
|
+
if (!encryptionKey && hasEncryptedValues(data)) {
|
|
819
|
+
onEncryptedDataDetected?.();
|
|
820
|
+
}
|
|
748
821
|
}
|
|
749
822
|
} catch (err) {
|
|
750
823
|
setLoadError(
|
|
@@ -760,6 +833,8 @@ function EventRow({
|
|
|
760
833
|
hasExistingEventData,
|
|
761
834
|
onLoadEventData,
|
|
762
835
|
onCacheEventData,
|
|
836
|
+
encryptionKey,
|
|
837
|
+
onEncryptedDataDetected,
|
|
763
838
|
]);
|
|
764
839
|
|
|
765
840
|
// Auto-load event data when remounting in expanded state without cached data
|
|
@@ -877,7 +952,9 @@ function EventRow({
|
|
|
877
952
|
className="tabular-nums min-w-0 px-4"
|
|
878
953
|
style={{ color: 'var(--ds-gray-900)', flex: '2 1 0%' }}
|
|
879
954
|
>
|
|
880
|
-
{
|
|
955
|
+
<TimestampTooltip date={createdAt}>
|
|
956
|
+
<span>{formatEventTime(createdAt)}</span>
|
|
957
|
+
</TimestampTooltip>
|
|
881
958
|
</div>
|
|
882
959
|
|
|
883
960
|
{/* Event Type */}
|
|
@@ -1079,22 +1156,26 @@ export function EventListView({
|
|
|
1079
1156
|
);
|
|
1080
1157
|
}, [events, effectiveSortOrder]);
|
|
1081
1158
|
|
|
1082
|
-
// Detect encrypted fields across all loaded events.
|
|
1083
|
-
|
|
1084
|
-
// the current data model guarantees encrypted markers appear at this level.
|
|
1085
|
-
const hasEncryptedData = useMemo(() => {
|
|
1159
|
+
// Detect encrypted fields across all loaded events (inline eventData).
|
|
1160
|
+
const hasEncryptedInlineData = useMemo(() => {
|
|
1086
1161
|
if (!events) return false;
|
|
1087
1162
|
for (const event of events) {
|
|
1088
1163
|
const ed = (event as Record<string, unknown>).eventData;
|
|
1089
|
-
if (
|
|
1090
|
-
const data = ed as Record<string, unknown>;
|
|
1091
|
-
for (const val of Object.values(data)) {
|
|
1092
|
-
if (isEncryptedMarker(val)) return true;
|
|
1093
|
-
}
|
|
1164
|
+
if (hasEncryptedValues(ed)) return true;
|
|
1094
1165
|
}
|
|
1095
1166
|
return false;
|
|
1096
1167
|
}, [events]);
|
|
1097
1168
|
|
|
1169
|
+
// Tracks whether any expanded row's lazy-loaded data contained encrypted markers.
|
|
1170
|
+
// Set to true by EventRow via onEncryptedDataDetected; never reset (sticky).
|
|
1171
|
+
const [foundEncryptedInLazyData, setFoundEncryptedInLazyData] =
|
|
1172
|
+
useState(false);
|
|
1173
|
+
const handleEncryptedDataDetected = useCallback(() => {
|
|
1174
|
+
setFoundEncryptedInLazyData(true);
|
|
1175
|
+
}, []);
|
|
1176
|
+
|
|
1177
|
+
const hasEncryptedData = hasEncryptedInlineData || foundEncryptedInLazyData;
|
|
1178
|
+
|
|
1098
1179
|
const { correlationNameMap, workflowName } = useMemo(
|
|
1099
1180
|
() => buildNameMaps(events ?? null, run ?? null),
|
|
1100
1181
|
[events, run]
|
|
@@ -1268,18 +1349,26 @@ export function EventListView({
|
|
|
1268
1349
|
</div>
|
|
1269
1350
|
{/* Skeleton header */}
|
|
1270
1351
|
<div
|
|
1271
|
-
className="flex items-center gap-0 h-10 border-b flex-shrink-0
|
|
1352
|
+
className="flex items-center gap-0 h-10 border-b flex-shrink-0"
|
|
1272
1353
|
style={{ borderColor: 'var(--ds-gray-alpha-200)' }}
|
|
1273
1354
|
>
|
|
1274
|
-
<
|
|
1275
|
-
<div
|
|
1276
|
-
<
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
<div style={{ flex: 1 }}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
<
|
|
1355
|
+
<div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
|
|
1356
|
+
<div className="w-5 flex-shrink-0" />
|
|
1357
|
+
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
|
|
1358
|
+
<Skeleton className="h-3" style={{ width: 40 }} />
|
|
1359
|
+
</div>
|
|
1360
|
+
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
|
|
1361
|
+
<Skeleton className="h-3" style={{ width: 72 }} />
|
|
1362
|
+
</div>
|
|
1363
|
+
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
|
|
1364
|
+
<Skeleton className="h-3" style={{ width: 44 }} />
|
|
1365
|
+
</div>
|
|
1366
|
+
<div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
|
|
1367
|
+
<Skeleton className="h-3" style={{ width: 92 }} />
|
|
1368
|
+
</div>
|
|
1369
|
+
<div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
|
|
1370
|
+
<Skeleton className="h-3" style={{ width: 60 }} />
|
|
1371
|
+
</div>
|
|
1283
1372
|
</div>
|
|
1284
1373
|
<RowsSkeleton />
|
|
1285
1374
|
</div>
|
|
@@ -1455,6 +1544,7 @@ export function EventListView({
|
|
|
1455
1544
|
}
|
|
1456
1545
|
onCacheEventData={cacheEventData}
|
|
1457
1546
|
encryptionKey={encryptionKey}
|
|
1547
|
+
onEncryptedDataDetected={handleEncryptedDataDetected}
|
|
1458
1548
|
/>
|
|
1459
1549
|
);
|
|
1460
1550
|
}}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Hook, WorkflowRunStatus } from '@workflow/world';
|
|
4
4
|
import { Send } from 'lucide-react';
|
|
5
5
|
import { useCallback, useState } from 'react';
|
|
6
|
-
import {
|
|
6
|
+
import { useToast } from '../lib/toast';
|
|
7
7
|
import { ResolveHookModal } from './sidebar/resolve-hook-modal';
|
|
8
8
|
|
|
9
9
|
// ============================================================================
|
|
@@ -45,6 +45,7 @@ export function useHookActions({
|
|
|
45
45
|
onResolve,
|
|
46
46
|
callbacks,
|
|
47
47
|
}: UseHookActionsOptions): UseHookActionsReturn {
|
|
48
|
+
const toast = useToast();
|
|
48
49
|
const [isResolving, setIsResolving] = useState(false);
|
|
49
50
|
const [selectedHook, setSelectedHook] = useState<Hook | null>(null);
|
|
50
51
|
|
|
@@ -6,10 +6,11 @@ import type { ModelMessage } from 'ai';
|
|
|
6
6
|
import { Lock } from 'lucide-react';
|
|
7
7
|
import type { KeyboardEvent, ReactNode } from 'react';
|
|
8
8
|
import { useCallback, useMemo, useState } from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import { useToast } from '../../lib/toast';
|
|
10
10
|
import { isEncryptedMarker } from '../../lib/hydration';
|
|
11
11
|
import { extractConversation, isDoStreamStep } from '../../lib/utils';
|
|
12
12
|
import { StreamClickContext } from '../ui/data-inspector';
|
|
13
|
+
import { TimestampTooltip } from '../ui/timestamp-tooltip';
|
|
13
14
|
import { ErrorCard } from '../ui/error-card';
|
|
14
15
|
import {
|
|
15
16
|
ErrorStackBlock,
|
|
@@ -336,6 +337,16 @@ const localMillisecondTimeOrNull = (value: unknown): string | null => {
|
|
|
336
337
|
return formatLocalMillisecondTime(date);
|
|
337
338
|
};
|
|
338
339
|
|
|
340
|
+
const timestampWithTooltipOrNull = (value: unknown): ReactNode | null => {
|
|
341
|
+
const date = parseDateValue(value);
|
|
342
|
+
if (!date) return null;
|
|
343
|
+
return (
|
|
344
|
+
<TimestampTooltip date={date}>
|
|
345
|
+
<span>{formatLocalMillisecondTime(date)}</span>
|
|
346
|
+
</TimestampTooltip>
|
|
347
|
+
);
|
|
348
|
+
};
|
|
349
|
+
|
|
339
350
|
interface DisplayContext {
|
|
340
351
|
stepName?: string;
|
|
341
352
|
}
|
|
@@ -375,15 +386,14 @@ const attributeToDisplayFn: Record<
|
|
|
375
386
|
projectId: (_value: unknown) => null,
|
|
376
387
|
environment: (_value: unknown) => null,
|
|
377
388
|
executionContext: (_value: unknown) => null,
|
|
378
|
-
// Dates
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
resumeAt: localMillisecondTimeOrNull,
|
|
389
|
+
// Dates — wrapped with TimestampTooltip showing UTC/local + relative time
|
|
390
|
+
createdAt: timestampWithTooltipOrNull,
|
|
391
|
+
startedAt: timestampWithTooltipOrNull,
|
|
392
|
+
updatedAt: timestampWithTooltipOrNull,
|
|
393
|
+
completedAt: timestampWithTooltipOrNull,
|
|
394
|
+
expiredAt: timestampWithTooltipOrNull,
|
|
395
|
+
retryAfter: timestampWithTooltipOrNull,
|
|
396
|
+
resumeAt: timestampWithTooltipOrNull,
|
|
387
397
|
// Resolved attributes, won't actually use this function
|
|
388
398
|
metadata: (value: unknown) => {
|
|
389
399
|
if (!hasDisplayContent(value)) return null;
|
|
@@ -580,12 +590,6 @@ export const AttributeBlock = ({
|
|
|
580
590
|
attribute === 'output' ||
|
|
581
591
|
attribute === 'eventData';
|
|
582
592
|
if (isLoading && isExpandableLoadingTarget) {
|
|
583
|
-
const label =
|
|
584
|
-
attribute === 'eventData'
|
|
585
|
-
? 'Event Data'
|
|
586
|
-
: attribute === 'output'
|
|
587
|
-
? 'Output'
|
|
588
|
-
: 'Input';
|
|
589
593
|
return (
|
|
590
594
|
<div
|
|
591
595
|
className={`my-2 flex flex-col ${attribute === 'input' || attribute === 'output' ? 'gap-2 my-3.5' : 'gap-0'}`}
|
|
@@ -596,19 +600,7 @@ export const AttributeBlock = ({
|
|
|
596
600
|
>
|
|
597
601
|
{attribute}
|
|
598
602
|
</span>
|
|
599
|
-
<
|
|
600
|
-
summary={label}
|
|
601
|
-
summaryClassName="text-base py-2"
|
|
602
|
-
disabled
|
|
603
|
-
/>
|
|
604
|
-
<div
|
|
605
|
-
className="overflow-x-auto rounded-md border p-3"
|
|
606
|
-
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
607
|
-
>
|
|
608
|
-
<Skeleton className="h-4 w-[38%]" />
|
|
609
|
-
<Skeleton className="mt-2 h-4 w-[88%]" />
|
|
610
|
-
<Skeleton className="mt-2 h-4 w-[72%]" />
|
|
611
|
-
</div>
|
|
603
|
+
<Skeleton className="h-9 w-full rounded-md" />
|
|
612
604
|
</div>
|
|
613
605
|
);
|
|
614
606
|
}
|
|
@@ -674,6 +666,7 @@ export const AttributePanel = ({
|
|
|
674
666
|
error,
|
|
675
667
|
expiredAt,
|
|
676
668
|
onStreamClick,
|
|
669
|
+
resource,
|
|
677
670
|
}: {
|
|
678
671
|
data: Record<string, unknown>;
|
|
679
672
|
moduleSpecifier?: string;
|
|
@@ -682,7 +675,10 @@ export const AttributePanel = ({
|
|
|
682
675
|
expiredAt?: string | Date;
|
|
683
676
|
/** Callback when a stream reference is clicked */
|
|
684
677
|
onStreamClick?: (streamId: string) => void;
|
|
678
|
+
/** Resource type of the selected span — used to show targeted loading skeletons. */
|
|
679
|
+
resource?: string;
|
|
685
680
|
}) => {
|
|
681
|
+
const toast = useToast();
|
|
686
682
|
// Extract workflowCoreVersion from executionContext for display
|
|
687
683
|
const displayData = useMemo(() => {
|
|
688
684
|
const result = { ...data };
|
|
@@ -705,9 +701,23 @@ export const AttributePanel = ({
|
|
|
705
701
|
const basicAttributes = Object.keys(displayData)
|
|
706
702
|
.filter((key) => !resolvableAttributes.includes(key))
|
|
707
703
|
.sort(sortByAttributeOrder);
|
|
708
|
-
const resolvedAttributes =
|
|
709
|
-
|
|
710
|
-
|
|
704
|
+
const resolvedAttributes = useMemo(() => {
|
|
705
|
+
const present = Object.keys(displayData)
|
|
706
|
+
.filter((key) => resolvableAttributes.includes(key))
|
|
707
|
+
.sort(sortByAttributeOrder);
|
|
708
|
+
|
|
709
|
+
if (!isLoading) return present;
|
|
710
|
+
|
|
711
|
+
// During loading, ensure input/output appear so their skeletons render
|
|
712
|
+
// in the correct position (above the events section).
|
|
713
|
+
const loadingDefaults = ['input', 'output'];
|
|
714
|
+
for (const key of loadingDefaults) {
|
|
715
|
+
if (!present.includes(key)) {
|
|
716
|
+
present.push(key);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return present.sort(sortByAttributeOrder);
|
|
720
|
+
}, [displayData, isLoading]);
|
|
711
721
|
|
|
712
722
|
// Filter out attributes that return null
|
|
713
723
|
const visibleBasicAttributes = basicAttributes.filter((attribute) => {
|
|
@@ -784,7 +794,11 @@ export const AttributePanel = ({
|
|
|
784
794
|
? displayValue
|
|
785
795
|
: String(displayValue ?? displayData.moduleSpecifier ?? '');
|
|
786
796
|
const shouldCapitalizeLabel = attribute !== 'workflowCoreVersion';
|
|
787
|
-
const
|
|
797
|
+
const showResumeAtSkeleton =
|
|
798
|
+
isLoading && resource === 'sleep' && !displayData.resumeAt;
|
|
799
|
+
const showDivider =
|
|
800
|
+
index < orderedBasicAttributes.length - 1 ||
|
|
801
|
+
showResumeAtSkeleton;
|
|
788
802
|
|
|
789
803
|
return (
|
|
790
804
|
<div key={attribute} className="py-1">
|
|
@@ -834,6 +848,19 @@ export const AttributePanel = ({
|
|
|
834
848
|
</div>
|
|
835
849
|
);
|
|
836
850
|
})}
|
|
851
|
+
{isLoading && resource === 'sleep' && !displayData.resumeAt && (
|
|
852
|
+
<div className="py-1">
|
|
853
|
+
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
854
|
+
<span
|
|
855
|
+
className="text-[14px] first-letter:uppercase"
|
|
856
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
857
|
+
>
|
|
858
|
+
resumeAt
|
|
859
|
+
</span>
|
|
860
|
+
<Skeleton className="h-4 w-[55%]" />
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
)}
|
|
837
864
|
</div>
|
|
838
865
|
)}
|
|
839
866
|
{error ? (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Copy } from 'lucide-react';
|
|
4
|
-
import {
|
|
4
|
+
import { useToast } from '../../lib/toast';
|
|
5
5
|
import { DataInspector } from '../ui/data-inspector';
|
|
6
6
|
|
|
7
7
|
const serializeForClipboard = (value: unknown): string => {
|
|
@@ -21,6 +21,7 @@ const serializeForClipboard = (value: unknown): string => {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export function CopyableDataBlock({ data }: { data: unknown }) {
|
|
24
|
+
const toast = useToast();
|
|
24
25
|
return (
|
|
25
26
|
<div
|
|
26
27
|
className="relative overflow-x-auto rounded-md border p-3 pt-9"
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import { Send, Zap } from 'lucide-react';
|
|
6
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
|
-
import {
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
|
+
import { useToast } from '../../lib/toast';
|
|
8
8
|
import { isEncryptedMarker } from '../../lib/hydration';
|
|
9
9
|
import { DecryptButton } from '../ui/decrypt-button';
|
|
10
10
|
import { AttributePanel } from './attribute-panel';
|
|
@@ -104,6 +104,7 @@ export function EntityDetailPanel({
|
|
|
104
104
|
/** Info about the currently selected span from the trace viewer */
|
|
105
105
|
selectedSpan: SelectedSpanInfo | null;
|
|
106
106
|
}): React.JSX.Element | null {
|
|
107
|
+
const toast = useToast();
|
|
107
108
|
const [stoppingSleep, setStoppingSleep] = useState(false);
|
|
108
109
|
const [showResolveHookModal, setShowResolveHookModal] = useState(false);
|
|
109
110
|
const [resolvingHook, setResolvingHook] = useState(false);
|
|
@@ -144,20 +145,26 @@ export function EntityDetailPanel({
|
|
|
144
145
|
return { resource: undefined, resourceId: undefined, runId: undefined };
|
|
145
146
|
}, [selectedSpan, data]);
|
|
146
147
|
|
|
147
|
-
// Notify parent when span selection changes
|
|
148
|
+
// Notify parent when span selection changes.
|
|
149
|
+
// Use a ref for the callback so the effect only fires when the actual
|
|
150
|
+
// selection values change, not when the callback identity changes due to
|
|
151
|
+
// parent re-renders from polling.
|
|
152
|
+
const onSpanSelectRef = useRef(onSpanSelect);
|
|
153
|
+
onSpanSelectRef.current = onSpanSelect;
|
|
154
|
+
|
|
148
155
|
useEffect(() => {
|
|
149
156
|
if (
|
|
150
157
|
resource &&
|
|
151
158
|
resourceId &&
|
|
152
159
|
['run', 'step', 'hook', 'sleep'].includes(resource)
|
|
153
160
|
) {
|
|
154
|
-
|
|
161
|
+
onSpanSelectRef.current({
|
|
155
162
|
resource: resource as 'run' | 'step' | 'hook' | 'sleep',
|
|
156
163
|
resourceId,
|
|
157
164
|
runId,
|
|
158
165
|
});
|
|
159
166
|
}
|
|
160
|
-
}, [resource, resourceId, runId
|
|
167
|
+
}, [resource, resourceId, runId]);
|
|
161
168
|
|
|
162
169
|
// Check if this sleep is still pending and can be woken up
|
|
163
170
|
const canWakeUp = useMemo(() => {
|
|
@@ -474,6 +481,7 @@ export function EntityDetailPanel({
|
|
|
474
481
|
isLoading={loading}
|
|
475
482
|
error={error ?? undefined}
|
|
476
483
|
onStreamClick={onStreamClick}
|
|
484
|
+
resource={resource}
|
|
477
485
|
/>
|
|
478
486
|
</section>
|
|
479
487
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Event } from '@workflow/world';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { ErrorCard } from '../ui/error-card';
|
|
5
6
|
import {
|
|
6
7
|
ErrorStackBlock,
|
|
7
8
|
isStructuredErrorWithStack,
|
|
@@ -23,10 +24,14 @@ const DATA_EVENT_TYPES = new Set([
|
|
|
23
24
|
'step_created',
|
|
24
25
|
'step_completed',
|
|
25
26
|
'step_failed',
|
|
27
|
+
'step_retrying',
|
|
26
28
|
'hook_created',
|
|
27
29
|
'hook_received',
|
|
28
30
|
'run_created',
|
|
29
31
|
'run_completed',
|
|
32
|
+
'run_failed',
|
|
33
|
+
'wait_created',
|
|
34
|
+
'wait_completed',
|
|
30
35
|
]);
|
|
31
36
|
|
|
32
37
|
/**
|
|
@@ -175,15 +180,11 @@ function EventItem({
|
|
|
175
180
|
|
|
176
181
|
{/* Error state */}
|
|
177
182
|
{loadError && (
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}}
|
|
184
|
-
>
|
|
185
|
-
{loadError}
|
|
186
|
-
</div>
|
|
183
|
+
<ErrorCard
|
|
184
|
+
title="Failed to load event data"
|
|
185
|
+
details={loadError}
|
|
186
|
+
className="mt-2"
|
|
187
|
+
/>
|
|
187
188
|
)}
|
|
188
189
|
|
|
189
190
|
{/* Event data */}
|
|
@@ -269,10 +270,10 @@ export function EventsList({
|
|
|
269
270
|
Events {!isLoading && `(${sortedEvents.length})`}
|
|
270
271
|
</h3>
|
|
271
272
|
{isLoading ? (
|
|
272
|
-
<div className="flex flex-col gap-
|
|
273
|
-
<Skeleton className="h-
|
|
274
|
-
<Skeleton className="h-
|
|
275
|
-
<Skeleton className="h-
|
|
273
|
+
<div className="flex flex-col gap-4">
|
|
274
|
+
<Skeleton className="h-9 w-full rounded-md" />
|
|
275
|
+
<Skeleton className="h-9 w-full rounded-md" />
|
|
276
|
+
<Skeleton className="h-9 w-full rounded-md" />
|
|
276
277
|
</div>
|
|
277
278
|
) : null}
|
|
278
279
|
{!isLoading && !error && sortedEvents.length === 0 && (
|
|
@@ -341,6 +341,26 @@ function computeRunSegments(node: SpanNode): Segment[] {
|
|
|
341
341
|
|
|
342
342
|
if (duration <= 0) return segments;
|
|
343
343
|
|
|
344
|
+
// V1 runs (specVersion 1) don't emit run lifecycle events (run_created,
|
|
345
|
+
// run_started, run_completed). Detect this by checking for the absence of
|
|
346
|
+
// run_created — it's always the first event for v2 runs and is always
|
|
347
|
+
// loaded in the first page, so its absence reliably signals a v1 run.
|
|
348
|
+
// For v1, fall back to the run entity's status from span.attributes.data.
|
|
349
|
+
const hasRunCreated = events.some((e) => e.event.name === 'run_created');
|
|
350
|
+
|
|
351
|
+
if (!hasRunCreated) {
|
|
352
|
+
const runData = node.span.attributes?.data as
|
|
353
|
+
| Record<string, unknown>
|
|
354
|
+
| undefined;
|
|
355
|
+
const runStatus = runData?.status as string | undefined;
|
|
356
|
+
return computeV1RunSegments(
|
|
357
|
+
startTime,
|
|
358
|
+
duration,
|
|
359
|
+
activeStartTime,
|
|
360
|
+
runStatus
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
344
364
|
const failedEvent = events.find((e) => e.event.name === 'run_failed');
|
|
345
365
|
const completedEvent = events.find((e) => e.event.name === 'run_completed');
|
|
346
366
|
|
|
@@ -396,7 +416,56 @@ function computeRunSegments(node: SpanNode): Segment[] {
|
|
|
396
416
|
status: 'succeeded',
|
|
397
417
|
});
|
|
398
418
|
} else {
|
|
399
|
-
// Running to completion
|
|
419
|
+
// Running to completion (or terminal events haven't loaded yet)
|
|
420
|
+
segments.push({
|
|
421
|
+
startFraction: cursor,
|
|
422
|
+
endFraction: 1,
|
|
423
|
+
status: 'running',
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return segments;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Compute segments for a v1 run using only the run entity's status and
|
|
432
|
+
* activeStartTime (derived from run.startedAt). V1 runs have no run
|
|
433
|
+
* lifecycle events, so we infer the visual segments from entity fields.
|
|
434
|
+
*/
|
|
435
|
+
function computeV1RunSegments(
|
|
436
|
+
startTime: number,
|
|
437
|
+
duration: number,
|
|
438
|
+
activeStartTime: number | undefined,
|
|
439
|
+
runStatus: string | undefined
|
|
440
|
+
): Segment[] {
|
|
441
|
+
const segments: Segment[] = [];
|
|
442
|
+
|
|
443
|
+
let cursor = 0;
|
|
444
|
+
if (activeStartTime && activeStartTime > startTime) {
|
|
445
|
+
const queuedFraction = timeToFraction(activeStartTime, startTime, duration);
|
|
446
|
+
if (queuedFraction > 0.001) {
|
|
447
|
+
segments.push({
|
|
448
|
+
startFraction: 0,
|
|
449
|
+
endFraction: queuedFraction,
|
|
450
|
+
status: 'queued',
|
|
451
|
+
});
|
|
452
|
+
cursor = queuedFraction;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (runStatus === 'failed') {
|
|
457
|
+
segments.push({
|
|
458
|
+
startFraction: cursor,
|
|
459
|
+
endFraction: 1,
|
|
460
|
+
status: 'failed',
|
|
461
|
+
});
|
|
462
|
+
} else if (runStatus === 'completed' || runStatus === 'cancelled') {
|
|
463
|
+
segments.push({
|
|
464
|
+
startFraction: cursor,
|
|
465
|
+
endFraction: 1,
|
|
466
|
+
status: 'succeeded',
|
|
467
|
+
});
|
|
468
|
+
} else {
|
|
400
469
|
segments.push({
|
|
401
470
|
startFraction: cursor,
|
|
402
471
|
endFraction: 1,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { AlertCircle, Copy } from 'lucide-react';
|
|
4
|
-
import {
|
|
4
|
+
import { useToast } from '../../lib/toast';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Check whether `value` looks like a structured error object with a `stack`
|
|
@@ -28,6 +28,7 @@ export function ErrorStackBlock({
|
|
|
28
28
|
}: {
|
|
29
29
|
value: Record<string, unknown> & { stack: string };
|
|
30
30
|
}) {
|
|
31
|
+
const toast = useToast();
|
|
31
32
|
const stack = value.stack;
|
|
32
33
|
const message = typeof value.message === 'string' ? value.message : undefined;
|
|
33
34
|
const copyText = message ? `${message}\n\n${stack}` : stack;
|