@workflow/web-shared 4.1.0-beta.62 → 4.1.0-beta.64

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 (70) hide show
  1. package/README.md +4 -0
  2. package/dist/components/event-list-view.d.ts +9 -3
  3. package/dist/components/event-list-view.d.ts.map +1 -1
  4. package/dist/components/event-list-view.js +222 -98
  5. package/dist/components/event-list-view.js.map +1 -1
  6. package/dist/components/index.d.ts +1 -0
  7. package/dist/components/index.d.ts.map +1 -1
  8. package/dist/components/index.js +1 -0
  9. package/dist/components/index.js.map +1 -1
  10. package/dist/components/run-trace-view.d.ts +1 -3
  11. package/dist/components/run-trace-view.d.ts.map +1 -1
  12. package/dist/components/run-trace-view.js +2 -2
  13. package/dist/components/run-trace-view.js.map +1 -1
  14. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  15. package/dist/components/sidebar/attribute-panel.js +11 -1
  16. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  17. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  18. package/dist/components/sidebar/detail-card.js +4 -2
  19. package/dist/components/sidebar/detail-card.js.map +1 -1
  20. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -3
  21. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  22. package/dist/components/sidebar/entity-detail-panel.js +43 -26
  23. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  24. package/dist/components/trace-viewer/trace-viewer.d.ts +7 -1
  25. package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -1
  26. package/dist/components/trace-viewer/trace-viewer.js +36 -11
  27. package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
  28. package/dist/components/ui/error-stack-block.d.ts +3 -4
  29. package/dist/components/ui/error-stack-block.d.ts.map +1 -1
  30. package/dist/components/ui/error-stack-block.js +18 -9
  31. package/dist/components/ui/error-stack-block.js.map +1 -1
  32. package/dist/components/ui/menu-dropdown.d.ts +16 -0
  33. package/dist/components/ui/menu-dropdown.d.ts.map +1 -0
  34. package/dist/components/ui/menu-dropdown.js +50 -0
  35. package/dist/components/ui/menu-dropdown.js.map +1 -0
  36. package/dist/components/workflow-trace-view.d.ts +3 -3
  37. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  38. package/dist/components/workflow-trace-view.js +31 -129
  39. package/dist/components/workflow-trace-view.js.map +1 -1
  40. package/dist/components/workflow-traces/trace-span-construction.d.ts +18 -5
  41. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  42. package/dist/components/workflow-traces/trace-span-construction.js +65 -18
  43. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  44. package/dist/index.d.ts +3 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/lib/event-materialization.d.ts +72 -0
  49. package/dist/lib/event-materialization.d.ts.map +1 -0
  50. package/dist/lib/event-materialization.js +171 -0
  51. package/dist/lib/event-materialization.js.map +1 -0
  52. package/dist/lib/trace-builder.d.ts +32 -0
  53. package/dist/lib/trace-builder.d.ts.map +1 -0
  54. package/dist/lib/trace-builder.js +129 -0
  55. package/dist/lib/trace-builder.js.map +1 -0
  56. package/package.json +3 -3
  57. package/src/components/event-list-view.tsx +324 -103
  58. package/src/components/index.ts +1 -0
  59. package/src/components/run-trace-view.tsx +0 -6
  60. package/src/components/sidebar/attribute-panel.tsx +17 -2
  61. package/src/components/sidebar/detail-card.tsx +10 -2
  62. package/src/components/sidebar/entity-detail-panel.tsx +59 -21
  63. package/src/components/trace-viewer/trace-viewer.tsx +47 -2
  64. package/src/components/ui/error-stack-block.tsx +26 -16
  65. package/src/components/ui/menu-dropdown.tsx +114 -0
  66. package/src/components/workflow-trace-view.tsx +95 -195
  67. package/src/components/workflow-traces/trace-span-construction.ts +85 -32
  68. package/src/index.ts +13 -0
  69. package/src/lib/event-materialization.ts +243 -0
  70. package/src/lib/trace-builder.ts +201 -0
