@workflow/web-shared 4.1.0-beta.63 → 4.1.0-beta.65

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 (52) hide show
  1. package/README.md +4 -0
  2. package/dist/components/event-list-view.d.ts +12 -1
  3. package/dist/components/event-list-view.d.ts.map +1 -1
  4. package/dist/components/event-list-view.js +233 -91
  5. package/dist/components/event-list-view.js.map +1 -1
  6. package/dist/components/index.d.ts +4 -0
  7. package/dist/components/index.d.ts.map +1 -1
  8. package/dist/components/index.js +4 -0
  9. package/dist/components/index.js.map +1 -1
  10. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  11. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  12. package/dist/components/sidebar/entity-detail-panel.js +4 -14
  13. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  14. package/dist/components/stream-viewer.d.ts +3 -1
  15. package/dist/components/stream-viewer.d.ts.map +1 -1
  16. package/dist/components/stream-viewer.js +23 -28
  17. package/dist/components/stream-viewer.js.map +1 -1
  18. package/dist/components/ui/decrypt-button.d.ts +15 -0
  19. package/dist/components/ui/decrypt-button.d.ts.map +1 -0
  20. package/dist/components/ui/decrypt-button.js +12 -0
  21. package/dist/components/ui/decrypt-button.js.map +1 -0
  22. package/dist/components/ui/error-stack-block.d.ts +3 -4
  23. package/dist/components/ui/error-stack-block.d.ts.map +1 -1
  24. package/dist/components/ui/error-stack-block.js +18 -9
  25. package/dist/components/ui/error-stack-block.js.map +1 -1
  26. package/dist/components/ui/load-more-button.d.ts +13 -0
  27. package/dist/components/ui/load-more-button.d.ts.map +1 -0
  28. package/dist/components/ui/load-more-button.js +12 -0
  29. package/dist/components/ui/load-more-button.js.map +1 -0
  30. package/dist/components/ui/menu-dropdown.d.ts +16 -0
  31. package/dist/components/ui/menu-dropdown.d.ts.map +1 -0
  32. package/dist/components/ui/menu-dropdown.js +46 -0
  33. package/dist/components/ui/menu-dropdown.js.map +1 -0
  34. package/dist/components/ui/spinner.d.ts +9 -0
  35. package/dist/components/ui/spinner.d.ts.map +1 -0
  36. package/dist/components/ui/spinner.js +57 -0
  37. package/dist/components/ui/spinner.js.map +1 -0
  38. package/dist/components/workflow-trace-view.d.ts +3 -1
  39. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  40. package/dist/components/workflow-trace-view.js +7 -6
  41. package/dist/components/workflow-trace-view.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/components/event-list-view.tsx +398 -141
  44. package/src/components/index.ts +4 -0
  45. package/src/components/sidebar/entity-detail-panel.tsx +9 -25
  46. package/src/components/stream-viewer.tsx +52 -63
  47. package/src/components/ui/decrypt-button.tsx +69 -0
  48. package/src/components/ui/error-stack-block.tsx +26 -16
  49. package/src/components/ui/load-more-button.tsx +38 -0
  50. package/src/components/ui/menu-dropdown.tsx +111 -0
  51. package/src/components/ui/spinner.tsx +76 -0
  52. package/src/components/workflow-trace-view.tsx +15 -22
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
@@ -8,7 +8,18 @@ interface EventsListProps {
8
8
  onLoadMoreEvents?: () => Promise<void> | void;
9
9
  /** When provided, signals that decryption is active (triggers re-load of expanded events) */
10
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;
18
+ /** Called when the user clicks the Decrypt button. */
19
+ onDecrypt?: () => void;
20
+ /** Whether the encryption key is currently being fetched. */
21
+ isDecrypting?: boolean;
11
22
  }
12
- export declare function EventListView({ events, run, onLoadEventData, hasMoreEvents, isLoadingMoreEvents, onLoadMoreEvents, encryptionKey, }: EventsListProps): import("react/jsx-runtime").JSX.Element;
23
+ export declare function EventListView({ events, run, onLoadEventData, hasMoreEvents, isLoadingMoreEvents, onLoadMoreEvents, encryptionKey, isLoading, sortOrder: sortOrderProp, onSortOrderChange, onDecrypt, isDecrypting, }: EventsListProps): import("react/jsx-runtime").JSX.Element;
13
24
  export {};
