@workflow/web-shared 4.1.0 → 4.1.2
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 +3 -1
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +76 -70
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts +5 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +37 -25
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +4 -3
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/sidebar/events-list.d.ts +1 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +26 -18
- package/dist/components/sidebar/events-list.js.map +1 -1
- package/dist/components/ui/data-inspector.d.ts +14 -1
- package/dist/components/ui/data-inspector.d.ts.map +1 -1
- package/dist/components/ui/data-inspector.js +36 -11
- package/dist/components/ui/data-inspector.js.map +1 -1
- package/dist/components/workflow-trace-view.d.ts +3 -1
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +2 -2
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/hydration.d.ts +8 -1
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +36 -12
- package/dist/lib/hydration.js.map +1 -1
- package/package.json +2 -2
- package/src/components/event-list-view.tsx +207 -193
- package/src/components/index.ts +6 -1
- package/src/components/sidebar/attribute-panel.tsx +150 -106
- package/src/components/sidebar/entity-detail-panel.tsx +13 -7
- package/src/components/sidebar/events-list.tsx +42 -32
- package/src/components/ui/data-inspector.tsx +91 -19
- package/src/components/workflow-trace-view.tsx +4 -0
- package/src/index.ts +1 -0
- package/src/lib/hydration.ts +39 -13
|
@@ -5,11 +5,11 @@ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
|
5
5
|
import type { ModelMessage } from 'ai';
|
|
6
6
|
import { Lock } from 'lucide-react';
|
|
7
7
|
import type { KeyboardEvent, ReactNode } from 'react';
|
|
8
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
8
|
+
import { useCallback, useContext, useMemo, useState } from 'react';
|
|
9
9
|
import { isEncryptedMarker, isExpiredMarker } from '../../lib/hydration';
|
|
10
10
|
import { useToast } from '../../lib/toast';
|
|
11
11
|
import { extractConversation, isDoStreamStep } from '../../lib/utils';
|
|
12
|
-
import { StreamClickContext } from '../ui/data-inspector';
|
|
12
|
+
import { DecryptClickContext, StreamClickContext } from '../ui/data-inspector';
|
|
13
13
|
import { ErrorCard } from '../ui/error-card';
|
|
14
14
|
import {
|
|
15
15
|
ErrorStackBlock,
|
|
@@ -180,9 +180,42 @@ function ConversationWithTabs({
|
|
|
180
180
|
* with the lucide Lock icon matching the title bar Decrypt button.
|
|
181
181
|
*/
|
|
182
182
|
function EncryptedFieldBlock() {
|
|
183
|
+
const ctx = useContext(DecryptClickContext);
|
|
184
|
+
if (ctx) {
|
|
185
|
+
return (
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
onClick={ctx.onDecrypt}
|
|
189
|
+
disabled={ctx.isDecrypting}
|
|
190
|
+
className="flex w-full items-center justify-center gap-1.5 rounded-md border px-3 py-2 text-xs cursor-pointer transition-colors"
|
|
191
|
+
style={{
|
|
192
|
+
borderColor: 'var(--ds-gray-400)',
|
|
193
|
+
backgroundColor: 'var(--ds-gray-100)',
|
|
194
|
+
color: 'var(--ds-gray-700)',
|
|
195
|
+
opacity: ctx.isDecrypting ? 0.6 : 1,
|
|
196
|
+
}}
|
|
197
|
+
title="Click to decrypt"
|
|
198
|
+
>
|
|
199
|
+
{ctx.isDecrypting ? (
|
|
200
|
+
<span
|
|
201
|
+
className="h-3 w-3 animate-spin rounded-full border-2"
|
|
202
|
+
style={{
|
|
203
|
+
borderColor: 'var(--ds-gray-400)',
|
|
204
|
+
borderTopColor: 'var(--ds-gray-700)',
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
) : (
|
|
208
|
+
<Lock className="h-3 w-3" />
|
|
209
|
+
)}
|
|
210
|
+
<span className="font-medium">
|
|
211
|
+
{ctx.isDecrypting ? 'Decrypting…' : 'Decrypt'}
|
|
212
|
+
</span>
|
|
213
|
+
</button>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
183
216
|
return (
|
|
184
217
|
<div
|
|
185
|
-
className="flex items-center gap-1.5 rounded-md border px-3 py-2 text-xs"
|
|
218
|
+
className="flex w-full items-center justify-center gap-1.5 rounded-md border px-3 py-2 text-xs"
|
|
186
219
|
style={{
|
|
187
220
|
borderColor: 'var(--ds-gray-300)',
|
|
188
221
|
backgroundColor: 'var(--ds-gray-100)',
|
|
@@ -689,6 +722,8 @@ export const AttributePanel = ({
|
|
|
689
722
|
error,
|
|
690
723
|
expiredAt,
|
|
691
724
|
onStreamClick,
|
|
725
|
+
onDecrypt,
|
|
726
|
+
isDecrypting = false,
|
|
692
727
|
resource,
|
|
693
728
|
}: {
|
|
694
729
|
data: Record<string, unknown>;
|
|
@@ -698,6 +733,10 @@ export const AttributePanel = ({
|
|
|
698
733
|
expiredAt?: string | Date;
|
|
699
734
|
/** Callback when a stream reference is clicked */
|
|
700
735
|
onStreamClick?: (streamId: string) => void;
|
|
736
|
+
/** Callback when an encrypted marker is clicked (triggers decryption) */
|
|
737
|
+
onDecrypt?: () => void;
|
|
738
|
+
/** Whether decryption is currently in progress */
|
|
739
|
+
isDecrypting?: boolean;
|
|
701
740
|
/** Resource type of the selected span — used to show targeted loading skeletons. */
|
|
702
741
|
resource?: string;
|
|
703
742
|
}) => {
|
|
@@ -798,116 +837,121 @@ export const AttributePanel = ({
|
|
|
798
837
|
|
|
799
838
|
return (
|
|
800
839
|
<StreamClickContext.Provider value={onStreamClick}>
|
|
801
|
-
<
|
|
802
|
-
{
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
showResumeAtSkeleton
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
840
|
+
<DecryptClickContext.Provider
|
|
841
|
+
value={onDecrypt ? { onDecrypt, isDecrypting } : undefined}
|
|
842
|
+
>
|
|
843
|
+
<div>
|
|
844
|
+
{/* Basic attributes in a vertical layout with border */}
|
|
845
|
+
{visibleBasicAttributes.length > 0 && (
|
|
846
|
+
<div
|
|
847
|
+
className="mb-3 flex flex-col overflow-hidden rounded-lg border"
|
|
848
|
+
style={{
|
|
849
|
+
borderColor: 'var(--ds-gray-300)',
|
|
850
|
+
}}
|
|
851
|
+
>
|
|
852
|
+
{orderedBasicAttributes.map((attribute, index) => {
|
|
853
|
+
const displayValue = attributeToDisplayFn[
|
|
854
|
+
attribute as keyof typeof attributeToDisplayFn
|
|
855
|
+
]?.(displayData[attribute as keyof typeof displayData]);
|
|
856
|
+
const isModuleSpecifier = attribute === 'moduleSpecifier';
|
|
857
|
+
const moduleSpecifierValue =
|
|
858
|
+
typeof displayValue === 'string'
|
|
859
|
+
? displayValue
|
|
860
|
+
: String(displayValue ?? displayData.moduleSpecifier ?? '');
|
|
861
|
+
const shouldCapitalizeLabel =
|
|
862
|
+
attribute !== 'workflowCoreVersion';
|
|
863
|
+
const showResumeAtSkeleton =
|
|
864
|
+
isLoading && resource === 'sleep' && !displayData.resumeAt;
|
|
865
|
+
const showDivider =
|
|
866
|
+
index < orderedBasicAttributes.length - 1 ||
|
|
867
|
+
showResumeAtSkeleton;
|
|
868
|
+
|
|
869
|
+
return (
|
|
870
|
+
<div key={attribute} className="py-1">
|
|
871
|
+
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
872
|
+
<span
|
|
873
|
+
className={
|
|
874
|
+
shouldCapitalizeLabel
|
|
875
|
+
? 'text-[14px] first-letter:uppercase'
|
|
876
|
+
: 'text-[14px]'
|
|
877
|
+
}
|
|
878
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
879
|
+
>
|
|
880
|
+
{getAttributeDisplayName(attribute)}
|
|
881
|
+
</span>
|
|
882
|
+
{isModuleSpecifier ? (
|
|
883
|
+
<button
|
|
884
|
+
type="button"
|
|
885
|
+
className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
886
|
+
style={{
|
|
887
|
+
color: 'var(--ds-gray-1000)',
|
|
888
|
+
background: 'transparent',
|
|
889
|
+
border: 'none',
|
|
890
|
+
padding: 0,
|
|
891
|
+
}}
|
|
892
|
+
title={moduleSpecifierValue}
|
|
893
|
+
onClick={() =>
|
|
894
|
+
handleCopyModuleSpecifier(moduleSpecifierValue)
|
|
895
|
+
}
|
|
896
|
+
>
|
|
897
|
+
{moduleSpecifierValue}
|
|
898
|
+
</button>
|
|
899
|
+
) : (
|
|
900
|
+
<span
|
|
901
|
+
className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
902
|
+
style={{ color: 'var(--ds-gray-1000)' }}
|
|
903
|
+
>
|
|
904
|
+
{displayValue}
|
|
905
|
+
</span>
|
|
906
|
+
)}
|
|
907
|
+
</div>
|
|
908
|
+
{showDivider ? (
|
|
909
|
+
<div
|
|
910
|
+
className="mx-2.5 border-b"
|
|
911
|
+
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
912
|
+
/>
|
|
913
|
+
) : null}
|
|
914
|
+
</div>
|
|
915
|
+
);
|
|
916
|
+
})}
|
|
917
|
+
{isLoading && resource === 'sleep' && !displayData.resumeAt && (
|
|
918
|
+
<div className="py-1">
|
|
828
919
|
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
829
920
|
<span
|
|
830
|
-
className=
|
|
831
|
-
shouldCapitalizeLabel
|
|
832
|
-
? 'text-[14px] first-letter:uppercase'
|
|
833
|
-
: 'text-[14px]'
|
|
834
|
-
}
|
|
921
|
+
className="text-[14px] first-letter:uppercase"
|
|
835
922
|
style={{ color: 'var(--ds-gray-700)' }}
|
|
836
923
|
>
|
|
837
|
-
|
|
924
|
+
resumeAt
|
|
838
925
|
</span>
|
|
839
|
-
|
|
840
|
-
<button
|
|
841
|
-
type="button"
|
|
842
|
-
className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
843
|
-
style={{
|
|
844
|
-
color: 'var(--ds-gray-1000)',
|
|
845
|
-
background: 'transparent',
|
|
846
|
-
border: 'none',
|
|
847
|
-
padding: 0,
|
|
848
|
-
}}
|
|
849
|
-
title={moduleSpecifierValue}
|
|
850
|
-
onClick={() =>
|
|
851
|
-
handleCopyModuleSpecifier(moduleSpecifierValue)
|
|
852
|
-
}
|
|
853
|
-
>
|
|
854
|
-
{moduleSpecifierValue}
|
|
855
|
-
</button>
|
|
856
|
-
) : (
|
|
857
|
-
<span
|
|
858
|
-
className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
859
|
-
style={{ color: 'var(--ds-gray-1000)' }}
|
|
860
|
-
>
|
|
861
|
-
{displayValue}
|
|
862
|
-
</span>
|
|
863
|
-
)}
|
|
926
|
+
<Skeleton className="h-4 w-[55%]" />
|
|
864
927
|
</div>
|
|
865
|
-
{showDivider ? (
|
|
866
|
-
<div
|
|
867
|
-
className="mx-2.5 border-b"
|
|
868
|
-
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
869
|
-
/>
|
|
870
|
-
) : null}
|
|
871
|
-
</div>
|
|
872
|
-
);
|
|
873
|
-
})}
|
|
874
|
-
{isLoading && resource === 'sleep' && !displayData.resumeAt && (
|
|
875
|
-
<div className="py-1">
|
|
876
|
-
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
877
|
-
<span
|
|
878
|
-
className="text-[14px] first-letter:uppercase"
|
|
879
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
880
|
-
>
|
|
881
|
-
resumeAt
|
|
882
|
-
</span>
|
|
883
|
-
<Skeleton className="h-4 w-[55%]" />
|
|
884
928
|
</div>
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
</
|
|
929
|
+
)}
|
|
930
|
+
</div>
|
|
931
|
+
)}
|
|
932
|
+
{error ? (
|
|
933
|
+
<ErrorCard
|
|
934
|
+
title="Failed to load resource details"
|
|
935
|
+
details={error.message}
|
|
936
|
+
className="my-4"
|
|
937
|
+
/>
|
|
938
|
+
) : hasExpired ? (
|
|
939
|
+
<ExpiredDataMessage />
|
|
940
|
+
) : (
|
|
941
|
+
<>
|
|
942
|
+
{resolvedAttributes.map((attribute) => (
|
|
943
|
+
<AttributeBlock
|
|
944
|
+
isLoading={isLoading}
|
|
945
|
+
key={attribute}
|
|
946
|
+
attribute={attribute}
|
|
947
|
+
value={displayData[attribute as keyof typeof displayData]}
|
|
948
|
+
context={displayContext}
|
|
949
|
+
/>
|
|
950
|
+
))}
|
|
951
|
+
</>
|
|
952
|
+
)}
|
|
953
|
+
</div>
|
|
954
|
+
</DecryptClickContext.Provider>
|
|
911
955
|
</StreamClickContext.Provider>
|
|
912
956
|
);
|
|
913
957
|
};
|
|
@@ -67,6 +67,7 @@ export function EntityDetailPanel({
|
|
|
67
67
|
onDecrypt,
|
|
68
68
|
isDecrypting = false,
|
|
69
69
|
selectedSpan,
|
|
70
|
+
hasEncryptedData = false,
|
|
70
71
|
}: {
|
|
71
72
|
run: WorkflowRun;
|
|
72
73
|
/** Callback when a stream reference is clicked */
|
|
@@ -103,6 +104,8 @@ export function EntityDetailPanel({
|
|
|
103
104
|
isDecrypting?: boolean;
|
|
104
105
|
/** Info about the currently selected span from the trace viewer */
|
|
105
106
|
selectedSpan: SelectedSpanInfo | null;
|
|
107
|
+
/** Run-level hint: the run contains encrypted data (from probe). */
|
|
108
|
+
hasEncryptedData?: boolean;
|
|
106
109
|
}): React.JSX.Element | null {
|
|
107
110
|
const toast = useToast();
|
|
108
111
|
const [stoppingSleep, setStoppingSleep] = useState(false);
|
|
@@ -391,13 +394,14 @@ export function EntityDetailPanel({
|
|
|
391
394
|
{resourceId}
|
|
392
395
|
</p>
|
|
393
396
|
</div>
|
|
394
|
-
{(hasEncryptedFields || encryptionKey) &&
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
397
|
+
{(hasEncryptedFields || hasEncryptedData || encryptionKey) &&
|
|
398
|
+
onDecrypt && (
|
|
399
|
+
<DecryptButton
|
|
400
|
+
decrypted={!!encryptionKey}
|
|
401
|
+
loading={isDecrypting}
|
|
402
|
+
onClick={onDecrypt}
|
|
403
|
+
/>
|
|
404
|
+
)}
|
|
401
405
|
</div>
|
|
402
406
|
</div>
|
|
403
407
|
|
|
@@ -481,6 +485,8 @@ export function EntityDetailPanel({
|
|
|
481
485
|
isLoading={loading}
|
|
482
486
|
error={error ?? undefined}
|
|
483
487
|
onStreamClick={onStreamClick}
|
|
488
|
+
onDecrypt={onDecrypt}
|
|
489
|
+
isDecrypting={isDecrypting}
|
|
484
490
|
resource={resource}
|
|
485
491
|
/>
|
|
486
492
|
</section>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { EVENT_DATA_REF_FIELDS, type Event } from '@workflow/world';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { isExpiredMarker } from '../../lib/hydration';
|
|
6
6
|
import { ErrorCard } from '../ui/error-card';
|
|
@@ -55,46 +55,56 @@ function EventItem({
|
|
|
55
55
|
const [isLoading, setIsLoading] = useState(false);
|
|
56
56
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
57
57
|
const wasExpandedRef = useRef(false);
|
|
58
|
+
/** Mirrors whether we have a fetched payload; avoids stale `loadedData` in load closure. */
|
|
59
|
+
const loadedDataRef = useRef<unknown | null>(null);
|
|
58
60
|
|
|
59
61
|
// Check if the event already has eventData from the store
|
|
60
62
|
const existingData =
|
|
61
63
|
'eventData' in event && event.eventData != null ? event.eventData : null;
|
|
62
|
-
const
|
|
64
|
+
const mergedDisplay = loadedData ?? existingData;
|
|
63
65
|
const canHaveData = DATA_EVENT_TYPES.has(event.eventType);
|
|
64
66
|
|
|
65
|
-
const loadEventData = useCallback(
|
|
66
|
-
|
|
67
|
+
const loadEventData = useCallback(
|
|
68
|
+
async (options?: { force?: boolean }) => {
|
|
69
|
+
if (!onLoadEventData || !event.correlationId || !event.eventId) return;
|
|
70
|
+
if (!options?.force && loadedDataRef.current !== null) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
try {
|
|
75
|
+
setIsLoading(true);
|
|
76
|
+
setLoadError(null);
|
|
77
|
+
const data = await onLoadEventData(event.correlationId, event.eventId);
|
|
78
|
+
loadedDataRef.current = data;
|
|
79
|
+
setLoadedData(data);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
setLoadError(err instanceof Error ? err.message : String(err));
|
|
82
|
+
} finally {
|
|
83
|
+
setIsLoading(false);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
[onLoadEventData, event.correlationId, event.eventId]
|
|
87
|
+
);
|
|
79
88
|
|
|
80
89
|
const handleExpand = useCallback(async () => {
|
|
81
|
-
if (
|
|
90
|
+
if (isLoading) return;
|
|
82
91
|
wasExpandedRef.current = true;
|
|
83
92
|
await loadEventData();
|
|
84
|
-
}, [
|
|
93
|
+
}, [isLoading, loadEventData]);
|
|
85
94
|
|
|
86
95
|
// When the encryption key changes and this event was previously expanded,
|
|
87
96
|
// re-load the data so it gets decrypted
|
|
88
97
|
useEffect(() => {
|
|
89
|
-
if (encryptionKey
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
}, [encryptionKey]);
|
|
98
|
+
if (!encryptionKey || !wasExpandedRef.current) return;
|
|
99
|
+
loadedDataRef.current = null;
|
|
100
|
+
setLoadedData(null);
|
|
101
|
+
void loadEventData({ force: true });
|
|
102
|
+
}, [encryptionKey, loadEventData]);
|
|
95
103
|
|
|
96
104
|
const createdAt = new Date(event.createdAt);
|
|
97
105
|
|
|
106
|
+
const displayPayload = isLoading ? loadedData : mergedDisplay;
|
|
107
|
+
|
|
98
108
|
return (
|
|
99
109
|
<DetailCard
|
|
100
110
|
summaryClassName="text-base py-2"
|
|
@@ -189,9 +199,9 @@ function EventItem({
|
|
|
189
199
|
)}
|
|
190
200
|
|
|
191
201
|
{/* Event data */}
|
|
192
|
-
{
|
|
202
|
+
{displayPayload != null && (
|
|
193
203
|
<div className="mt-2">
|
|
194
|
-
<EventDataBlock eventType={event.eventType} data={
|
|
204
|
+
<EventDataBlock eventType={event.eventType} data={displayPayload} />
|
|
195
205
|
</div>
|
|
196
206
|
)}
|
|
197
207
|
</DetailCard>
|
|
@@ -199,17 +209,17 @@ function EventItem({
|
|
|
199
209
|
}
|
|
200
210
|
|
|
201
211
|
/**
|
|
202
|
-
* Check if an eventData object has only expired marker values in
|
|
203
|
-
*
|
|
204
|
-
*
|
|
212
|
+
* Check if an eventData object has only expired marker values in ref/payload
|
|
213
|
+
* fields for this event type (see {@link EVENT_DATA_REF_FIELDS}). Other keys
|
|
214
|
+
* (e.g. `resumeAt`, `stepName`) are ignored.
|
|
205
215
|
*/
|
|
206
|
-
function hasOnlyExpiredFields(data: unknown): boolean {
|
|
216
|
+
function hasOnlyExpiredFields(data: unknown, eventType: string): boolean {
|
|
207
217
|
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
208
218
|
return false;
|
|
209
219
|
}
|
|
210
220
|
const record = data as Record<string, unknown>;
|
|
211
|
-
const
|
|
212
|
-
const presentKeys =
|
|
221
|
+
const refKeys = EVENT_DATA_REF_FIELDS[eventType] ?? [];
|
|
222
|
+
const presentKeys = refKeys.filter((k) => k in record);
|
|
213
223
|
return (
|
|
214
224
|
presentKeys.length > 0 &&
|
|
215
225
|
presentKeys.every((k) => isExpiredMarker(record[k]))
|
|
@@ -230,7 +240,7 @@ function EventDataBlock({
|
|
|
230
240
|
// Expired data — show a simple message instead of the raw stub.
|
|
231
241
|
// Check both the top-level eventData and nested sub-fields (result, input, etc.)
|
|
232
242
|
// since the server stubs each ref field independently.
|
|
233
|
-
if (isExpiredMarker(data) || hasOnlyExpiredFields(data)) {
|
|
243
|
+
if (isExpiredMarker(data) || hasOnlyExpiredFields(data, eventType)) {
|
|
234
244
|
return (
|
|
235
245
|
<div
|
|
236
246
|
className="flex items-center gap-1.5 rounded-md border px-3 py-2 text-xs"
|
|
@@ -75,6 +75,76 @@ export const StreamClickContext = createContext<
|
|
|
75
75
|
((streamId: string) => void) | undefined
|
|
76
76
|
>(undefined);
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Context for passing a decrypt handler down to DataInspector instances.
|
|
80
|
+
* When provided, encrypted markers become clickable buttons that trigger decryption.
|
|
81
|
+
*/
|
|
82
|
+
export type DecryptClickContextValue = {
|
|
83
|
+
onDecrypt: () => void;
|
|
84
|
+
isDecrypting: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const DecryptClickContext = createContext<
|
|
88
|
+
DecryptClickContextValue | undefined
|
|
89
|
+
>(undefined);
|
|
90
|
+
|
|
91
|
+
function EncryptedInlineLabel() {
|
|
92
|
+
const ctx = useContext(DecryptClickContext);
|
|
93
|
+
if (ctx) {
|
|
94
|
+
return (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] cursor-pointer"
|
|
98
|
+
style={{
|
|
99
|
+
backgroundColor: 'var(--ds-gray-100)',
|
|
100
|
+
color: 'var(--ds-gray-700)',
|
|
101
|
+
border: '1px solid var(--ds-gray-400)',
|
|
102
|
+
fontStyle: 'italic',
|
|
103
|
+
opacity: ctx.isDecrypting ? 0.6 : 1,
|
|
104
|
+
}}
|
|
105
|
+
disabled={ctx.isDecrypting}
|
|
106
|
+
onClick={(e) => {
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
ctx.onDecrypt();
|
|
109
|
+
}}
|
|
110
|
+
title="Click to decrypt"
|
|
111
|
+
>
|
|
112
|
+
{ctx.isDecrypting ? (
|
|
113
|
+
<span
|
|
114
|
+
className="h-3 w-3 animate-spin rounded-full border-2"
|
|
115
|
+
style={{
|
|
116
|
+
display: 'inline-block',
|
|
117
|
+
flexShrink: 0,
|
|
118
|
+
borderColor: 'var(--ds-gray-400)',
|
|
119
|
+
borderTopColor: 'var(--ds-gray-700)',
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
) : (
|
|
123
|
+
<Lock
|
|
124
|
+
className="h-3 w-3"
|
|
125
|
+
style={{ display: 'inline', flexShrink: 0 }}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
<span>{ctx.isDecrypting ? 'Decrypting…' : 'Decrypt'}</span>
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return (
|
|
133
|
+
<span style={{ color: 'var(--ds-gray-600)', fontStyle: 'italic' }}>
|
|
134
|
+
<Lock
|
|
135
|
+
className="h-3 w-3"
|
|
136
|
+
style={{
|
|
137
|
+
display: 'inline',
|
|
138
|
+
verticalAlign: 'middle',
|
|
139
|
+
marginRight: '3px',
|
|
140
|
+
marginTop: '-1px',
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
Encrypted
|
|
144
|
+
</span>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
78
148
|
function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
|
|
79
149
|
const onStreamClick = useContext(StreamClickContext);
|
|
80
150
|
return (
|
|
@@ -132,26 +202,13 @@ function NodeRenderer({
|
|
|
132
202
|
}) {
|
|
133
203
|
const extendedTheme = useContext(ExtendedThemeContext);
|
|
134
204
|
|
|
135
|
-
// Encrypted marker → flat label with Lock icon,
|
|
205
|
+
// Encrypted marker → flat label with Lock icon, clickable when onDecrypt is available
|
|
136
206
|
if (
|
|
137
207
|
data !== null &&
|
|
138
208
|
typeof data === 'object' &&
|
|
139
209
|
data.constructor?.name === ENCRYPTED_DISPLAY_NAME
|
|
140
210
|
) {
|
|
141
|
-
const label =
|
|
142
|
-
<span style={{ color: 'var(--ds-gray-600)', fontStyle: 'italic' }}>
|
|
143
|
-
<Lock
|
|
144
|
-
className="h-3 w-3"
|
|
145
|
-
style={{
|
|
146
|
-
display: 'inline',
|
|
147
|
-
verticalAlign: 'middle',
|
|
148
|
-
marginRight: '3px',
|
|
149
|
-
marginTop: '-1px',
|
|
150
|
-
}}
|
|
151
|
-
/>
|
|
152
|
-
Encrypted
|
|
153
|
-
</span>
|
|
154
|
-
);
|
|
211
|
+
const label = <EncryptedInlineLabel />;
|
|
155
212
|
if (depth === 0) {
|
|
156
213
|
return label;
|
|
157
214
|
}
|
|
@@ -234,6 +291,10 @@ export interface DataInspectorProps {
|
|
|
234
291
|
name?: string;
|
|
235
292
|
/** Callback when a stream reference is clicked */
|
|
236
293
|
onStreamClick?: (streamId: string) => void;
|
|
294
|
+
/** Callback when an encrypted marker is clicked (triggers decryption) */
|
|
295
|
+
onDecrypt?: () => void;
|
|
296
|
+
/** Whether decryption is currently in progress */
|
|
297
|
+
isDecrypting?: boolean;
|
|
237
298
|
}
|
|
238
299
|
|
|
239
300
|
export function DataInspector({
|
|
@@ -241,6 +302,8 @@ export function DataInspector({
|
|
|
241
302
|
expandLevel = 2,
|
|
242
303
|
name,
|
|
243
304
|
onStreamClick,
|
|
305
|
+
onDecrypt,
|
|
306
|
+
isDecrypting = false,
|
|
244
307
|
}: DataInspectorProps) {
|
|
245
308
|
const stableData = useStableInspectorData(data);
|
|
246
309
|
const [initialExpandLevel, setInitialExpandLevel] = useState(expandLevel);
|
|
@@ -269,16 +332,25 @@ export function DataInspector({
|
|
|
269
332
|
</ExtendedThemeContext.Provider>
|
|
270
333
|
);
|
|
271
334
|
|
|
272
|
-
|
|
335
|
+
let wrapped = content;
|
|
336
|
+
|
|
273
337
|
if (onStreamClick) {
|
|
274
|
-
|
|
338
|
+
wrapped = (
|
|
275
339
|
<StreamClickContext.Provider value={onStreamClick}>
|
|
276
|
-
{
|
|
340
|
+
{wrapped}
|
|
277
341
|
</StreamClickContext.Provider>
|
|
278
342
|
);
|
|
279
343
|
}
|
|
280
344
|
|
|
281
|
-
|
|
345
|
+
if (onDecrypt) {
|
|
346
|
+
wrapped = (
|
|
347
|
+
<DecryptClickContext.Provider value={{ onDecrypt, isDecrypting }}>
|
|
348
|
+
{wrapped}
|
|
349
|
+
</DecryptClickContext.Provider>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return wrapped;
|
|
282
354
|
}
|
|
283
355
|
|
|
284
356
|
function useStableInspectorData<T>(next: T): T {
|
|
@@ -775,6 +775,7 @@ export const WorkflowTraceViewer = ({
|
|
|
775
775
|
encryptionKey,
|
|
776
776
|
onDecrypt,
|
|
777
777
|
isDecrypting = false,
|
|
778
|
+
hasEncryptedData = false,
|
|
778
779
|
}: {
|
|
779
780
|
run: WorkflowRun;
|
|
780
781
|
events: Event[];
|
|
@@ -815,6 +816,8 @@ export const WorkflowTraceViewer = ({
|
|
|
815
816
|
onDecrypt?: () => void;
|
|
816
817
|
/** Whether the encryption key is currently being fetched */
|
|
817
818
|
isDecrypting?: boolean;
|
|
819
|
+
/** Run-level hint: the run contains encrypted data (from probe). */
|
|
820
|
+
hasEncryptedData?: boolean;
|
|
818
821
|
}) => {
|
|
819
822
|
const toast = useToast();
|
|
820
823
|
const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
|
|
@@ -1151,6 +1154,7 @@ export const WorkflowTraceViewer = ({
|
|
|
1151
1154
|
onDecrypt={onDecrypt}
|
|
1152
1155
|
isDecrypting={isDecrypting}
|
|
1153
1156
|
selectedSpan={selectedSpan}
|
|
1157
|
+
hasEncryptedData={hasEncryptedData}
|
|
1154
1158
|
/>
|
|
1155
1159
|
</ErrorBoundary>
|
|
1156
1160
|
</div>
|