@workflow/web-shared 4.1.0-beta.55 → 4.1.0-beta.57
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 +34 -2
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/run-trace-view.d.ts +4 -1
- package/dist/components/run-trace-view.d.ts.map +1 -1
- package/dist/components/run-trace-view.js +2 -2
- package/dist/components/run-trace-view.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +122 -45
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/copyable-data-block.d.ts +4 -0
- package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -0
- package/dist/components/sidebar/copyable-data-block.js +33 -0
- package/dist/components/sidebar/copyable-data-block.js.map +1 -0
- package/dist/components/sidebar/detail-card.d.ts +7 -1
- package/dist/components/sidebar/detail-card.d.ts.map +1 -1
- package/dist/components/sidebar/detail-card.js +12 -3
- package/dist/components/sidebar/detail-card.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +30 -16
- 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 +37 -7
- package/dist/components/sidebar/events-list.js.map +1 -1
- package/dist/components/ui/error-stack-block.d.ts +19 -0
- package/dist/components/ui/error-stack-block.d.ts.map +1 -0
- package/dist/components/ui/error-stack-block.js +39 -0
- package/dist/components/ui/error-stack-block.js.map +1 -0
- package/dist/components/workflow-trace-view.d.ts +7 -1
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +137 -24
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/package.json +10 -10
- package/src/components/event-list-view.tsx +53 -2
- package/src/components/run-trace-view.tsx +9 -0
- package/src/components/sidebar/attribute-panel.tsx +285 -127
- package/src/components/sidebar/copyable-data-block.tsx +51 -0
- package/src/components/sidebar/detail-card.tsx +28 -2
- package/src/components/sidebar/entity-detail-panel.tsx +138 -81
- package/src/components/sidebar/events-list.tsx +72 -21
- package/src/components/ui/error-stack-block.tsx +80 -0
- package/src/components/workflow-trace-view.tsx +208 -28
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
|
|
4
4
|
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ChevronDown,
|
|
7
|
+
ChevronUp,
|
|
8
|
+
Clock,
|
|
9
|
+
Copy,
|
|
10
|
+
Info,
|
|
11
|
+
Send,
|
|
12
|
+
Type,
|
|
13
|
+
X,
|
|
14
|
+
XCircle,
|
|
15
|
+
} from 'lucide-react';
|
|
6
16
|
import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
|
7
17
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
18
|
import { createPortal } from 'react-dom';
|
|
@@ -89,7 +99,7 @@ function useLiveTick(isLive: boolean): void {
|
|
|
89
99
|
}, [isLive, dispatch]);
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
const DEFAULT_PANEL_WIDTH =
|
|
102
|
+
const DEFAULT_PANEL_WIDTH = 450;
|
|
93
103
|
const MIN_PANEL_WIDTH = 240;
|
|
94
104
|
|
|
95
105
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -268,6 +278,9 @@ function TraceViewerWithContextMenu({
|
|
|
268
278
|
onWakeUpSleep,
|
|
269
279
|
onCancelRun,
|
|
270
280
|
onResolveHook,
|
|
281
|
+
onLoadMoreSpans,
|
|
282
|
+
hasMoreSpans = false,
|
|
283
|
+
isLoadingMoreSpans = false,
|
|
271
284
|
children,
|
|
272
285
|
}: {
|
|
273
286
|
trace: { spans: Span[] };
|
|
@@ -284,9 +297,12 @@ function TraceViewerWithContextMenu({
|
|
|
284
297
|
payload: unknown,
|
|
285
298
|
hook?: Hook
|
|
286
299
|
) => Promise<void>;
|
|
300
|
+
onLoadMoreSpans?: () => void | Promise<void>;
|
|
301
|
+
hasMoreSpans?: boolean;
|
|
302
|
+
isLoadingMoreSpans?: boolean;
|
|
287
303
|
children: ReactNode;
|
|
288
304
|
}): ReactNode {
|
|
289
|
-
const { dispatch } = useTraceViewer();
|
|
305
|
+
const { state, dispatch } = useTraceViewer();
|
|
290
306
|
|
|
291
307
|
// Drive active span widths at 60fps without React re-renders
|
|
292
308
|
useLiveTick(isLive);
|
|
@@ -403,6 +419,37 @@ function TraceViewerWithContextMenu({
|
|
|
403
419
|
};
|
|
404
420
|
}, [handleContextMenu]);
|
|
405
421
|
|
|
422
|
+
const loadingMoreRef = useRef(false);
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
const timeline = state.timelineRef.current;
|
|
425
|
+
if (!timeline || !onLoadMoreSpans || !hasMoreSpans) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const thresholdPx = 200;
|
|
430
|
+
const maybeLoadMore = () => {
|
|
431
|
+
if (loadingMoreRef.current || isLoadingMoreSpans || !hasMoreSpans) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const remaining =
|
|
435
|
+
timeline.scrollHeight - timeline.scrollTop - timeline.clientHeight;
|
|
436
|
+
if (remaining > thresholdPx) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
loadingMoreRef.current = true;
|
|
441
|
+
Promise.resolve(onLoadMoreSpans()).finally(() => {
|
|
442
|
+
loadingMoreRef.current = false;
|
|
443
|
+
});
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
timeline.addEventListener('scroll', maybeLoadMore);
|
|
447
|
+
maybeLoadMore();
|
|
448
|
+
return () => {
|
|
449
|
+
timeline.removeEventListener('scroll', maybeLoadMore);
|
|
450
|
+
};
|
|
451
|
+
}, [state.timelineRef, onLoadMoreSpans, hasMoreSpans, isLoadingMoreSpans]);
|
|
452
|
+
|
|
406
453
|
const closeMenu = useCallback(() => {
|
|
407
454
|
setContextMenu(null);
|
|
408
455
|
}, []);
|
|
@@ -768,6 +815,25 @@ function DeselectBridge({ triggerDeselect }: { triggerDeselect: number }) {
|
|
|
768
815
|
return null;
|
|
769
816
|
}
|
|
770
817
|
|
|
818
|
+
/**
|
|
819
|
+
* Bridge component to select a span from outside the context.
|
|
820
|
+
*/
|
|
821
|
+
function SelectBridge({
|
|
822
|
+
selectRequest,
|
|
823
|
+
}: {
|
|
824
|
+
selectRequest: { id: string; token: number } | null;
|
|
825
|
+
}) {
|
|
826
|
+
const { dispatch } = useTraceViewer();
|
|
827
|
+
|
|
828
|
+
useEffect(() => {
|
|
829
|
+
if (selectRequest?.id) {
|
|
830
|
+
dispatch({ type: 'select', id: selectRequest.id });
|
|
831
|
+
}
|
|
832
|
+
}, [dispatch, selectRequest]);
|
|
833
|
+
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
|
|
771
837
|
// ---------------------------------------------------------------------------
|
|
772
838
|
// Panel chrome (header with name/duration, close button)
|
|
773
839
|
// ---------------------------------------------------------------------------
|
|
@@ -824,6 +890,9 @@ export const WorkflowTraceViewer = ({
|
|
|
824
890
|
onStreamClick,
|
|
825
891
|
onSpanSelect,
|
|
826
892
|
onLoadEventData,
|
|
893
|
+
onLoadMoreSpans,
|
|
894
|
+
hasMoreSpans = false,
|
|
895
|
+
isLoadingMoreSpans = false,
|
|
827
896
|
}: {
|
|
828
897
|
run: WorkflowRun;
|
|
829
898
|
steps: Step[];
|
|
@@ -854,12 +923,22 @@ export const WorkflowTraceViewer = ({
|
|
|
854
923
|
correlationId: string,
|
|
855
924
|
eventId: string
|
|
856
925
|
) => Promise<unknown | null>;
|
|
926
|
+
/** Load next trace page when vertical scroll reaches bottom. */
|
|
927
|
+
onLoadMoreSpans?: () => void | Promise<void>;
|
|
928
|
+
/** Whether trace pagination has more data to load. */
|
|
929
|
+
hasMoreSpans?: boolean;
|
|
930
|
+
/** Whether trace pagination is currently fetching another page. */
|
|
931
|
+
isLoadingMoreSpans?: boolean;
|
|
857
932
|
}) => {
|
|
858
933
|
const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
|
|
859
934
|
null
|
|
860
935
|
);
|
|
861
936
|
const [panelWidth, setPanelWidth] = useState(DEFAULT_PANEL_WIDTH);
|
|
862
937
|
const [deselectTrigger, setDeselectTrigger] = useState(0);
|
|
938
|
+
const [selectRequest, setSelectRequest] = useState<{
|
|
939
|
+
id: string;
|
|
940
|
+
token: number;
|
|
941
|
+
} | null>(null);
|
|
863
942
|
|
|
864
943
|
const isLive = Boolean(run && !run.completedAt);
|
|
865
944
|
|
|
@@ -944,6 +1023,27 @@ export const WorkflowTraceViewer = ({
|
|
|
944
1023
|
const handleResize = useCallback((deltaX: number) => {
|
|
945
1024
|
setPanelWidth((w) => Math.max(MIN_PANEL_WIDTH, w + deltaX));
|
|
946
1025
|
}, []);
|
|
1026
|
+
const selectedSpanIndex = useMemo(() => {
|
|
1027
|
+
if (!selectedSpan?.spanId || !trace) return -1;
|
|
1028
|
+
return trace.spans.findIndex((span) => span.spanId === selectedSpan.spanId);
|
|
1029
|
+
}, [selectedSpan?.spanId, trace]);
|
|
1030
|
+
const canSelectPrevSpan = selectedSpanIndex > 0;
|
|
1031
|
+
const canSelectNextSpan =
|
|
1032
|
+
selectedSpanIndex >= 0 &&
|
|
1033
|
+
trace != null &&
|
|
1034
|
+
selectedSpanIndex < trace.spans.length - 1;
|
|
1035
|
+
const handleSelectPrevSpan = useCallback(() => {
|
|
1036
|
+
if (!trace || selectedSpanIndex <= 0) return;
|
|
1037
|
+
const targetId = trace.spans[selectedSpanIndex - 1]?.spanId;
|
|
1038
|
+
if (!targetId) return;
|
|
1039
|
+
setSelectRequest({ id: targetId, token: Date.now() });
|
|
1040
|
+
}, [selectedSpanIndex, trace]);
|
|
1041
|
+
const handleSelectNextSpan = useCallback(() => {
|
|
1042
|
+
if (!trace || selectedSpanIndex < 0) return;
|
|
1043
|
+
const targetId = trace.spans[selectedSpanIndex + 1]?.spanId;
|
|
1044
|
+
if (!targetId) return;
|
|
1045
|
+
setSelectRequest({ id: targetId, token: Date.now() });
|
|
1046
|
+
}, [selectedSpanIndex, trace]);
|
|
947
1047
|
|
|
948
1048
|
// Get the selected span name for the panel header
|
|
949
1049
|
const selectedSpanName = useMemo(() => {
|
|
@@ -986,6 +1086,7 @@ export const WorkflowTraceViewer = ({
|
|
|
986
1086
|
>
|
|
987
1087
|
<SelectionBridge onSelectionChange={handleSelectionChange} />
|
|
988
1088
|
<DeselectBridge triggerDeselect={deselectTrigger} />
|
|
1089
|
+
<SelectBridge selectRequest={selectRequest} />
|
|
989
1090
|
<TraceViewerWithContextMenu
|
|
990
1091
|
trace={trace}
|
|
991
1092
|
run={run}
|
|
@@ -994,6 +1095,9 @@ export const WorkflowTraceViewer = ({
|
|
|
994
1095
|
onWakeUpSleep={onWakeUpSleep}
|
|
995
1096
|
onCancelRun={onCancelRun}
|
|
996
1097
|
onResolveHook={onResolveHook}
|
|
1098
|
+
onLoadMoreSpans={onLoadMoreSpans}
|
|
1099
|
+
hasMoreSpans={hasMoreSpans}
|
|
1100
|
+
isLoadingMoreSpans={isLoadingMoreSpans}
|
|
997
1101
|
>
|
|
998
1102
|
<TraceViewerTimeline
|
|
999
1103
|
eagerRender
|
|
@@ -1018,7 +1122,7 @@ export const WorkflowTraceViewer = ({
|
|
|
1018
1122
|
<PanelResizeHandle onResize={handleResize} />
|
|
1019
1123
|
{/* Panel header */}
|
|
1020
1124
|
<div
|
|
1021
|
-
className="flex items-center justify-between px-3 py-2 border-
|
|
1125
|
+
className="flex items-center justify-between px-3 py-2 border-y flex-shrink-0"
|
|
1022
1126
|
style={{ borderColor: 'var(--ds-gray-200)' }}
|
|
1023
1127
|
>
|
|
1024
1128
|
<div className="min-w-0 flex-1">
|
|
@@ -1030,33 +1134,109 @@ export const WorkflowTraceViewer = ({
|
|
|
1030
1134
|
{selectedSpanName}
|
|
1031
1135
|
</div>
|
|
1032
1136
|
</div>
|
|
1033
|
-
<
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1137
|
+
<div className="flex items-center">
|
|
1138
|
+
<button
|
|
1139
|
+
type="button"
|
|
1140
|
+
aria-label="Previous span"
|
|
1141
|
+
onClick={handleSelectPrevSpan}
|
|
1142
|
+
disabled={!canSelectPrevSpan}
|
|
1143
|
+
style={{
|
|
1144
|
+
display: 'flex',
|
|
1145
|
+
alignItems: 'center',
|
|
1146
|
+
justifyContent: 'center',
|
|
1147
|
+
marginLeft: 6,
|
|
1148
|
+
width: 24,
|
|
1149
|
+
height: 24,
|
|
1150
|
+
padding: 0,
|
|
1151
|
+
borderRadius: 6,
|
|
1152
|
+
border: 'none',
|
|
1153
|
+
background: 'transparent',
|
|
1154
|
+
color: 'var(--ds-gray-700)',
|
|
1155
|
+
cursor: canSelectPrevSpan ? 'pointer' : 'not-allowed',
|
|
1156
|
+
opacity: canSelectPrevSpan ? 1 : 0.45,
|
|
1157
|
+
flexShrink: 0,
|
|
1158
|
+
transition: 'background 0.15s',
|
|
1159
|
+
}}
|
|
1160
|
+
onMouseEnter={(e) => {
|
|
1161
|
+
if (!canSelectPrevSpan) return;
|
|
1162
|
+
e.currentTarget.style.background = 'var(--ds-gray-alpha-100)';
|
|
1163
|
+
}}
|
|
1164
|
+
onMouseLeave={(e) => {
|
|
1165
|
+
e.currentTarget.style.background = 'transparent';
|
|
1166
|
+
}}
|
|
1167
|
+
>
|
|
1168
|
+
<ChevronUp size={16} />
|
|
1169
|
+
</button>
|
|
1170
|
+
<button
|
|
1171
|
+
type="button"
|
|
1172
|
+
aria-label="Next span"
|
|
1173
|
+
onClick={handleSelectNextSpan}
|
|
1174
|
+
disabled={!canSelectNextSpan}
|
|
1175
|
+
style={{
|
|
1176
|
+
display: 'flex',
|
|
1177
|
+
alignItems: 'center',
|
|
1178
|
+
justifyContent: 'center',
|
|
1179
|
+
marginLeft: 2,
|
|
1180
|
+
width: 24,
|
|
1181
|
+
height: 24,
|
|
1182
|
+
padding: 0,
|
|
1183
|
+
borderRadius: 6,
|
|
1184
|
+
border: 'none',
|
|
1185
|
+
background: 'transparent',
|
|
1186
|
+
color: 'var(--ds-gray-700)',
|
|
1187
|
+
cursor: canSelectNextSpan ? 'pointer' : 'not-allowed',
|
|
1188
|
+
opacity: canSelectNextSpan ? 1 : 0.45,
|
|
1189
|
+
flexShrink: 0,
|
|
1190
|
+
transition: 'background 0.15s',
|
|
1191
|
+
}}
|
|
1192
|
+
onMouseEnter={(e) => {
|
|
1193
|
+
if (!canSelectNextSpan) return;
|
|
1194
|
+
e.currentTarget.style.background = 'var(--ds-gray-alpha-100)';
|
|
1195
|
+
}}
|
|
1196
|
+
onMouseLeave={(e) => {
|
|
1197
|
+
e.currentTarget.style.background = 'transparent';
|
|
1198
|
+
}}
|
|
1199
|
+
>
|
|
1200
|
+
<ChevronDown size={16} />
|
|
1201
|
+
</button>
|
|
1202
|
+
</div>
|
|
1203
|
+
<div
|
|
1204
|
+
className="flex items-center"
|
|
1037
1205
|
style={{
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
justifyContent: 'center',
|
|
1041
|
-
marginLeft: 8,
|
|
1042
|
-
padding: 4,
|
|
1043
|
-
borderRadius: 6,
|
|
1044
|
-
border: 'none',
|
|
1045
|
-
background: 'transparent',
|
|
1046
|
-
color: 'var(--ds-gray-900)',
|
|
1047
|
-
cursor: 'pointer',
|
|
1048
|
-
flexShrink: 0,
|
|
1049
|
-
transition: 'background 0.15s',
|
|
1050
|
-
}}
|
|
1051
|
-
onMouseEnter={(e) => {
|
|
1052
|
-
e.currentTarget.style.background = 'var(--ds-gray-alpha-200)';
|
|
1053
|
-
}}
|
|
1054
|
-
onMouseLeave={(e) => {
|
|
1055
|
-
e.currentTarget.style.background = 'transparent';
|
|
1206
|
+
borderLeft: '1px solid var(--ds-gray-300)',
|
|
1207
|
+
marginLeft: 6,
|
|
1056
1208
|
}}
|
|
1057
1209
|
>
|
|
1058
|
-
<
|
|
1059
|
-
|
|
1210
|
+
<button
|
|
1211
|
+
type="button"
|
|
1212
|
+
aria-label="Close panel"
|
|
1213
|
+
onClick={handleClose}
|
|
1214
|
+
style={{
|
|
1215
|
+
display: 'flex',
|
|
1216
|
+
alignItems: 'center',
|
|
1217
|
+
justifyContent: 'center',
|
|
1218
|
+
marginLeft: 6,
|
|
1219
|
+
width: 24,
|
|
1220
|
+
height: 24,
|
|
1221
|
+
padding: 0,
|
|
1222
|
+
borderRadius: 6,
|
|
1223
|
+
border: 'none',
|
|
1224
|
+
background: 'transparent',
|
|
1225
|
+
color: 'var(--ds-gray-700)',
|
|
1226
|
+
cursor: 'pointer',
|
|
1227
|
+
flexShrink: 0,
|
|
1228
|
+
transition: 'background 0.15s',
|
|
1229
|
+
}}
|
|
1230
|
+
onMouseEnter={(e) => {
|
|
1231
|
+
e.currentTarget.style.background = 'var(--ds-gray-alpha-100)';
|
|
1232
|
+
}}
|
|
1233
|
+
onMouseLeave={(e) => {
|
|
1234
|
+
e.currentTarget.style.background = 'transparent';
|
|
1235
|
+
}}
|
|
1236
|
+
>
|
|
1237
|
+
<X size={16} />
|
|
1238
|
+
</button>
|
|
1239
|
+
</div>
|
|
1060
1240
|
</div>
|
|
1061
1241
|
{/* Panel body */}
|
|
1062
1242
|
<div className="flex-1 overflow-y-auto">
|