14
25
  //# 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,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA0kB1D,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;CAC5B;AA+WD,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,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;AAknB1D,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;IACpD,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;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,EACjB,SAAS,EACT,YAAoB,GACrB,EAAE,eAAe,2CAmbjB"}
@@ -4,9 +4,13 @@ import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
4
4
  import { Check, ChevronRight, Copy } from 'lucide-react';
5
5
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { Virtuoso } from 'react-virtuoso';
7
+ import { isEncryptedMarker } from '../lib/hydration';
8
+ import { DecryptButton } from './ui/decrypt-button';
7
9
  import { formatDuration } from '../lib/utils';
8
10
  import { DataInspector } from './ui/data-inspector';
9
11
  import { ErrorStackBlock, isStructuredErrorWithStack, } from './ui/error-stack-block';
12
+ import { LoadMoreButton } from './ui/load-more-button';
13
+ import { MenuDropdown } from './ui/menu-dropdown';
10
14
  import { Skeleton } from './ui/skeleton';
11
15
  /**
12
16
  * Event types whose eventData contains an error field with a StructuredError.
@@ -240,7 +244,7 @@ function TreeGutter({ isFirst, isLast, isRunLevel: isRun, statusDotColor, pulse
240
244
  // ──────────────────────────────────────────────────────────────────────────
241
245
  // Copyable cell — shows a copy button on hover
242
246
  // ──────────────────────────────────────────────────────────────────────────
243
- function CopyableCell({ value, className, }) {
247
+ function CopyableCell({ value, className, style: styleProp, }) {
244
248
  const [copied, setCopied] = useState(false);
245
249
  const resetCopiedTimeoutRef = useRef(null);
246
250
  useEffect(() => {
@@ -263,7 +267,7 @@ function CopyableCell({ value, className, }) {
263
267
  }, 1500);
264
268
  });
265
269
  }, [value]);
266
- 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] }));
270
+ 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] }));
267
271
  }
268
272
  /** Recursively parse stringified JSON values so escaped slashes / quotes are cleaned up */
269
273
  function deepParseJson(value) {
@@ -354,20 +358,24 @@ function PayloadBlock({ data, eventType, }) {
354
358
  }
355
359
  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" })] })) })] }));
356
360
  }
357
- function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroupKey, selectedGroupRange, correlationNameMap, workflowName, durationMap, onSelectGroup, onHoverGroup, onLoadEventData, encryptionKey, }) {
358
- const [isExpanded, setIsExpanded] = useState(false);
361
+ // ──────────────────────────────────────────────────────────────────────────
362
+ // Sort options for the events list
363
+ // ──────────────────────────────────────────────────────────────────────────
364
+ const SORT_OPTIONS = [
365
+ { value: 'desc', label: 'Newest' },
366
+ { value: 'asc', label: 'Oldest' },
367
+ ];
368
+ function RowsSkeleton() {
369
+ return (_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))) }));
370
+ }
371
+ function EventRow({ event, index, isFirst, isLast, isExpanded, onToggleExpand, activeGroupKey, selectedGroupKey, selectedGroupRange, correlationNameMap, workflowName, durationMap, onSelectGroup, onHoverGroup, onLoadEventData, cachedEventData, onCacheEventData, encryptionKey, }) {
359
372
  const [isLoading, setIsLoading] = useState(false);
360
- const [loadedEventData, setLoadedEventData] = useState(null);
373
+ const [loadedEventData, setLoadedEventData] = useState(cachedEventData);
361
374
  const [loadError, setLoadError] = useState(null);
362
- const [hasAttemptedLoad, setHasAttemptedLoad] = useState(false);
363
- const rowGroupKey = event.correlationId ??
364
- (isRunLevel(event.eventType) ? '__run__' : undefined);
365
- // Collapse when a different group gets selected
366
- useEffect(() => {
367
- if (selectedGroupKey !== undefined && selectedGroupKey !== rowGroupKey) {
368
- setIsExpanded(false);
369
- }
370
- }, [selectedGroupKey, rowGroupKey]);
375
+ const [hasAttemptedLoad, setHasAttemptedLoad] = useState(cachedEventData !== null);
376
+ const rowGroupKey = isRunLevel(event.eventType)
377
+ ? '__run__'
378
+ : (event.correlationId ?? undefined);
371
379
  const statusDotColor = getStatusDotColor(event.eventType);
372
380
  const createdAt = new Date(event.createdAt);
373
381
  const hasExistingEventData = 'eventData' in event && event.eventData != null;
@@ -401,9 +409,10 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
401
409
  setLoadError('Event details unavailable');
402
410
  return;
403
411
  }
