@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.
Files changed (42) hide show
  1. package/dist/components/event-list-view.d.ts.map +1 -1
  2. package/dist/components/event-list-view.js +34 -2
  3. package/dist/components/event-list-view.js.map +1 -1
  4. package/dist/components/run-trace-view.d.ts +4 -1
  5. package/dist/components/run-trace-view.d.ts.map +1 -1
  6. package/dist/components/run-trace-view.js +2 -2
  7. package/dist/components/run-trace-view.js.map +1 -1
  8. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  9. package/dist/components/sidebar/attribute-panel.js +122 -45
  10. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  11. package/dist/components/sidebar/copyable-data-block.d.ts +4 -0
  12. package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -0
  13. package/dist/components/sidebar/copyable-data-block.js +33 -0
  14. package/dist/components/sidebar/copyable-data-block.js.map +1 -0
  15. package/dist/components/sidebar/detail-card.d.ts +7 -1
  16. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  17. package/dist/components/sidebar/detail-card.js +12 -3
  18. package/dist/components/sidebar/detail-card.js.map +1 -1
  19. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  20. package/dist/components/sidebar/entity-detail-panel.js +30 -16
  21. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  22. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  23. package/dist/components/sidebar/events-list.js +37 -7
  24. package/dist/components/sidebar/events-list.js.map +1 -1
  25. package/dist/components/ui/error-stack-block.d.ts +19 -0
  26. package/dist/components/ui/error-stack-block.d.ts.map +1 -0
  27. package/dist/components/ui/error-stack-block.js +39 -0
  28. package/dist/components/ui/error-stack-block.js.map +1 -0
  29. package/dist/components/workflow-trace-view.d.ts +7 -1
  30. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  31. package/dist/components/workflow-trace-view.js +137 -24
  32. package/dist/components/workflow-trace-view.js.map +1 -1
  33. package/package.json +10 -10
  34. package/src/components/event-list-view.tsx +53 -2
  35. package/src/components/run-trace-view.tsx +9 -0
  36. package/src/components/sidebar/attribute-panel.tsx +285 -127
  37. package/src/components/sidebar/copyable-data-block.tsx +51 -0
  38. package/src/components/sidebar/detail-card.tsx +28 -2
  39. package/src/components/sidebar/entity-detail-panel.tsx +138 -81
  40. package/src/components/sidebar/events-list.tsx +72 -21
  41. package/src/components/ui/error-stack-block.tsx +80 -0
  42. 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 { Clock, Copy, Info, Send, Type, X, XCircle } from 'lucide-react';
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 = 380;
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-b flex-shrink-0"
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
- <button
1034
- type="button"
1035
- aria-label="Close panel"
1036
- onClick={handleClose}
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
- display: 'flex',
1039
- alignItems: 'center',
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
- <X size={16} />
1059
- </button>
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">