@workflow/web-shared 5.0.0-beta.15 → 5.0.0-beta.16
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 +1 -1
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +20 -3
- package/dist/components/new-trace-viewer/components/timeline.d.ts +1 -1
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +40 -4
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +8 -9
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +26 -32
- package/dist/components/sidebar/attributes-block.d.ts +28 -0
- package/dist/components/sidebar/attributes-block.d.ts.map +1 -0
- package/dist/components/sidebar/attributes-block.js +83 -0
- package/dist/components/sidebar/entity-detail-panel.js +2 -2
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +8 -1
- package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/node.js +2 -1
- package/dist/components/ui/context-card.d.ts +47 -0
- package/dist/components/ui/context-card.d.ts.map +1 -0
- package/dist/components/ui/context-card.js +471 -0
- package/dist/components/ui/timestamp-tooltip.d.ts +34 -1
- package/dist/components/ui/timestamp-tooltip.d.ts.map +1 -1
- package/dist/components/ui/timestamp-tooltip.js +80 -132
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +8 -10
- package/dist/components/workflow-traces/event-colors.d.ts +1 -0
- package/dist/components/workflow-traces/event-colors.d.ts.map +1 -1
- package/dist/components/workflow-traces/event-colors.js +12 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +2 -1
- package/dist/lib/trace-builder.d.ts +15 -0
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +27 -2
- package/dist/styles.css +9 -0
- package/package.json +5 -3
- package/src/components/event-list-view.tsx +25 -2
- package/src/components/new-trace-viewer/components/timeline.tsx +82 -2
- package/src/components/new-trace-viewer/trace-viewer.tsx +17 -14
- package/src/components/sidebar/attribute-panel.tsx +117 -119
- package/src/components/sidebar/attributes-block.tsx +182 -0
- package/src/components/sidebar/entity-detail-panel.tsx +1 -1
- package/src/components/sidebar/events-list.tsx +8 -0
- package/src/components/trace-viewer/components/node.tsx +3 -2
- package/src/components/ui/context-card.tsx +784 -0
- package/src/components/ui/timestamp-tooltip.tsx +132 -194
- package/src/components/workflow-trace-view.tsx +18 -10
- package/src/components/workflow-traces/event-colors.ts +12 -0
- package/src/components/workflow-traces/trace-span-construction.ts +1 -0
- package/src/lib/trace-builder.ts +32 -1
- package/src/styles.css +9 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
|
|
4
4
|
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
5
5
|
import type { ModelMessage } from 'ai';
|
|
6
|
+
import { format } from 'date-fns';
|
|
6
7
|
import type { KeyboardEvent, ReactNode } from 'react';
|
|
7
8
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
|
8
9
|
import { isEncryptedMarker, isExpiredMarker } from '../../lib/hydration';
|
|
@@ -10,6 +11,7 @@ import { useToast } from '../../lib/toast';
|
|
|
10
11
|
import { extractConversation, isDoStreamStep } from '../../lib/utils';
|
|
11
12
|
import { CopyButton } from '../new-trace-viewer/components/copy-button';
|
|
12
13
|
import { MiddleTruncate } from '../new-trace-viewer/components/middle-truncate/middle-truncate';
|
|
14
|
+
import { ContextCardProvider } from '../ui/context-card';
|
|
13
15
|
import {
|
|
14
16
|
DecryptClickContext,
|
|
15
17
|
RunClickContext,
|
|
@@ -19,6 +21,7 @@ import { ErrorCard } from '../ui/error-card';
|
|
|
19
21
|
import { ErrorStackBlock, isStructuredError } from '../ui/error-stack-block';
|
|
20
22
|
import { Skeleton } from '../ui/skeleton';
|
|
21
23
|
import { TimestampTooltip } from '../ui/timestamp-tooltip';
|
|
24
|
+
import { RunAttributesCard } from './attributes-block';
|
|
22
25
|
import { ConversationView } from './conversation-view';
|
|
23
26
|
import { CopyableDataBlock, EncryptedDataBlock } from './copyable-data-block';
|
|
24
27
|
import { DetailCard } from './detail-card';
|
|
@@ -339,15 +342,7 @@ const parseDateValue = (value: unknown): Date | null => {
|
|
|
339
342
|
};
|
|
340
343
|
|
|
341
344
|
const formatLocalMillisecondTime = (date: Date): string =>
|
|
342
|
-
date.
|
|
343
|
-
year: 'numeric',
|
|
344
|
-
month: 'numeric',
|
|
345
|
-
day: 'numeric',
|
|
346
|
-
hour: 'numeric',
|
|
347
|
-
minute: 'numeric',
|
|
348
|
-
second: 'numeric',
|
|
349
|
-
fractionalSecondDigits: 3,
|
|
350
|
-
});
|
|
345
|
+
format(date, 'MMM dd HH:mm:ss.SS OO').toUpperCase();
|
|
351
346
|
|
|
352
347
|
export const localMillisecondTime = (value: unknown): string => {
|
|
353
348
|
const date = parseDateValue(value);
|
|
@@ -355,7 +350,6 @@ export const localMillisecondTime = (value: unknown): string => {
|
|
|
355
350
|
return '-';
|
|
356
351
|
}
|
|
357
352
|
|
|
358
|
-
// e.g. 12/17/2025, 9:08:55.182 AM
|
|
359
353
|
return formatLocalMillisecondTime(date);
|
|
360
354
|
};
|
|
361
355
|
|
|
@@ -416,12 +410,12 @@ const attributeToDisplayFn: Record<
|
|
|
416
410
|
projectId: (_value: unknown) => null,
|
|
417
411
|
environment: (_value: unknown) => null,
|
|
418
412
|
executionContext: (_value: unknown) => null,
|
|
419
|
-
// Attributes
|
|
420
|
-
// Rendered in its own collapsible DetailCard;
|
|
421
|
-
// by the hasDisplayContent gate.
|
|
413
|
+
// Attributes — string-string metadata attached to the run.
|
|
414
|
+
// Rendered as key-value rows in its own collapsible DetailCard;
|
|
415
|
+
// if empty/missing, hidden by the hasDisplayContent gate.
|
|
422
416
|
attributes: (value: unknown) => {
|
|
423
417
|
if (!hasDisplayContent(value)) return null;
|
|
424
|
-
return <
|
|
418
|
+
return <RunAttributesCard attributes={value as Record<string, string>} />;
|
|
425
419
|
},
|
|
426
420
|
// Dates — wrapped with TimestampTooltip showing UTC/local + relative time
|
|
427
421
|
createdAt: timestampWithTooltipOrNull,
|
|
@@ -846,113 +840,117 @@ export const AttributePanel = ({
|
|
|
846
840
|
: outerDecryptCtx;
|
|
847
841
|
|
|
848
842
|
return (
|
|
849
|
-
<
|
|
850
|
-
<
|
|
851
|
-
<
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
{
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
843
|
+
<ContextCardProvider>
|
|
844
|
+
<RunClickContext.Provider value={onRunClick}>
|
|
845
|
+
<StreamClickContext.Provider value={onStreamClick}>
|
|
846
|
+
<DecryptClickContext.Provider value={decryptValue}>
|
|
847
|
+
{visibleBasicAttributes.length > 0 && (
|
|
848
|
+
<div className="flex flex-col overflow-hidden divide-y divide-gray-alpha-400 mb-3">
|
|
849
|
+
{orderedBasicAttributes.map((attribute) => {
|
|
850
|
+
const displayValue = attributeToDisplayFn[
|
|
851
|
+
attribute as keyof typeof attributeToDisplayFn
|
|
852
|
+
]?.(displayData[attribute as keyof typeof displayData]);
|
|
853
|
+
const isModuleSpecifier = attribute === 'moduleSpecifier';
|
|
854
|
+
const isCopyableBasicAttribute =
|
|
855
|
+
copyableBasicAttributes.has(attribute as AttributeKey) &&
|
|
856
|
+
typeof displayValue === 'string';
|
|
857
|
+
const moduleSpecifierValue =
|
|
858
|
+
typeof displayValue === 'string'
|
|
859
|
+
? displayValue
|
|
860
|
+
: String(
|
|
861
|
+
displayValue ?? displayData.moduleSpecifier ?? ''
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
return (
|
|
865
|
+
<div
|
|
866
|
+
className="flex items-center justify-between py-2"
|
|
867
|
+
key={attribute}
|
|
868
|
+
>
|
|
869
|
+
<span className="text-label-14 text-gray-900">
|
|
870
|
+
{getAttributeDisplayName(attribute)}
|
|
871
|
+
</span>
|
|
872
|
+
{isModuleSpecifier ? (
|
|
873
|
+
<button
|
|
874
|
+
type="button"
|
|
875
|
+
className="min-w-0 max-w-[70%] truncate text-right text-label-13 font-mono"
|
|
876
|
+
style={{
|
|
877
|
+
color: 'var(--ds-gray-1000)',
|
|
878
|
+
background: 'transparent',
|
|
879
|
+
border: 'none',
|
|
880
|
+
padding: 0,
|
|
881
|
+
}}
|
|
882
|
+
title={moduleSpecifierValue}
|
|
883
|
+
onClick={() =>
|
|
884
|
+
handleCopyModuleSpecifier(moduleSpecifierValue)
|
|
885
|
+
}
|
|
886
|
+
>
|
|
887
|
+
{moduleSpecifierValue}
|
|
888
|
+
</button>
|
|
889
|
+
) : isCopyableBasicAttribute ? (
|
|
890
|
+
<div
|
|
891
|
+
className="flex min-w-0 max-w-[70%] items-center justify-end gap-1 text-right text-[13px] font-mono"
|
|
892
|
+
style={{
|
|
893
|
+
color: 'var(--ds-gray-1000)',
|
|
894
|
+
}}
|
|
895
|
+
title={displayValue}
|
|
896
|
+
>
|
|
897
|
+
<MiddleTruncate
|
|
898
|
+
value={displayValue}
|
|
899
|
+
className="flex-1"
|
|
900
|
+
/>
|
|
901
|
+
<CopyButton
|
|
902
|
+
copyText={displayValue}
|
|
903
|
+
ariaLabel={`Copy ${getAttributeDisplayName(attribute)}`}
|
|
904
|
+
className="shrink-0 -mr-1"
|
|
905
|
+
/>
|
|
906
|
+
</div>
|
|
907
|
+
) : (
|
|
908
|
+
<span className="text-right text-label-13 font-mono">
|
|
909
|
+
{displayValue}
|
|
910
|
+
</span>
|
|
911
|
+
)}
|
|
912
|
+
</div>
|
|
913
|
+
);
|
|
914
|
+
})}
|
|
915
|
+
{isLoading && resource === 'sleep' && !displayData.resumeAt && (
|
|
916
|
+
<div className="py-1">
|
|
917
|
+
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
918
|
+
<span
|
|
919
|
+
className="text-[14px] first-letter:uppercase"
|
|
920
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
899
921
|
>
|
|
900
|
-
|
|
901
|
-
value={displayValue}
|
|
902
|
-
className="flex-1"
|
|
903
|
-
/>
|
|
904
|
-
<CopyButton
|
|
905
|
-
copyText={displayValue}
|
|
906
|
-
ariaLabel={`Copy ${getAttributeDisplayName(attribute)}`}
|
|
907
|
-
className="shrink-0 -mr-1"
|
|
908
|
-
/>
|
|
909
|
-
</div>
|
|
910
|
-
) : (
|
|
911
|
-
<span className="text-right text-label-13 font-mono">
|
|
912
|
-
{displayValue}
|
|
922
|
+
resumeAt
|
|
913
923
|
</span>
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
);
|
|
917
|
-
})}
|
|
918
|
-
{isLoading && resource === 'sleep' && !displayData.resumeAt && (
|
|
919
|
-
<div className="py-1">
|
|
920
|
-
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
921
|
-
<span
|
|
922
|
-
className="text-[14px] first-letter:uppercase"
|
|
923
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
924
|
-
>
|
|
925
|
-
resumeAt
|
|
926
|
-
</span>
|
|
927
|
-
<Skeleton className="h-4 w-[55%]" />
|
|
924
|
+
<Skeleton className="h-4 w-[55%]" />
|
|
925
|
+
</div>
|
|
928
926
|
</div>
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
</
|
|
955
|
-
</
|
|
956
|
-
</
|
|
927
|
+
)}
|
|
928
|
+
</div>
|
|
929
|
+
)}
|
|
930
|
+
{error ? (
|
|
931
|
+
<ErrorCard
|
|
932
|
+
title="Failed to load resource details"
|
|
933
|
+
details={error.message}
|
|
934
|
+
className="my-4"
|
|
935
|
+
/>
|
|
936
|
+
) : hasExpired ? (
|
|
937
|
+
<ExpiredDataMessage />
|
|
938
|
+
) : resolvedAttributes.length > 0 ? (
|
|
939
|
+
<>
|
|
940
|
+
{resolvedAttributes.map((attribute) => (
|
|
941
|
+
<AttributeBlock
|
|
942
|
+
isLoading={isLoading}
|
|
943
|
+
key={attribute}
|
|
944
|
+
attribute={attribute}
|
|
945
|
+
value={displayData[attribute as keyof typeof displayData]}
|
|
946
|
+
context={displayContext}
|
|
947
|
+
/>
|
|
948
|
+
))}
|
|
949
|
+
</>
|
|
950
|
+
) : null}
|
|
951
|
+
</DecryptClickContext.Provider>
|
|
952
|
+
</StreamClickContext.Provider>
|
|
953
|
+
</RunClickContext.Provider>
|
|
954
|
+
</ContextCardProvider>
|
|
957
955
|
);
|
|
958
956
|
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AttributeChange,
|
|
5
|
+
RESERVED_ATTRIBUTE_KEY_PREFIX,
|
|
6
|
+
} from '@workflow/world';
|
|
7
|
+
import { CopyableDataBlock } from './copyable-data-block';
|
|
8
|
+
import { DetailCard } from './detail-card';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check whether an attribute key is in the reserved (`$`-prefixed)
|
|
12
|
+
* namespace used by framework/library code.
|
|
13
|
+
*/
|
|
14
|
+
export const isReservedAttributeKey = (key: string): boolean =>
|
|
15
|
+
key.startsWith(RESERVED_ATTRIBUTE_KEY_PREFIX);
|
|
16
|
+
|
|
17
|
+
function ReservedBadge() {
|
|
18
|
+
return (
|
|
19
|
+
<span
|
|
20
|
+
className="shrink-0 rounded border px-1 py-px text-[10px] font-medium leading-3"
|
|
21
|
+
style={{
|
|
22
|
+
borderColor: 'var(--ds-gray-alpha-400)',
|
|
23
|
+
backgroundColor: 'var(--ds-gray-100)',
|
|
24
|
+
color: 'var(--ds-gray-700)',
|
|
25
|
+
}}
|
|
26
|
+
title={`Keys prefixed with "${RESERVED_ATTRIBUTE_KEY_PREFIX}" are reserved for framework and library code`}
|
|
27
|
+
>
|
|
28
|
+
Reserved
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function AttributeRow({
|
|
34
|
+
attributeKey,
|
|
35
|
+
value,
|
|
36
|
+
removed = false,
|
|
37
|
+
}: {
|
|
38
|
+
attributeKey: string;
|
|
39
|
+
value?: string;
|
|
40
|
+
removed?: boolean;
|
|
41
|
+
}) {
|
|
42
|
+
const reserved = isReservedAttributeKey(attributeKey);
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex items-center justify-between gap-3 py-1.5">
|
|
45
|
+
<span
|
|
46
|
+
className="flex min-w-0 items-center gap-1.5 text-label-12 font-mono"
|
|
47
|
+
style={{
|
|
48
|
+
color: reserved ? 'var(--ds-gray-700)' : 'var(--ds-gray-900)',
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<span className="truncate">{attributeKey}</span>
|
|
52
|
+
{reserved && <ReservedBadge />}
|
|
53
|
+
</span>
|
|
54
|
+
{removed ? (
|
|
55
|
+
<span
|
|
56
|
+
className="shrink-0 text-label-12 italic"
|
|
57
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
58
|
+
>
|
|
59
|
+
removed
|
|
60
|
+
</span>
|
|
61
|
+
) : (
|
|
62
|
+
<span
|
|
63
|
+
className="max-w-[60%] truncate text-right text-label-12 font-mono text-gray-1000"
|
|
64
|
+
title={value}
|
|
65
|
+
>
|
|
66
|
+
{value}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sort attribute keys for display: user keys first (alphabetical), then
|
|
75
|
+
* reserved `$`-prefixed keys (alphabetical), so framework metadata doesn't
|
|
76
|
+
* crowd out user-set attributes.
|
|
77
|
+
*/
|
|
78
|
+
export function sortAttributeKeys(keys: string[]): string[] {
|
|
79
|
+
return [...keys].sort((a, b) => {
|
|
80
|
+
const aReserved = isReservedAttributeKey(a);
|
|
81
|
+
const bReserved = isReservedAttributeKey(b);
|
|
82
|
+
if (aReserved !== bReserved) return aReserved ? 1 : -1;
|
|
83
|
+
return a.localeCompare(b);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Collapsible card showing a run's materialized attributes as key-value
|
|
89
|
+
* rows. Reserved (`$`-prefixed) keys are visually de-emphasized with a
|
|
90
|
+
* badge and sorted after user keys.
|
|
91
|
+
*/
|
|
92
|
+
export function RunAttributesCard({
|
|
93
|
+
attributes,
|
|
94
|
+
}: {
|
|
95
|
+
attributes: Record<string, string>;
|
|
96
|
+
}) {
|
|
97
|
+
const keys = sortAttributeKeys(Object.keys(attributes));
|
|
98
|
+
if (keys.length === 0) return null;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<DetailCard
|
|
102
|
+
summary={`Attributes (${keys.length})`}
|
|
103
|
+
defaultOpen
|
|
104
|
+
contentClassName="mb-0"
|
|
105
|
+
>
|
|
106
|
+
<div className="flex flex-col divide-y divide-gray-alpha-400">
|
|
107
|
+
{keys.map((key) => (
|
|
108
|
+
<AttributeRow attributeKey={key} key={key} value={attributes[key]} />
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
</DetailCard>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface AttrSetEventData {
|
|
116
|
+
changes: AttributeChange[];
|
|
117
|
+
writer?:
|
|
118
|
+
| { type: 'workflow' }
|
|
119
|
+
| { type: 'step'; stepId: string; attempt: number };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isAttrSetEventData(data: unknown): data is AttrSetEventData {
|
|
123
|
+
if (data === null || typeof data !== 'object') return false;
|
|
124
|
+
const record = data as Record<string, unknown>;
|
|
125
|
+
return (
|
|
126
|
+
Array.isArray(record.changes) &&
|
|
127
|
+
record.changes.every(
|
|
128
|
+
(c) =>
|
|
129
|
+
c !== null &&
|
|
130
|
+
typeof c === 'object' &&
|
|
131
|
+
typeof (c as Record<string, unknown>).key === 'string'
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function describeWriter(writer: AttrSetEventData['writer']): string | null {
|
|
137
|
+
if (!writer) return null;
|
|
138
|
+
if (writer.type === 'workflow') return 'Set by workflow';
|
|
139
|
+
if (writer.type === 'step') {
|
|
140
|
+
return `Set by step ${writer.stepId} (attempt ${writer.attempt})`;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Renders the payload of an `attr_set` event: the list of attribute
|
|
147
|
+
* changes (sets and removals) plus which writer (workflow or step)
|
|
148
|
+
* made them. Falls back to the generic JSON block for unexpected shapes.
|
|
149
|
+
*/
|
|
150
|
+
export function AttrSetEventBlock({ data }: { data: unknown }) {
|
|
151
|
+
if (!isAttrSetEventData(data)) {
|
|
152
|
+
return <CopyableDataBlock data={data} />;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const writerLabel = describeWriter(data.writer);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="flex flex-col px-3 py-1">
|
|
159
|
+
<div className="flex flex-col divide-y divide-gray-alpha-400">
|
|
160
|
+
{data.changes.map((change, index) => (
|
|
161
|
+
<AttributeRow
|
|
162
|
+
attributeKey={change.key}
|
|
163
|
+
key={`${change.key}:${index}`}
|
|
164
|
+
removed={change.value === null}
|
|
165
|
+
value={change.value ?? undefined}
|
|
166
|
+
/>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
{writerLabel && (
|
|
170
|
+
<div
|
|
171
|
+
className="border-t py-1.5 text-label-12"
|
|
172
|
+
style={{
|
|
173
|
+
borderColor: 'var(--ds-gray-alpha-400)',
|
|
174
|
+
color: 'var(--ds-gray-700)',
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
{writerLabel}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -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 { AttrSetEventBlock } from './attributes-block';
|
|
10
11
|
import { CopyableDataBlock, EncryptedDataBlock } from './copyable-data-block';
|
|
11
12
|
import { DetailCard } from './detail-card';
|
|
12
13
|
|
|
@@ -30,6 +31,7 @@ const DATA_EVENT_TYPES = new Set([
|
|
|
30
31
|
'run_failed',
|
|
31
32
|
'wait_created',
|
|
32
33
|
'wait_completed',
|
|
34
|
+
'attr_set',
|
|
33
35
|
]);
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -226,6 +228,12 @@ function EventDataBlock({
|
|
|
226
228
|
return <EncryptedDataBlock />;
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
// Attribute changes — render the changed keys and the writer instead of
|
|
232
|
+
// the raw JSON payload.
|
|
233
|
+
if (eventType === 'attr_set') {
|
|
234
|
+
return <AttrSetEventBlock data={data} />;
|
|
235
|
+
}
|
|
236
|
+
|
|
229
237
|
// For error events (step_failed, step_retrying), the eventData has the shape
|
|
230
238
|
// { error: StructuredError, stack?: string, ... }. Check both the top-level
|
|
231
239
|
// value and the nested `error` field for a stack trace.
|
|
@@ -18,10 +18,10 @@ import { formatDuration, getHighResInMs } from '../util/timing';
|
|
|
18
18
|
import { SpanContent } from './span-content';
|
|
19
19
|
import { computeSegments } from './span-segments';
|
|
20
20
|
import {
|
|
21
|
-
type ResourceType,
|
|
22
|
-
type SpanLayout,
|
|
23
21
|
getResourceType,
|
|
24
22
|
getSpanLayout,
|
|
23
|
+
type ResourceType,
|
|
24
|
+
type SpanLayout,
|
|
25
25
|
} from './span-strategies';
|
|
26
26
|
|
|
27
27
|
export const getSpanColorClassName = (node: SpanNode): string => {
|
|
@@ -362,6 +362,7 @@ const BOUNDARY_LABELS: Record<string, string> = {
|
|
|
362
362
|
run_completed: 'Completed',
|
|
363
363
|
run_failed: 'Run failed',
|
|
364
364
|
step_completed: 'Completed',
|
|
365
|
+
attr_set: 'Attributes set',
|
|
365
366
|
};
|
|
366
367
|
|
|
367
368
|
export const SpanEventComponent = memo(function SpanEventComponent({
|