404
- const eventData = await onLoadEventData(event);
405
- if (eventData !== null && eventData !== undefined) {
406
- setLoadedEventData(eventData);
412
+ const data = await onLoadEventData(event);
413
+ if (data !== null && data !== undefined) {
414
+ setLoadedEventData(data);
415
+ onCacheEventData(event.eventId, data);
407
416
  }
408
417
  }
409
418
  catch (err) {
@@ -413,7 +422,24 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
413
422
  setIsLoading(false);
414
423
  setHasAttemptedLoad(true);
415
424
  }
416
- }, [event, loadedEventData, hasExistingEventData, onLoadEventData]);
425
+ }, [
426
+ event,
427
+ loadedEventData,
428
+ hasExistingEventData,
429
+ onLoadEventData,
430
+ onCacheEventData,
431
+ ]);
432
+ // Auto-load event data when remounting in expanded state without cached data
433
+ useEffect(() => {
434
+ if (isExpanded &&
435
+ loadedEventData === null &&
436
+ !hasExistingEventData &&
437
+ !isLoading &&
438
+ !hasAttemptedLoad) {
439
+ loadEventDetails();
440
+ }
441
+ // eslint-disable-next-line react-hooks/exhaustive-deps
442
+ }, []);
417
443
  // When encryption key changes and this event was previously loaded,
418
444
  // re-load to get decrypted data
419
445
  useEffect(() => {
@@ -424,6 +450,7 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
424
450
  .then((data) => {
425
451
  if (data !== null && data !== undefined) {
426
452
  setLoadedEventData(data);
453
+ onCacheEventData(event.eventId, data);
427
454
  }
428
455
  setHasAttemptedLoad(true);
429
456
  })
@@ -433,22 +460,23 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
433
460
  }
434
461
  // eslint-disable-next-line react-hooks/exhaustive-deps
435
462
  }, [encryptionKey]);
436
- const handleExpandToggle = useCallback((e) => {
437
- e.stopPropagation();
438
- const newExpanded = !isExpanded;
439
- setIsExpanded(newExpanded);
440
- if (newExpanded && loadedEventData === null && !hasExistingEventData) {
441
- loadEventDetails();
442
- }
443
- }, [isExpanded, loadedEventData, hasExistingEventData, loadEventDetails]);
444
463
  const handleRowClick = useCallback(() => {
445
- if (selectedGroupKey === rowGroupKey) {
446
- onSelectGroup(undefined);
447
- }
448
- else {
449
- onSelectGroup(rowGroupKey);
464
+ onSelectGroup(rowGroupKey === selectedGroupKey ? undefined : rowGroupKey);
465
+ onToggleExpand(event.eventId);
466
+ if (!isExpanded && loadedEventData === null && !hasExistingEventData) {
467
+ loadEventDetails();
450
468
  }
451
- }, [selectedGroupKey, rowGroupKey, onSelectGroup]);
469
+ }, [
470
+ selectedGroupKey,
471
+ rowGroupKey,
472
+ onSelectGroup,
473
+ onToggleExpand,
474
+ event.eventId,
475
+ isExpanded,
476
+ loadedEventData,
477
+ hasExistingEventData,
478
+ loadEventDetails,
479
+ ]);
452
480
  const eventData = hasExistingEventData
453
481
  ? event.eventData
454
482
  : loadedEventData;