package/README.md CHANGED
@@ -37,6 +37,10 @@ export default function MyRunDetailView({
37
37
  Server actions and data fetching are intentionally **not** part of `web-shared`. Implement those in your app
38
38
  and pass data + callbacks into these components. If you need world run helpers, use `@workflow/core/runtime`.
39
39
 
40
+ > **Security notice:** If you implement server-side data fetching using `@workflow/world-vercel` or similar backends,
41
+ > ensure that user-supplied IDs (runId, stepId, etc.) are validated before passing them to world functions.
42
+ > The server actions in `@workflow/web` do not include authentication — see that package's README for details on securing self-hosted deployments.
43
+
40
44
  ## Styling
41
45
 
42
46
  In order for tailwind classes to be picked up correctly, you might need to configure your NextJS app
@@ -1,7 +1,6 @@
1
- import type { Event, Step, WorkflowRun } from '@workflow/world';
1
+ import type { Event, WorkflowRun } from '@workflow/world';
2
2
  interface EventsListProps {
3
3
  events: Event[] | null;
4
- steps?: Step[] | null;
5
4
  run?: WorkflowRun | null;
6
5
  onLoadEventData?: (event: Event) => Promise<unknown | null>;
7
6
  hasMoreEvents?: boolean;
@@ -9,7 +8,14 @@ interface EventsListProps {
9
8
  onLoadMoreEvents?: () => Promise<void> | void;
10
9
  /** When provided, signals that decryption is active (triggers re-load of expanded events) */
11
10
  encryptionKey?: Uint8Array;
11
+ /** When true, shows a loading state instead of "No events found" for empty lists */
12
+ isLoading?: boolean;
13
+ /** Sort order for events. Defaults to 'asc'. */
14
+ sortOrder?: 'asc' | 'desc';
15
+ /** Called when the user changes sort order. When provided, the sort dropdown is shown
16
+ * and the parent is expected to refetch from the API with the new order. */
17
+ onSortOrderChange?: (order: 'asc' | 'desc') => void;
12
18
  }
13
- export declare function EventListView({ events, steps, run, onLoadEventData, hasMoreEvents, isLoadingMoreEvents, onLoadMoreEvents, encryptionKey, }: EventsListProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function EventListView({ events, run, onLoadEventData, hasMoreEvents, isLoadingMoreEvents, onLoadMoreEvents, encryptionKey, isLoading, sortOrder: sortOrderProp, onSortOrderChange, }: EventsListProps): import("react/jsx-runtime").JSX.Element;
14
20
  export {};
15
21
  //# sourceMappingURL=event-list-view.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"event-list-view.d.ts","sourceRoot":"","sources":["../../src/components/event-list-view.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAokBhE,UAAU,eAAe;IACvB,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACvB,KAAK,CAAC,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;IACtB,GAAG,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC5D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9C,6FAA6F;IAC7F,aAAa,CAAC,EAAE,UAAU,CAAC;CAC5B;AA+WD,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,KAAK,EACL,GAAG,EACH,eAAe,EACf,aAAqB,EACrB,mBAA2B,EAC3B,gBAAgB,EAChB,aAAa,GACd,EAAE,eAAe,2CAoQjB"}
1
+ {"version":3,"file":"event-list-view.d.ts","sourceRoot":"","sources":["../../src/components/event-list-view.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAulB1D,UAAU,eAAe;IACvB,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACvB,GAAG,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC5D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9C,6FAA6F;IAC7F,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,oFAAoF;IACpF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gDAAgD;IAChD,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC3B;iFAC6E;IAC7E,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,KAAK,IAAI,CAAC;CACrD;AAyYD,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,GAAG,EACH,eAAe,EACf,aAAqB,EACrB,mBAA2B,EAC3B,gBAAgB,EAChB,aAAa,EACb,SAAiB,EACjB,SAAS,EAAE,aAAa,EACxB,iBAAiB,GAClB,EAAE,eAAe,2CA4ajB"}
@@ -7,6 +7,7 @@ import { Virtuoso } from 'react-virtuoso';
7
7
  import { formatDuration } from '../lib/utils';
8
8
  import { DataInspector } from './ui/data-inspector';
9
9
  import { ErrorStackBlock, isStructuredErrorWithStack, } from './ui/error-stack-block';
10
+ import { MenuDropdown } from './ui/menu-dropdown';
10
11
  import { Skeleton } from './ui/skeleton';
11
12
  /**
12
13
  * Event types whose eventData contains an error field with a StructuredError.
@@ -81,16 +82,19 @@ function getStatusDotColor(eventType) {
81
82
  return 'var(--ds-gray-600)';
82
83
  }
83
84
  /**
84
- * Build a map from correlationId (stepId) → display name using step entities,
85
- * and parse the workflow name from the run.
85
+ * Build a map from correlationId (stepId) → display name using step_created
86
+ * events, and parse the workflow name from the run.
86
87
  */
87
- function buildNameMaps(steps, run) {
88
+ function buildNameMaps(events, run) {
88
89
  const correlationNameMap = new Map();
89
- // Map step correlationId (= stepId) → parsed step name
90
- if (steps) {
91
- for (const step of steps) {
92
- const parsed = parseStepName(String(step.stepName));
93
- correlationNameMap.set(step.stepId, parsed?.shortName ?? step.stepName);
90
+ // Map step correlationId (= stepId) → parsed step name from step_created events
91
+ if (events) {
92
+ for (const event of events) {
93
+ if (event.eventType === 'step_created' && event.correlationId) {
94
+ const stepName = event.eventData?.stepName ?? '';
95
+ const parsed = parseStepName(String(stepName));
96
+ correlationNameMap.set(event.correlationId, parsed?.shortName ?? stepName);
97
+ }
94
98
  }
95
99
  }
96
100
  // Parse workflow name from run
@@ -237,7 +241,7 @@ function TreeGutter({ isFirst, isLast, isRunLevel: isRun, statusDotColor, pulse
237
241
  // ──────────────────────────────────────────────────────────────────────────
238
242
  // Copyable cell — shows a copy button on hover
239
243
  // ──────────────────────────────────────────────────────────────────────────
240
- function CopyableCell({ value, className, }) {
244
+ function CopyableCell({ value, className, style: styleProp, }) {
241
245
  const [copied, setCopied] = useState(false);
242
246
  const resetCopiedTimeoutRef = useRef(null);
243
247
  useEffect(() => {
@@ -260,7 +264,7 @@ function CopyableCell({ value, className, }) {
260
264
  }, 1500);
261
265
  });
262
266
  }, [value]);
263
- return (_jsxs("div", { className: `group/copy flex items-center gap-1 flex-1 min-w-0 px-4 ${className ?? ''}`, children: [_jsx("span", { className: "overflow-hidden text-ellipsis whitespace-nowrap", children: value || '-' }), value ? (_jsx("button", { type: "button", onClick: handleCopy, className: "flex-shrink-0 opacity-0 group-hover/copy:opacity-100 transition-opacity p-0.5 rounded hover:bg-[var(--ds-gray-alpha-200)]", style: BUTTON_RESET_STYLE, "aria-label": `Copy ${value}`, children: copied ? (_jsx(Check, { className: "h-3 w-3", style: { color: 'var(--ds-green-700)' } })) : (_jsx(Copy, { className: "h-3 w-3", style: { color: 'var(--ds-gray-700)' } })) })) : null] }));
267
+ return (_jsxs("div", { className: `group/copy flex items-center gap-1 min-w-0 px-4 ${className ?? ''}`, style: styleProp, children: [_jsx("span", { className: "overflow-hidden text-ellipsis whitespace-nowrap", children: value || '-' }), value ? (_jsx("button", { type: "button", onClick: handleCopy, className: "flex-shrink-0 opacity-0 group-hover/copy:opacity-100 transition-opacity p-0.5 rounded hover:bg-[var(--ds-gray-alpha-200)]", style: BUTTON_RESET_STYLE, "aria-label": `Copy ${value}`, children: copied ? (_jsx(Check, { className: "h-3 w-3", style: { color: 'var(--ds-green-700)' } })) : (_jsx(Copy, { className: "h-3 w-3", style: { color: 'var(--ds-gray-700)' } })) })) : null] }));
264
268
  }
265
269
  /** Recursively parse stringified JSON values so escaped slashes / quotes are cleaned up */
266
270
  function deepParseJson(value) {
@@ -351,20 +355,21 @@ function PayloadBlock({ data, eventType, }) {
351
355
  }
352
356
  return (_jsxs("div", { className: "relative group/payload", children: [_jsx("div", { className: "overflow-x-auto p-2 text-[11px]", style: { color: 'var(--ds-gray-1000)' }, children: _jsx(DataInspector, { data: cleaned, expandLevel: 2 }) }), _jsx("button", { type: "button", onClick: handleCopy, className: "absolute bottom-2 right-2 opacity-0 group-hover/payload:opacity-100 transition-opacity flex items-center gap-1 px-2 py-1 rounded-md text-xs hover:bg-[var(--ds-gray-alpha-200)]", style: { ...BUTTON_RESET_STYLE, color: 'var(--ds-gray-700)' }, "aria-label": "Copy payload", children: copied ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-3 w-3", style: { color: 'var(--ds-green-700)' } }), _jsx("span", { style: { color: 'var(--ds-green-700)' }, children: "Copied" })] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "h-3 w-3" }), _jsx("span", { children: "Copy" })] })) })] }));
353
357
  }
354
- function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroupKey, selectedGroupRange, correlationNameMap, workflowName, durationMap, onSelectGroup, onHoverGroup, onLoadEventData, encryptionKey, }) {
355
- const [isExpanded, setIsExpanded] = useState(false);
358
+ // ──────────────────────────────────────────────────────────────────────────
359
+ // Sort options for the events list
360
+ // ──────────────────────────────────────────────────────────────────────────
361
+ const SORT_OPTIONS = [
362
+ { value: 'desc', label: 'Newest' },
363
+ { value: 'asc', label: 'Oldest' },
364
+ ];
365
+ function EventRow({ event, index, isFirst, isLast, isExpanded, onToggleExpand, activeGroupKey, selectedGroupKey, selectedGroupRange, correlationNameMap, workflowName, durationMap, onSelectGroup, onHoverGroup, onLoadEventData, cachedEventData, onCacheEventData, encryptionKey, }) {
356
366
  const [isLoading, setIsLoading] = useState(false);
357
- const [loadedEventData, setLoadedEventData] = useState(null);
367
+ const [loadedEventData, setLoadedEventData] = useState(cachedEventData);
358
368
  const [loadError, setLoadError] = useState(null);
359
- const [hasAttemptedLoad, setHasAttemptedLoad] = useState(false);
360
- const rowGroupKey = event.correlationId ??
361
- (isRunLevel(event.eventType) ? '__run__' : undefined);
362
- // Collapse when a different group gets selected
363
- useEffect(() => {
364
- if (selectedGroupKey !== undefined && selectedGroupKey !== rowGroupKey) {
365
- setIsExpanded(false);
366
- }
367
- }, [selectedGroupKey, rowGroupKey]);
369
+ const [hasAttemptedLoad, setHasAttemptedLoad] = useState(cachedEventData !== null);
370
+ const rowGroupKey = isRunLevel(event.eventType)
371
+ ? '__run__'
372
+ : (event.correlationId ?? undefined);
368
373
  const statusDotColor = getStatusDotColor(event.eventType);
369
374
  const createdAt = new Date(event.createdAt);
370
375
  const hasExistingEventData = 'eventData' in event && event.eventData != null;
@@ -398,9 +403,10 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
398
403
  setLoadError('Event details unavailable');
399
404
  return;
400
405
  }
401
- const eventData = await onLoadEventData(event);
402
- if (eventData !== null && eventData !== undefined) {
403
- setLoadedEventData(eventData);
406
+ const data = await onLoadEventData(event);
407
+ if (data !== null && data !== undefined) {
408
+ setLoadedEventData(data);
409
+ onCacheEventData(event.eventId, data);
404
410
  }
405
411
  }
406
412
  catch (err) {
@@ -410,7 +416,24 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
410
416
  setIsLoading(false);
411
417
  setHasAttemptedLoad(true);
412
418
  }
413
- }, [event, loadedEventData, hasExistingEventData, onLoadEventData]);
419
+ }, [
420
+ event,
421
+ loadedEventData,
422
+ hasExistingEventData,
423
+ onLoadEventData,
424
+ onCacheEventData,
425
+ ]);
426
+ // Auto-load event data when remounting in expanded state without cached data
427
+ useEffect(() => {
428
+ if (isExpanded &&
429
+ loadedEventData === null &&
430
+ !hasExistingEventData &&
431
+ !isLoading &&
432
+ !hasAttemptedLoad) {
433
+ loadEventDetails();
434
+ }
435
+ // eslint-disable-next-line react-hooks/exhaustive-deps
436
+ }, []);
414
437
  // When encryption key changes and this event was previously loaded,
415
438
  // re-load to get decrypted data
416
439
  useEffect(() => {
@@ -421,6 +444,7 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
421
444
  .then((data) => {
422
445
  if (data !== null && data !== undefined) {
423
446
  setLoadedEventData(data);
447
+ onCacheEventData(event.eventId, data);
424
448
  }
425
449
  setHasAttemptedLoad(true);
426
450
  })
@@ -430,22 +454,23 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
430
454
  }
431
455
  // eslint-disable-next-line react-hooks/exhaustive-deps
432
456
  }, [encryptionKey]);
433
- const handleExpandToggle = useCallback((e) => {
434
- e.stopPropagation();
435
- const newExpanded = !isExpanded;
436
- setIsExpanded(newExpanded);
437
- if (newExpanded && loadedEventData === null && !hasExistingEventData) {
438
- loadEventDetails();
439
- }
440
- }, [isExpanded, loadedEventData, hasExistingEventData, loadEventDetails]);
441
457
  const handleRowClick = useCallback(() => {
442
- if (selectedGroupKey === rowGroupKey) {
443
- onSelectGroup(undefined);
444
- }
445
- else {
446
- onSelectGroup(rowGroupKey);
458
+ onSelectGroup(rowGroupKey === selectedGroupKey ? undefined : rowGroupKey);
459
+ onToggleExpand(event.eventId);
460
+ if (!isExpanded && loadedEventData === null && !hasExistingEventData) {
461
+ loadEventDetails();
447
462
  }
448
- }, [selectedGroupKey, rowGroupKey, onSelectGroup]);
463
+ }, [
464
+ selectedGroupKey,
465
+ rowGroupKey,
466
+ onSelectGroup,
467
+ onToggleExpand,
468
+ event.eventId,
469
+ isExpanded,
470
+ loadedEventData,
471
+ hasExistingEventData,
472
+ loadEventDetails,
473
+ ]);
449
474
  const eventData = hasExistingEventData
450
475
  ? event.eventData
451
476
  : loadedEventData;
@@ -453,13 +478,12 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
453
478
  return (_jsxs("div", { "data-event-id": event.eventId, onMouseEnter: () => onHoverGroup(rowGroupKey), onMouseLeave: () => onHoverGroup(undefined), children: [_jsxs("div", { role: "button", tabIndex: 0, onClick: handleRowClick, onKeyDown: (e) => {
454
479
  if (e.key === 'Enter' || e.key === ' ')
455
480
  handleRowClick();
456
- }, className: "w-full text-left flex items-center gap-0 text-sm hover:bg-[var(--ds-gray-alpha-100)] transition-colors cursor-pointer", style: { minHeight: 40 }, children: [_jsx(TreeGutter, { isFirst: isFirst, isLast: isLast && !isExpanded, isRunLevel: isRun, statusDotColor: statusDotColor, pulse: isPulsing, hasSelection: hasActive, showBranch: showBranch, showLaneLine: showLaneLine, isLaneStart: isLaneStart, isLaneEnd: isLaneEnd }), _jsxs("div", { className: "flex items-center flex-1 min-w-0", style: { opacity: contentOpacity, transition: 'opacity 150ms' }, children: [_jsx("button", { type: "button", onClick: handleExpandToggle, className: "flex items-center justify-center w-5 h-5 flex-shrink-0 rounded hover:bg-[var(--ds-gray-alpha-200)] transition-colors", style: {
457
- ...BUTTON_RESET_STYLE,
458
- border: '1px solid var(--ds-gray-alpha-400)',
459
- }, "aria-label": isExpanded ? 'Collapse details' : 'Expand details', children: _jsx(ChevronRight, { className: "h-3 w-3 transition-transform", style: {
460
- color: 'var(--ds-gray-700)',
481
+ }, className: "w-full text-left flex items-center gap-0 text-[13px] hover:bg-[var(--ds-gray-alpha-100)] transition-colors cursor-pointer", style: { minHeight: 40 }, children: [_jsx(TreeGutter, { isFirst: isFirst, isLast: isLast && !isExpanded, isRunLevel: isRun, statusDotColor: statusDotColor, pulse: isPulsing, hasSelection: hasActive, showBranch: showBranch, showLaneLine: showLaneLine, isLaneStart: isLaneStart, isLaneEnd: isLaneEnd }), _jsxs("div", { className: "flex items-center flex-1 min-w-0", style: { opacity: contentOpacity, transition: 'opacity 150ms' }, children: [_jsx("div", { className: "flex items-center justify-center w-5 h-5 flex-shrink-0 rounded", style: {
482
+ border: '1px solid var(--ds-gray-400)',
483
+ }, children: _jsx(ChevronRight, { className: "h-3 w-3 transition-transform", style: {
484
+ color: 'var(--ds-gray-900)',
461
485
  transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
462
- } }) }), _jsx("div", { className: "text-xs tabular-nums flex-1 min-w-0 px-4", style: { color: 'var(--ds-gray-900)' }, children: formatEventTime(createdAt) }), _jsx("div", { className: "text-xs font-medium flex-1 min-w-0 px-4", children: _jsxs("span", { className: "inline-flex items-center gap-1.5", style: { color: 'var(--ds-gray-900)' }, children: [_jsxs("span", { style: {
486
+ } }) }), _jsx("div", { className: "tabular-nums min-w-0 px-4", style: { color: 'var(--ds-gray-900)', flex: '2 1 0%' }, children: formatEventTime(createdAt) }), _jsx("div", { className: "font-medium min-w-0 px-4", style: { flex: '2 1 0%' }, children: _jsxs("span", { className: "inline-flex items-center gap-1.5", style: { color: 'var(--ds-gray-900)' }, children: [_jsxs("span", { style: {
463
487
  position: 'relative',
464
488
  display: 'inline-flex',
465
489
  width: 6,
@@ -478,7 +502,7 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
478
502
  height: 6,
479
503
  borderRadius: '50%',
480
504
  backgroundColor: statusDotColor,
481
- } })] }), formatEventType(event.eventType)] }) }), _jsx("div", { className: "text-xs flex-1 min-w-0 px-4 overflow-hidden text-ellipsis whitespace-nowrap", title: eventName !== '-' ? eventName : undefined, children: eventName }), _jsx(CopyableCell, { value: event.correlationId || '', className: "font-mono text-xs" }), _jsx(CopyableCell, { value: event.eventId, className: "font-mono text-xs" })] })] }), isExpanded && (_jsxs("div", { className: "flex", children: [_jsx(TreeGutter, { isFirst: false, isLast: isLast, isRunLevel: isRun, hasSelection: hasActive, showBranch: false, showLaneLine: showLaneLine && !isLaneEnd, isLaneStart: false, isLaneEnd: false, continuationOnly: true }), _jsx("div", { className: "w-5 flex-shrink-0" }), _jsxs("div", { className: "flex-1 my-1.5 mr-3 ml-2 py-2 rounded-md border overflow-hidden", style: {
505
+ } })] }), formatEventType(event.eventType)] }) }), _jsx("div", { className: "min-w-0 px-4 overflow-hidden text-ellipsis whitespace-nowrap", style: { flex: '2 1 0%' }, title: eventName !== '-' ? eventName : undefined, children: eventName }), _jsx(CopyableCell, { value: event.correlationId || '', className: "font-mono", style: { flex: '3 1 0%' } }), _jsx(CopyableCell, { value: event.eventId, className: "font-mono", style: { flex: '3 1 0%' } })] })] }), isExpanded && (_jsxs("div", { className: "flex", children: [_jsx(TreeGutter, { isFirst: false, isLast: isLast, isRunLevel: isRun, hasSelection: hasActive, showBranch: false, showLaneLine: showLaneLine && !isLaneEnd, isLaneStart: false, isLaneEnd: false, continuationOnly: true }), _jsx("div", { className: "w-5 flex-shrink-0" }), _jsxs("div", { className: "flex-1 my-1.5 mr-3 ml-2 py-2 rounded-md border overflow-hidden", style: {
482
506
  borderColor: 'var(--ds-gray-alpha-200)',
483
507
  opacity: contentOpacity,
484
508
  transition: 'opacity 150ms',
@@ -496,13 +520,25 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
496
520
  // ──────────────────────────────────────────────────────────────────────────
497
521
  // Main component
498
522
  // ──────────────────────────────────────────────────────────────────────────
499
- export function EventListView({ events, steps, run, onLoadEventData, hasMoreEvents = false, isLoadingMoreEvents = false, onLoadMoreEvents, encryptionKey, }) {
523
+ export function EventListView({ events, run, onLoadEventData, hasMoreEvents = false, isLoadingMoreEvents = false, onLoadMoreEvents, encryptionKey, isLoading = false, sortOrder: sortOrderProp, onSortOrderChange, }) {
524
+ const [internalSortOrder, setInternalSortOrder] = useState('asc');
525
+ const effectiveSortOrder = sortOrderProp ?? internalSortOrder;
526
+ const handleSortOrderChange = useCallback((order) => {
527
+ if (onSortOrderChange) {
528
+ onSortOrderChange(order);
529
+ }
530
+ else {
531
+ setInternalSortOrder(order);
532
+ }
533
+ }, [onSortOrderChange]);
500
534
  const sortedEvents = useMemo(() => {
501
535
  if (!events || events.length === 0)
502
536
  return [];
503
- return [...events].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
504
- }, [events]);
505
- const { correlationNameMap, workflowName } = useMemo(() => buildNameMaps(steps ?? null, run ?? null), [steps, run]);
537
+ const dir = effectiveSortOrder === 'desc' ? -1 : 1;
538
+ return [...events].sort((a, b) => dir *
539
+ (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()));
540
+ }, [events, effectiveSortOrder]);
541
+ const { correlationNameMap, workflowName } = useMemo(() => buildNameMaps(events ?? null, run ?? null), [events, run]);
506
542
  const durationMap = useMemo(() => buildDurationMap(sortedEvents), [sortedEvents]);
507
543
  const [selectedGroupKey, setSelectedGroupKey] = useState(undefined);
508
544
  const [hoveredGroupKey, setHoveredGroupKey] = useState(undefined);
@@ -513,6 +549,57 @@ export function EventListView({ events, steps, run, onLoadEventData, hasMoreEven
513
549
  setHoveredGroupKey(groupKey);
514
550
  }, []);
515
551
  const activeGroupKey = selectedGroupKey ?? hoveredGroupKey;
552
+ // Expanded state lifted out of EventRow so it survives virtualization
553
+ const [expandedEventIds, setExpandedEventIds] = useState(() => new Set());
554
+ const toggleEventExpanded = useCallback((eventId) => {
555
+ setExpandedEventIds((prev) => {
556
+ const next = new Set(prev);
557
+ if (next.has(eventId)) {
558
+ next.delete(eventId);
559
+ }
560
+ else {
561
+ next.add(eventId);
562
+ }
563
+ return next;
564
+ });
565
+ }, []);
566
+ // Event data cache — ref avoids re-renders when cache updates
567
+ const eventDataCacheRef = useRef(new Map());
568
+ const cacheEventData = useCallback((eventId, data) => {
569
+ eventDataCacheRef.current.set(eventId, data);
570
+ }, []);
571
+ // Lookup from eventId → groupKey for efficient collapse filtering
572
+ const eventGroupKeyMap = useMemo(() => {
573
+ const map = new Map();
574
+ for (const ev of sortedEvents) {
575
+ const gk = isRunLevel(ev.eventType)
576
+ ? '__run__'
577
+ : (ev.correlationId ?? '');
578
+ if (gk)
579
+ map.set(ev.eventId, gk);
580
+ }
581
+ return map;
582
+ }, [sortedEvents]);
583
+ // Collapse expanded events that don't belong to the newly selected group
584
+ useEffect(() => {
585
+ if (selectedGroupKey === undefined)
586
+ return;
587
+ setExpandedEventIds((prev) => {
588
+ if (prev.size === 0)
589
+ return prev;
590
+ let changed = false;
591
+ const next = new Set();
592
+ for (const eventId of prev) {
593
+ if (eventGroupKeyMap.get(eventId) === selectedGroupKey) {
594
+ next.add(eventId);
595
+ }
596
+ else {
597
+ changed = true;
598
+ }
599
+ }
600
+ return changed ? next : prev;
601
+ });
602
+ }, [selectedGroupKey, eventGroupKeyMap]);
516
603
  // Compute the row-index range for the active group's connecting lane line.
517
604
  // Only applies to non-run groups (step/hook/wait correlations).
518
605
  const selectedGroupRange = useMemo(() => {
@@ -535,83 +622,120 @@ export function EventListView({ events, steps, run, onLoadEventData, hasMoreEven
535
622
  const entries = [];
536
623
  for (let i = 0; i < sortedEvents.length; i++) {
537
624
  const ev = sortedEvents[i];
625
+ const isRun = isRunLevel(ev.eventType);
626
+ const name = isRun
627
+ ? (workflowName ?? '')
628
+ : ev.correlationId
629
+ ? (correlationNameMap.get(ev.correlationId) ?? '')
630
+ : '';
538
631
  entries.push({
539
- text: [ev.eventId, ev.correlationId ?? ''].join(' ').toLowerCase(),
540
- groupKey: ev.correlationId ??
541
- (isRunLevel(ev.eventType) ? '__run__' : undefined),
632
+ fields: [
633
+ ev.eventId,
634
+ ev.correlationId ?? '',
635
+ ev.eventType,
636
+ formatEventType(ev.eventType),
637
+ name,
638
+ ].map((f) => f.toLowerCase()),
639
+ groupKey: ev.correlationId ?? (isRun ? '__run__' : undefined),
542
640
  eventId: ev.eventId,
543
641
  index: i,
544
642
  });
545
643
  }
546
644
  return entries;
547
- }, [sortedEvents]);
645
+ }, [sortedEvents, correlationNameMap, workflowName]);
548
646
  useEffect(() => {
549
647
  const q = searchQuery.trim().toLowerCase();
550
648
  if (!q) {
551
649
  setSelectedGroupKey(undefined);
552
650
  return;
553
651
  }
554
- const match = searchIndex.find((entry) => entry.text.includes(q));
555
- if (match) {
556
- setSelectedGroupKey(match.groupKey);
652
+ let bestMatch = null;
653
+ let bestScore = 0;
654
+ for (const entry of searchIndex) {
655
+ for (const field of entry.fields) {
656
+ if (field && field.includes(q)) {
657
+ const score = q.length / field.length;
658
+ if (score > bestScore) {
659
+ bestScore = score;
660
+ bestMatch = entry;
661
+ }
662
+ }
663
+ }
664
+ }
665
+ if (bestMatch) {
666
+ setSelectedGroupKey(bestMatch.groupKey);
557
667
  virtuosoRef.current?.scrollToIndex({
558
- index: match.index,
668
+ index: bestMatch.index,
559
669
  align: 'center',
560
670
  behavior: 'smooth',
561
671
  });
562
672
  }
563
673
  }, [searchQuery, searchIndex]);
564
674
  if (!events || events.length === 0) {
675
+ if (isLoading) {
676
+ return (_jsxs("div", { className: "h-full flex flex-col overflow-hidden", children: [_jsx("div", { style: { padding: 6 }, children: _jsx(Skeleton, { style: { height: 40, borderRadius: 6 } }) }), _jsxs("div", { className: "flex items-center gap-0 h-10 border-b flex-shrink-0 px-4", style: { borderColor: 'var(--ds-gray-alpha-200)' }, children: [_jsx(Skeleton, { className: "h-3", style: { width: 60 } }), _jsx("div", { style: { flex: 1 } }), _jsx(Skeleton, { className: "h-3", style: { width: 80 } }), _jsx("div", { style: { flex: 1 } }), _jsx(Skeleton, { className: "h-3", style: { width: 50 } }), _jsx("div", { style: { flex: 1 } }), _jsx(Skeleton, { className: "h-3", style: { width: 90 } }), _jsx("div", { style: { flex: 1 } }), _jsx(Skeleton, { className: "h-3", style: { width: 70 } })] }), _jsx("div", { className: "flex-1 overflow-hidden", children: Array.from({ length: 8 }, (_, i) => (_jsxs("div", { className: "flex items-center gap-3 px-4", style: { height: 40 }, children: [_jsx(Skeleton, { className: "h-2 w-2 flex-shrink-0", style: { borderRadius: '50%' } }), _jsx(Skeleton, { className: "h-3", style: { width: 90 } }), _jsx(Skeleton, { className: "h-3", style: { width: 100 } }), _jsx(Skeleton, { className: "h-3", style: { width: 80 } }), _jsx(Skeleton, { className: "h-3 flex-1" }), _jsx(Skeleton, { className: "h-3 flex-1" })] }, i))) })] }));
677
+ }
565
678
  return (_jsx("div", { className: "flex items-center justify-center h-full text-sm", style: { color: 'var(--ds-gray-700)' }, children: "No events found" }));
566
679
  }
567
- return (_jsxs("div", { className: "h-full flex flex-col overflow-hidden", children: [_jsx("style", { children: `@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}` }), _jsx("div", { style: { padding: 6, backgroundColor: 'var(--ds-background-100)' }, children: _jsxs("label", { style: {
568
- display: 'flex',
569
- alignItems: 'center',
570
- justifyContent: 'center',
571
- borderRadius: 6,
572
- boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
573
- background: 'var(--ds-background-100)',
574
- height: 40,
575
- }, children: [_jsx("div", { style: {
576
- width: 40,
577
- height: 40,
578
- display: 'flex',
579
- alignItems: 'center',
580
- justifyContent: 'center',
581
- color: 'var(--ds-gray-800)',
582
- flexShrink: 0,
583
- }, children: _jsxs("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", focusable: "false", children: [_jsx("circle", { cx: "7", cy: "7", r: "4.5", stroke: "currentColor", strokeWidth: "1.5" }), _jsx("path", { d: "M11.5 11.5L14 14", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })] }) }), _jsx("input", { type: "search", placeholder: "Search by event ID or correlation ID\u2026", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), style: {
584
- marginLeft: -16,
585
- paddingInline: 12,
586
- fontFamily: 'inherit',
587
- fontSize: 14,
588
- background: 'transparent',
589
- border: 'none',
590
- outline: 'none',
591
- height: 40,
592
- width: '100%',
593
- } })] }) }), _jsxs("div", { className: "flex items-center gap-0 text-sm font-medium h-10 border-b flex-shrink-0", style: {
680
+ return (_jsxs("div", { className: "h-full flex flex-col overflow-hidden", children: [_jsx("style", { children: `@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}` }), _jsxs("div", { style: {
681
+ padding: 6,
682
+ backgroundColor: 'var(--ds-background-100)',
683
+ display: 'flex',
684
+ gap: 6,
685
+ }, children: [_jsxs("label", { style: {
686
+ display: 'flex',
687
+ alignItems: 'center',
688
+ justifyContent: 'center',
689
+ borderRadius: 6,
690
+ boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
691
+ background: 'var(--ds-background-100)',
692
+ height: 40,
693
+ flex: 1,
694
+ minWidth: 0,
695
+ }, children: [_jsx("div", { style: {
696
+ width: 40,
697
+ height: 40,
698
+ display: 'flex',
699
+ alignItems: 'center',
700
+ justifyContent: 'center',
701
+ color: 'var(--ds-gray-800)',
702
+ flexShrink: 0,
703
+ }, children: _jsxs("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", focusable: "false", children: [_jsx("circle", { cx: "7", cy: "7", r: "4.5", stroke: "currentColor", strokeWidth: "1.5" }), _jsx("path", { d: "M11.5 11.5L14 14", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })] }) }), _jsx("input", { type: "search", placeholder: "Search by name, event type, or ID\u2026", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), style: {
704
+ marginLeft: -16,
705
+ paddingInline: 12,
706
+ fontFamily: 'inherit',
707
+ fontSize: 14,
708
+ background: 'transparent',
709
+ border: 'none',
710
+ outline: 'none',
711
+ height: 40,
712
+ width: '100%',
713
+ } })] }), _jsx(MenuDropdown, { options: SORT_OPTIONS, value: effectiveSortOrder, onChange: handleSortOrderChange })] }), _jsxs("div", { className: "flex items-center gap-0 text-[13px] font-medium h-10 border-b flex-shrink-0", style: {
594
714
  borderColor: 'var(--ds-gray-alpha-200)',
595
715
  color: 'var(--ds-gray-900)',
596
716
  backgroundColor: 'var(--ds-background-100)',
597
- }, children: [_jsx("div", { className: "flex-shrink-0", style: { width: GUTTER_WIDTH } }), _jsx("div", { className: "w-5 flex-shrink-0" }), _jsx("div", { className: "flex-1 min-w-0 px-4", children: "Time" }), _jsx("div", { className: "flex-1 min-w-0 px-4", children: "Event Type" }), _jsx("div", { className: "flex-1 min-w-0 px-4", children: "Name" }), _jsx("div", { className: "flex-1 min-w-0 px-4", children: "Correlation ID" }), _jsx("div", { className: "flex-1 min-w-0 px-4", children: "Event ID" })] }), _jsx(Virtuoso, { ref: virtuosoRef, totalCount: sortedEvents.length, overscan: 20, defaultItemHeight: 40, endReached: () => {
717
+ }, children: [_jsx("div", { className: "flex-shrink-0", style: { width: GUTTER_WIDTH } }), _jsx("div", { className: "w-5 flex-shrink-0" }), _jsx("div", { className: "min-w-0 px-4", style: { flex: '2 1 0%' }, children: "Time" }), _jsx("div", { className: "min-w-0 px-4", style: { flex: '2 1 0%' }, children: "Event Type" }), _jsx("div", { className: "min-w-0 px-4", style: { flex: '2 1 0%' }, children: "Name" }), _jsx("div", { className: "min-w-0 px-4", style: { flex: '3 1 0%' }, children: "Correlation ID" }), _jsx("div", { className: "min-w-0 px-4", style: { flex: '3 1 0%' }, children: "Event ID" })] }), _jsx(Virtuoso, { ref: virtuosoRef, totalCount: sortedEvents.length, overscan: 20, defaultItemHeight: 40, endReached: () => {
598
718
  if (!hasMoreEvents || isLoadingMoreEvents) {
599
719
  return;
600
720
  }
601
721
  void onLoadMoreEvents?.();
602
722
  }, itemContent: (index) => {
603
- return (_jsx(EventRow, { event: sortedEvents[index], index: index, isFirst: index === 0, isLast: index === sortedEvents.length - 1, activeGroupKey: activeGroupKey, selectedGroupKey: selectedGroupKey, selectedGroupRange: selectedGroupRange, correlationNameMap: correlationNameMap, workflowName: workflowName, durationMap: durationMap, onSelectGroup: onSelectGroup, onHoverGroup: onHoverGroup, onLoadEventData: onLoadEventData, encryptionKey: encryptionKey }));
723
+ const ev = sortedEvents[index];
724
+ return (_jsx(EventRow, { event: ev, index: index, isFirst: index === 0, isLast: index === sortedEvents.length - 1, isExpanded: expandedEventIds.has(ev.eventId), onToggleExpand: toggleEventExpanded, activeGroupKey: activeGroupKey, selectedGroupKey: selectedGroupKey, selectedGroupRange: selectedGroupRange, correlationNameMap: correlationNameMap, workflowName: workflowName, durationMap: durationMap, onSelectGroup: onSelectGroup, onHoverGroup: onHoverGroup, onLoadEventData: onLoadEventData, cachedEventData: eventDataCacheRef.current.get(ev.eventId) ?? null, onCacheEventData: cacheEventData, encryptionKey: encryptionKey }));
604
725
  }, components: {
605
- Footer: () => (_jsxs(_Fragment, { children: [hasMoreEvents && (_jsx("div", { className: "px-3 pt-3 flex justify-center", children: _jsx("button", { type: "button", onClick: () => void onLoadMoreEvents?.(), disabled: isLoadingMoreEvents, className: "h-8 px-3 text-xs rounded-md border transition-colors disabled:opacity-60 disabled:cursor-not-allowed", style: {
606
- borderColor: 'var(--ds-gray-alpha-400)',
607
- color: 'var(--ds-gray-900)',
608
- backgroundColor: 'var(--ds-background-100)',
609
- }, children: isLoadingMoreEvents
610
- ? 'Loading more events...'
611
- : 'Load more' }) })), _jsxs("div", { className: "mt-4 pt-3 border-t text-xs px-3", style: {
612
- borderColor: 'var(--ds-gray-alpha-200)',
726
+ Footer: hasMoreEvents
727
+ ? () => (_jsx("div", { className: "px-3 pt-3 flex justify-center", children: _jsx("button", { type: "button", onClick: () => void onLoadMoreEvents?.(), disabled: isLoadingMoreEvents, className: "h-8 px-3 text-xs rounded-md border transition-colors disabled:opacity-60 disabled:cursor-not-allowed", style: {
728
+ borderColor: 'var(--ds-gray-alpha-400)',
613
729
  color: 'var(--ds-gray-900)',
614
- }, children: [sortedEvents.length, " event", sortedEvents.length !== 1 ? 's' : '', " total"] })] })),
615
- }, style: { flex: 1, minHeight: 0 } })] }));
730
+ backgroundColor: 'var(--ds-background-100)',
731
+ }, children: isLoadingMoreEvents
732
+ ? 'Loading more events...'
733
+ : 'Load more' }) }))
734
+ : undefined,
735
+ }, style: { flex: 1, minHeight: 0 } }), _jsxs("div", { className: "flex-shrink-0 border-t text-xs px-3 py-2", style: {
736
+ borderColor: 'var(--ds-gray-alpha-200)',
737
+ color: 'var(--ds-gray-900)',
738
+ backgroundColor: 'var(--ds-background-100)',
739
+ }, children: [sortedEvents.length, " event", sortedEvents.length !== 1 ? 's' : '', " total"] })] }));
616
740
  }
617
741
  //# sourceMappingURL=event-list-view.js.map