@@ -456,13 +484,12 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
456
484
  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) => {
457
485
  if (e.key === 'Enter' || e.key === ' ')
458
486
  handleRowClick();
459
- }, 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: {
460
- ...BUTTON_RESET_STYLE,
461
- border: '1px solid var(--ds-gray-alpha-400)',
462
- }, "aria-label": isExpanded ? 'Collapse details' : 'Expand details', children: _jsx(ChevronRight, { className: "h-3 w-3 transition-transform", style: {
463
- color: 'var(--ds-gray-700)',
487
+ }, 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: {
488
+ border: '1px solid var(--ds-gray-400)',
489
+ }, children: _jsx(ChevronRight, { className: "h-3 w-3 transition-transform", style: {
490
+ color: 'var(--ds-gray-900)',
464
491
  transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
465
- } }) }), _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: {
492
+ } }) }), _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: {
466
493
  position: 'relative',
467
494
  display: 'inline-flex',
468
495
  width: 6,
@@ -481,7 +508,7 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
481
508
  height: 6,
482
509
  borderRadius: '50%',
483
510
  backgroundColor: statusDotColor,
484
- } })] }), 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: {
511
+ } })] }), 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: {
485
512
  borderColor: 'var(--ds-gray-alpha-200)',
486
513
  opacity: contentOpacity,
487
514
  transition: 'opacity 150ms',
@@ -499,11 +526,41 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
499
526
  // ──────────────────────────────────────────────────────────────────────────
500
527
  // Main component
501
528
  // ──────────────────────────────────────────────────────────────────────────
502
- export function EventListView({ events, run, onLoadEventData, hasMoreEvents = false, isLoadingMoreEvents = false, onLoadMoreEvents, encryptionKey, }) {
529
+ export function EventListView({ events, run, onLoadEventData, hasMoreEvents = false, isLoadingMoreEvents = false, onLoadMoreEvents, encryptionKey, isLoading = false, sortOrder: sortOrderProp, onSortOrderChange, onDecrypt, isDecrypting = false, }) {
530
+ const [internalSortOrder, setInternalSortOrder] = useState('asc');
531
+ const effectiveSortOrder = sortOrderProp ?? internalSortOrder;
532
+ const handleSortOrderChange = useCallback((order) => {
533
+ if (onSortOrderChange) {
534
+ onSortOrderChange(order);
535
+ }
536
+ else {
537
+ setInternalSortOrder(order);
538
+ }
539
+ }, [onSortOrderChange]);
503
540
  const sortedEvents = useMemo(() => {
504
541
  if (!events || events.length === 0)
505
542
  return [];
506
- return [...events].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
543
+ const dir = effectiveSortOrder === 'desc' ? -1 : 1;
544
+ return [...events].sort((a, b) => dir *
545
+ (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()));
546
+ }, [events, effectiveSortOrder]);
547
+ // Detect encrypted fields across all loaded events.
548
+ // Only checks top-level eventData values (input, output, result, etc.) —
549
+ // the current data model guarantees encrypted markers appear at this level.
550
+ const hasEncryptedData = useMemo(() => {
551
+ if (!events)
552
+ return false;
553
+ for (const event of events) {
554
+ const ed = event.eventData;
555
+ if (!ed || typeof ed !== 'object')
556
+ continue;
557
+ const data = ed;
558
+ for (const val of Object.values(data)) {
559
+ if (isEncryptedMarker(val))
560
+ return true;
561
+ }
562
+ }
563
+ return false;
507
564
  }, [events]);
508
565
  const { correlationNameMap, workflowName } = useMemo(() => buildNameMaps(events ?? null, run ?? null), [events, run]);
509
566
  const durationMap = useMemo(() => buildDurationMap(sortedEvents), [sortedEvents]);
@@ -516,6 +573,57 @@ export function EventListView({ events, run, onLoadEventData, hasMoreEvents = fa
516
573
  setHoveredGroupKey(groupKey);
517
574
  }, []);
518
575
  const activeGroupKey = selectedGroupKey ?? hoveredGroupKey;
576
+ // Expanded state lifted out of EventRow so it survives virtualization
577
+ const [expandedEventIds, setExpandedEventIds] = useState(() => new Set());
578
+ const toggleEventExpanded = useCallback((eventId) => {
579
+ setExpandedEventIds((prev) => {
580
+ const next = new Set(prev);
581
+ if (next.has(eventId)) {
582
+ next.delete(eventId);
583
+ }
584
+ else {
585
+ next.add(eventId);
586
+ }
587
+ return next;
588
+ });
589
+ }, []);
590
+ // Event data cache — ref avoids re-renders when cache updates
591
+ const eventDataCacheRef = useRef(new Map());
592
+ const cacheEventData = useCallback((eventId, data) => {
593
+ eventDataCacheRef.current.set(eventId, data);
594
+ }, []);
595
+ // Lookup from eventId → groupKey for efficient collapse filtering
596
+ const eventGroupKeyMap = useMemo(() => {
597
+ const map = new Map();
598
+ for (const ev of sortedEvents) {
599
+ const gk = isRunLevel(ev.eventType)
600
+ ? '__run__'
601
+ : (ev.correlationId ?? '');
602
+ if (gk)
603
+ map.set(ev.eventId, gk);
604
+ }
605
+ return map;
606
+ }, [sortedEvents]);
607
+ // Collapse expanded events that don't belong to the newly selected group
608
+ useEffect(() => {
609
+ if (selectedGroupKey === undefined)
610
+ return;
611
+ setExpandedEventIds((prev) => {
612
+ if (prev.size === 0)
613
+ return prev;
614
+ let changed = false;
615
+ const next = new Set();
616
+ for (const eventId of prev) {
617
+ if (eventGroupKeyMap.get(eventId) === selectedGroupKey) {
618
+ next.add(eventId);
619
+ }
620
+ else {
621
+ changed = true;
622
+ }
623
+ }
624
+ return changed ? next : prev;
625
+ });
626
+ }, [selectedGroupKey, eventGroupKeyMap]);
519
627
  // Compute the row-index range for the active group's connecting lane line.
520
628
  // Only applies to non-run groups (step/hook/wait correlations).
521
629
  const selectedGroupRange = useMemo(() => {
@@ -538,83 +646,117 @@ export function EventListView({ events, run, onLoadEventData, hasMoreEvents = fa
538
646
  const entries = [];
539
647
  for (let i = 0; i < sortedEvents.length; i++) {
540
648
  const ev = sortedEvents[i];
649
+ const isRun = isRunLevel(ev.eventType);
650
+ const name = isRun
651
+ ? (workflowName ?? '')
652
+ : ev.correlationId
653
+ ? (correlationNameMap.get(ev.correlationId) ?? '')
654
+ : '';
541
655
  entries.push({
542
- text: [ev.eventId, ev.correlationId ?? ''].join(' ').toLowerCase(),
543
- groupKey: ev.correlationId ??
544
- (isRunLevel(ev.eventType) ? '__run__' : undefined),
656
+ fields: [
657
+ ev.eventId,
658
+ ev.correlationId ?? '',
659
+ ev.eventType,
660
+ formatEventType(ev.eventType),
661
+ name,
662
+ ].map((f) => f.toLowerCase()),
663
+ groupKey: ev.correlationId ?? (isRun ? '__run__' : undefined),
545
664
  eventId: ev.eventId,
546
665
  index: i,
547
666
  });
548
667
  }
549
668
  return entries;
550
- }, [sortedEvents]);
669
+ }, [sortedEvents, correlationNameMap, workflowName]);
551
670
  useEffect(() => {
552
671
  const q = searchQuery.trim().toLowerCase();
553
672
  if (!q) {
554
673
  setSelectedGroupKey(undefined);
555
674
  return;
556
675
  }
557
- const match = searchIndex.find((entry) => entry.text.includes(q));
558
- if (match) {
559
- setSelectedGroupKey(match.groupKey);
676
+ let bestMatch = null;
677
+ let bestScore = 0;
678
+ for (const entry of searchIndex) {
679
+ for (const field of entry.fields) {
680
+ if (field && field.includes(q)) {
681
+ const score = q.length / field.length;
682
+ if (score > bestScore) {
683
+ bestScore = score;
684
+ bestMatch = entry;
685
+ }
686
+ }
687
+ }
688
+ }
689
+ if (bestMatch) {
690
+ setSelectedGroupKey(bestMatch.groupKey);
560
691
  virtuosoRef.current?.scrollToIndex({
561
- index: match.index,
692
+ index: bestMatch.index,
562
693
  align: 'center',
563
694
  behavior: 'smooth',
564
695
  });
565
696
  }
566
697
  }, [searchQuery, searchIndex]);
567
- if (!events || events.length === 0) {
698
+ // Track whether we've ever had events to distinguish initial load from refetch
699
+ const hasHadEventsRef = useRef(false);
700
+ if (sortedEvents.length > 0) {
701
+ hasHadEventsRef.current = true;
702
+ }
703
+ const isInitialLoad = isLoading && !hasHadEventsRef.current;
704
+ const isRefetching = isLoading && hasHadEventsRef.current && sortedEvents.length === 0;
705
+ if (isInitialLoad) {
706
+ 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(RowsSkeleton, {})] }));
707
+ }
708
+ if (!isLoading && (!events || events.length === 0)) {
568
709
  return (_jsx("div", { className: "flex items-center justify-center h-full text-sm", style: { color: 'var(--ds-gray-700)' }, children: "No events found" }));
569
710
  }
570
- 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: {
571
- display: 'flex',
572
- alignItems: 'center',
573
- justifyContent: 'center',
574
- borderRadius: 6,
575
- boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
576
- background: 'var(--ds-background-100)',
577
- height: 40,
578
- }, children: [_jsx("div", { style: {
579
- width: 40,
580
- height: 40,
581
- display: 'flex',
582
- alignItems: 'center',
583
- justifyContent: 'center',
584
- color: 'var(--ds-gray-800)',
585
- flexShrink: 0,
586
- }, 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: {
587
- marginLeft: -16,
588
- paddingInline: 12,
589
- fontFamily: 'inherit',
590
- fontSize: 14,
591
- background: 'transparent',
592
- border: 'none',
593
- outline: 'none',
594
- height: 40,
595
- width: '100%',
596
- } })] }) }), _jsxs("div", { className: "flex items-center gap-0 text-sm font-medium h-10 border-b flex-shrink-0", style: {
711
+ 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: {
712
+ padding: 6,
713
+ backgroundColor: 'var(--ds-background-100)',
714
+ display: 'flex',
715
+ gap: 6,
716
+ }, children: [_jsxs("label", { style: {
717
+ display: 'flex',
718
+ alignItems: 'center',
719
+ justifyContent: 'center',
720
+ borderRadius: 6,
721
+ boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
722
+ background: 'var(--ds-background-100)',
723
+ height: 40,
724
+ flex: 1,
725
+ minWidth: 0,
726
+ }, children: [_jsx("div", { style: {
727
+ width: 40,
728
+ height: 40,
729
+ display: 'flex',
730
+ alignItems: 'center',
731
+ justifyContent: 'center',
732
+ color: 'var(--ds-gray-800)',
733
+ flexShrink: 0,
734
+ }, 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: {
735
+ marginLeft: -16,
736
+ paddingInline: 12,
737
+ fontFamily: 'inherit',
738
+ fontSize: 14,
739
+ background: 'transparent',
740
+ border: 'none',
741
+ outline: 'none',
742
+ height: 40,
743
+ width: '100%',
744
+ } })] }), _jsx(MenuDropdown, { options: SORT_OPTIONS, value: effectiveSortOrder, onChange: handleSortOrderChange }), (hasEncryptedData || encryptionKey) && onDecrypt && (_jsx(DecryptButton, { decrypted: !!encryptionKey, loading: isDecrypting, onClick: onDecrypt }))] }), _jsxs("div", { className: "flex items-center gap-0 text-[13px] font-medium h-10 border-b flex-shrink-0", style: {
597
745
  borderColor: 'var(--ds-gray-alpha-200)',
598
746
  color: 'var(--ds-gray-900)',
599
747
  backgroundColor: 'var(--ds-background-100)',
600
- }, 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: () => {
748
+ }, 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" })] }), isRefetching ? (_jsx(RowsSkeleton, {})) : (_jsx(Virtuoso, { ref: virtuosoRef, totalCount: sortedEvents.length, overscan: 20, defaultItemHeight: 40, endReached: () => {
601
749
  if (!hasMoreEvents || isLoadingMoreEvents) {
602
750
  return;
603
751
  }
604
752
  void onLoadMoreEvents?.();
605
753
  }, itemContent: (index) => {
606
- 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 }));
607
- }, components: {
608
- 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: {
609
- borderColor: 'var(--ds-gray-alpha-400)',
610
- color: 'var(--ds-gray-900)',
611
- backgroundColor: 'var(--ds-background-100)',
612
- }, children: isLoadingMoreEvents
613
- ? 'Loading more events...'
614
- : 'Load more' }) })), _jsxs("div", { className: "mt-4 pt-3 border-t text-xs px-3", style: {
615
- borderColor: 'var(--ds-gray-alpha-200)',
616
- color: 'var(--ds-gray-900)',
617
- }, children: [sortedEvents.length, " event", sortedEvents.length !== 1 ? 's' : '', " total"] })] })),
618
- }, style: { flex: 1, minHeight: 0 } })] }));
754
+ const ev = sortedEvents[index];
755
+ 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 }));
756
+ }, style: { flex: 1, minHeight: 0 } })), _jsxs("div", { className: "relative flex-shrink-0 flex items-center h-10 border-t px-4 text-xs", style: {
757
+ borderColor: 'var(--ds-gray-alpha-200)',
758
+ color: 'var(--ds-gray-900)',
759
+ backgroundColor: 'var(--ds-background-100)',
760
+ }, children: [_jsxs("span", { children: [sortedEvents.length, " event", sortedEvents.length !== 1 ? 's' : '', " loaded"] }), hasMoreEvents && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: _jsx("div", { className: "pointer-events-auto", children: _jsx(LoadMoreButton, { loading: isLoadingMoreEvents, onClick: () => void onLoadMoreEvents?.() }) }) }))] })] }));
619
761
  }
620
762
  //# sourceMappingURL=event-list-view.js.map