@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
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
4
- import type { Event, Step, WorkflowRun } from '@workflow/world';
4
+ import type { Event, WorkflowRun } from '@workflow/world';
5
5
  import { Check, ChevronRight, Copy } from 'lucide-react';
6
6
  import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
7
7
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -12,6 +12,7 @@ import {
12
12
  ErrorStackBlock,
13
13
  isStructuredErrorWithStack,
14
14
  } from './ui/error-stack-block';
15
+ import { MenuDropdown } from './ui/menu-dropdown';
15
16
  import { Skeleton } from './ui/skeleton';
16
17
 
17
18
  /**
@@ -103,11 +104,11 @@ function getStatusDotColor(eventType: string): string {
103
104
  }
104
105
 
105
106
  /**
106
- * Build a map from correlationId (stepId) → display name using step entities,
107
- * and parse the workflow name from the run.
107
+ * Build a map from correlationId (stepId) → display name using step_created
108
+ * events, and parse the workflow name from the run.
108
109
  */
109
110
  function buildNameMaps(
110
- steps: Step[] | null,
111
+ events: Event[] | null,
111
112
  run: WorkflowRun | null
112
113
  ): {
113
114
  correlationNameMap: Map<string, string>;
@@ -115,11 +116,17 @@ function buildNameMaps(
115
116
  } {
116
117
  const correlationNameMap = new Map<string, string>();
117
118
 
118
- // Map step correlationId (= stepId) → parsed step name
119
- if (steps) {
120
- for (const step of steps) {
121
- const parsed = parseStepName(String(step.stepName));
122
- correlationNameMap.set(step.stepId, parsed?.shortName ?? step.stepName);
119
+ // Map step correlationId (= stepId) → parsed step name from step_created events
120
+ if (events) {
121
+ for (const event of events) {
122
+ if (event.eventType === 'step_created' && event.correlationId) {
123
+ const stepName = event.eventData?.stepName ?? '';
124
+ const parsed = parseStepName(String(stepName));
125
+ correlationNameMap.set(
126
+ event.correlationId,
127
+ parsed?.shortName ?? stepName
128
+ );
129
+ }
123
130
  }
124
131
  }
125
132
 
@@ -372,9 +379,11 @@ function TreeGutter({
372
379
  function CopyableCell({
373
380
  value,
374
381
  className,
382
+ style: styleProp,
375
383
  }: {
376
384
  value: string;
377
385
  className?: string;
386
+ style?: React.CSSProperties;
378
387
  }): ReactNode {
379
388
  const [copied, setCopied] = useState(false);
380
389
  const resetCopiedTimeoutRef = useRef<number | null>(null);
@@ -406,7 +415,8 @@ function CopyableCell({
406
415
 
407
416
  return (
408
417
  <div
409
- className={`group/copy flex items-center gap-1 flex-1 min-w-0 px-4 ${className ?? ''}`}
418
+ className={`group/copy flex items-center gap-1 min-w-0 px-4 ${className ?? ''}`}
419
+ style={styleProp}
410
420
  >
411
421
  <span className="overflow-hidden text-ellipsis whitespace-nowrap">
412
422
  {value || '-'}
@@ -577,13 +587,21 @@ function PayloadBlock({
577
587
  );
578
588
  }
579
589
 
590
+ // ──────────────────────────────────────────────────────────────────────────
591
+ // Sort options for the events list
592
+ // ──────────────────────────────────────────────────────────────────────────
593
+
594
+ const SORT_OPTIONS = [
595
+ { value: 'desc' as const, label: 'Newest' },
596
+ { value: 'asc' as const, label: 'Oldest' },
597
+ ];
598
+
580
599
  // ──────────────────────────────────────────────────────────────────────────
581
600
  // Event row
582
601
  // ──────────────────────────────────────────────────────────────────────────
583
602
 
584
603
  interface EventsListProps {
585
604
  events: Event[] | null;
586
- steps?: Step[] | null;
587
605
  run?: WorkflowRun | null;
588
606
  onLoadEventData?: (event: Event) => Promise<unknown | null>;
589
607
  hasMoreEvents?: boolean;
@@ -591,6 +609,13 @@ interface EventsListProps {
591
609
  onLoadMoreEvents?: () => Promise<void> | void;
592
610
  /** When provided, signals that decryption is active (triggers re-load of expanded events) */
593
611
  encryptionKey?: Uint8Array;
612
+ /** When true, shows a loading state instead of "No events found" for empty lists */
613
+ isLoading?: boolean;
614
+ /** Sort order for events. Defaults to 'asc'. */
615
+ sortOrder?: 'asc' | 'desc';
616
+ /** Called when the user changes sort order. When provided, the sort dropdown is shown
617
+ * and the parent is expected to refetch from the API with the new order. */
618
+ onSortOrderChange?: (order: 'asc' | 'desc') => void;
594
619
  }
595
620
 
596
621
  function EventRow({
@@ -598,6 +623,8 @@ function EventRow({
598
623
  index,
599
624
  isFirst,
600
625
  isLast,
626
+ isExpanded,
627
+ onToggleExpand,
601
628
  activeGroupKey,
602
629
  selectedGroupKey,
603
630
  selectedGroupRange,
@@ -607,12 +634,16 @@ function EventRow({
607
634
  onSelectGroup,
608
635
  onHoverGroup,
609
636
  onLoadEventData,
637
+ cachedEventData,
638
+ onCacheEventData,
610
639
  encryptionKey,
611
640
  }: {
612
641
  event: Event;
613
642
  index: number;
614
643
  isFirst: boolean;
615
644
  isLast: boolean;
645
+ isExpanded: boolean;
646
+ onToggleExpand: (eventId: string) => void;
616
647
  activeGroupKey?: string;
617
648
  selectedGroupKey?: string;
618
649
  selectedGroupRange: { first: number; last: number } | null;
@@ -622,24 +653,22 @@ function EventRow({
622
653
  onSelectGroup: (groupKey: string | undefined) => void;
623
654
  onHoverGroup: (groupKey: string | undefined) => void;
624
655
  onLoadEventData?: (event: Event) => Promise<unknown | null>;
656
+ cachedEventData: unknown | null;
657
+ onCacheEventData: (eventId: string, data: unknown) => void;
625
658
  encryptionKey?: Uint8Array;
626
659
  }) {
627
- const [isExpanded, setIsExpanded] = useState(false);
628
660
  const [isLoading, setIsLoading] = useState(false);
629
- const [loadedEventData, setLoadedEventData] = useState<unknown | null>(null);
661
+ const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
662
+ cachedEventData
663
+ );
630
664
  const [loadError, setLoadError] = useState<string | null>(null);
631
- const [hasAttemptedLoad, setHasAttemptedLoad] = useState(false);
665
+ const [hasAttemptedLoad, setHasAttemptedLoad] = useState(
666
+ cachedEventData !== null
667
+ );
632
668
 
633
- const rowGroupKey =
634
- event.correlationId ??
635
- (isRunLevel(event.eventType) ? '__run__' : undefined);
636
-
637
- // Collapse when a different group gets selected
638
- useEffect(() => {
639
- if (selectedGroupKey !== undefined && selectedGroupKey !== rowGroupKey) {
640
- setIsExpanded(false);
641
- }
642
- }, [selectedGroupKey, rowGroupKey]);
669
+ const rowGroupKey = isRunLevel(event.eventType)
670
+ ? '__run__'
671
+ : (event.correlationId ?? undefined);
643
672
 
644
673
  const statusDotColor = getStatusDotColor(event.eventType);
645
674
  const createdAt = new Date(event.createdAt);
@@ -681,9 +710,10 @@ function EventRow({
681
710
  setLoadError('Event details unavailable');
682
711
  return;
683
712
  }
684
- const eventData = await onLoadEventData(event);
685
- if (eventData !== null && eventData !== undefined) {
686
- setLoadedEventData(eventData);
713
+ const data = await onLoadEventData(event);
714
+ if (data !== null && data !== undefined) {
715
+ setLoadedEventData(data);
716
+ onCacheEventData(event.eventId, data);
687
717
  }
688
718
  } catch (err) {
689
719
  setLoadError(
@@ -693,7 +723,27 @@ function EventRow({
693
723
  setIsLoading(false);
694
724
  setHasAttemptedLoad(true);
695
725
  }
696
- }, [event, loadedEventData, hasExistingEventData, onLoadEventData]);
726
+ }, [
727
+ event,
728
+ loadedEventData,
729
+ hasExistingEventData,
730
+ onLoadEventData,
731
+ onCacheEventData,
732
+ ]);
733
+
734
+ // Auto-load event data when remounting in expanded state without cached data
735
+ useEffect(() => {
736
+ if (
737
+ isExpanded &&
738
+ loadedEventData === null &&
739
+ !hasExistingEventData &&
740
+ !isLoading &&
741
+ !hasAttemptedLoad
742
+ ) {
743
+ loadEventDetails();
744
+ }
745
+ // eslint-disable-next-line react-hooks/exhaustive-deps
746
+ }, []);
697
747
 
698
748
  // When encryption key changes and this event was previously loaded,
699
749
  // re-load to get decrypted data
@@ -705,6 +755,7 @@ function EventRow({
705
755
  .then((data) => {
706
756
  if (data !== null && data !== undefined) {
707
757
  setLoadedEventData(data);
758
+ onCacheEventData(event.eventId, data);
708
759
  }
709
760
  setHasAttemptedLoad(true);
710
761
  })
@@ -715,25 +766,23 @@ function EventRow({
715
766
  // eslint-disable-next-line react-hooks/exhaustive-deps
716
767
  }, [encryptionKey]);
717
768
 
718
- const handleExpandToggle = useCallback(
719
- (e: ReactMouseEvent) => {
720
- e.stopPropagation();
721
- const newExpanded = !isExpanded;
722
- setIsExpanded(newExpanded);
723
- if (newExpanded && loadedEventData === null && !hasExistingEventData) {
724
- loadEventDetails();
725
- }
726
- },
727
- [isExpanded, loadedEventData, hasExistingEventData, loadEventDetails]
728
- );
729
-
730
769
  const handleRowClick = useCallback(() => {
731
- if (selectedGroupKey === rowGroupKey) {
732
- onSelectGroup(undefined);
733
- } else {
734
- onSelectGroup(rowGroupKey);
770
+ onSelectGroup(rowGroupKey === selectedGroupKey ? undefined : rowGroupKey);
771
+ onToggleExpand(event.eventId);
772
+ if (!isExpanded && loadedEventData === null && !hasExistingEventData) {
773
+ loadEventDetails();
735
774
  }
736
- }, [selectedGroupKey, rowGroupKey, onSelectGroup]);
775
+ }, [
776
+ selectedGroupKey,
777
+ rowGroupKey,
778
+ onSelectGroup,
779
+ onToggleExpand,
780
+ event.eventId,
781
+ isExpanded,
782
+ loadedEventData,
783
+ hasExistingEventData,
784
+ loadEventDetails,
785
+ ]);
737
786
 
738
787
  const eventData = hasExistingEventData
739
788
  ? (event as Event & { eventData: unknown }).eventData
@@ -755,7 +804,7 @@ function EventRow({
755
804
  onKeyDown={(e) => {
756
805
  if (e.key === 'Enter' || e.key === ' ') handleRowClick();
757
806
  }}
758
- className="w-full text-left flex items-center gap-0 text-sm hover:bg-[var(--ds-gray-alpha-100)] transition-colors cursor-pointer"
807
+ className="w-full text-left flex items-center gap-0 text-[13px] hover:bg-[var(--ds-gray-alpha-100)] transition-colors cursor-pointer"
759
808
  style={{ minHeight: 40 }}
760
809
  >
761
810
  <TreeGutter
@@ -776,36 +825,32 @@ function EventRow({
776
825
  className="flex items-center flex-1 min-w-0"
777
826
  style={{ opacity: contentOpacity, transition: 'opacity 150ms' }}
778
827
  >
779
- {/* Expand chevron button */}
780
- <button
781
- type="button"
782
- onClick={handleExpandToggle}
783
- className="flex items-center justify-center w-5 h-5 flex-shrink-0 rounded hover:bg-[var(--ds-gray-alpha-200)] transition-colors"
828
+ {/* Expand chevron indicator */}
829
+ <div
830
+ className="flex items-center justify-center w-5 h-5 flex-shrink-0 rounded"
784
831
  style={{
785
- ...BUTTON_RESET_STYLE,
786
- border: '1px solid var(--ds-gray-alpha-400)',
832
+ border: '1px solid var(--ds-gray-400)',
787
833
  }}
788
- aria-label={isExpanded ? 'Collapse details' : 'Expand details'}
789
834
  >
790
835
  <ChevronRight
791
836
  className="h-3 w-3 transition-transform"
792
837
  style={{
793
- color: 'var(--ds-gray-700)',
838
+ color: 'var(--ds-gray-900)',
794
839
  transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
795
840
  }}
796
841
  />
797
- </button>
842
+ </div>
798
843
 
799
844
  {/* Time */}
800
845
  <div
801
- className="text-xs tabular-nums flex-1 min-w-0 px-4"
802
- style={{ color: 'var(--ds-gray-900)' }}
846
+ className="tabular-nums min-w-0 px-4"
847
+ style={{ color: 'var(--ds-gray-900)', flex: '2 1 0%' }}
803
848
  >
804
849
  {formatEventTime(createdAt)}
805
850
  </div>
806
851
 
807
852
  {/* Event Type */}
808
- <div className="text-xs font-medium flex-1 min-w-0 px-4">
853
+ <div className="font-medium min-w-0 px-4" style={{ flex: '2 1 0%' }}>
809
854
  <span
810
855
  className="inline-flex items-center gap-1.5"
811
856
  style={{ color: 'var(--ds-gray-900)' }}
@@ -847,7 +892,8 @@ function EventRow({
847
892
 
848
893
  {/* Name */}
849
894
  <div
850
- className="text-xs flex-1 min-w-0 px-4 overflow-hidden text-ellipsis whitespace-nowrap"
895
+ className="min-w-0 px-4 overflow-hidden text-ellipsis whitespace-nowrap"
896
+ style={{ flex: '2 1 0%' }}
851
897
  title={eventName !== '-' ? eventName : undefined}
852
898
  >
853
899
  {eventName}
@@ -856,11 +902,16 @@ function EventRow({
856
902
  {/* Correlation ID */}
857
903
  <CopyableCell
858
904
  value={event.correlationId || ''}
859
- className="font-mono text-xs"
905
+ className="font-mono"
906
+ style={{ flex: '3 1 0%' }}
860
907
  />
861
908
 
862
909
  {/* Event ID */}
863
- <CopyableCell value={event.eventId} className="font-mono text-xs" />
910
+ <CopyableCell
911
+ value={event.eventId}
912
+ className="font-mono"
913
+ style={{ flex: '3 1 0%' }}
914
+ />
864
915
  </div>
865
916
  </div>
866
917
 
@@ -960,25 +1011,44 @@ function EventRow({
960
1011
 
961
1012
  export function EventListView({
962
1013
  events,
963
- steps,
964
1014
  run,
965
1015
  onLoadEventData,
966
1016
  hasMoreEvents = false,
967
1017
  isLoadingMoreEvents = false,
968
1018
  onLoadMoreEvents,
969
1019
  encryptionKey,
1020
+ isLoading = false,
1021
+ sortOrder: sortOrderProp,
1022
+ onSortOrderChange,
970
1023
  }: EventsListProps) {
1024
+ const [internalSortOrder, setInternalSortOrder] = useState<'asc' | 'desc'>(
1025
+ 'asc'
1026
+ );
1027
+ const effectiveSortOrder = sortOrderProp ?? internalSortOrder;
1028
+ const handleSortOrderChange = useCallback(
1029
+ (order: 'asc' | 'desc') => {
1030
+ if (onSortOrderChange) {
1031
+ onSortOrderChange(order);
1032
+ } else {
1033
+ setInternalSortOrder(order);
1034
+ }
1035
+ },
1036
+ [onSortOrderChange]
1037
+ );
1038
+
971
1039
  const sortedEvents = useMemo(() => {
972
1040
  if (!events || events.length === 0) return [];
1041
+ const dir = effectiveSortOrder === 'desc' ? -1 : 1;
973
1042
  return [...events].sort(
974
1043
  (a, b) =>
975
- new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
1044
+ dir *
1045
+ (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
976
1046
  );
977
- }, [events]);
1047
+ }, [events, effectiveSortOrder]);
978
1048
 
979
1049
  const { correlationNameMap, workflowName } = useMemo(
980
- () => buildNameMaps(steps ?? null, run ?? null),
981
- [steps, run]
1050
+ () => buildNameMaps(events ?? null, run ?? null),
1051
+ [events, run]
982
1052
  );
983
1053
 
984
1054
  const durationMap = useMemo(
@@ -1001,6 +1071,58 @@ export function EventListView({
1001
1071
 
1002
1072
  const activeGroupKey = selectedGroupKey ?? hoveredGroupKey;
1003
1073
 
1074
+ // Expanded state lifted out of EventRow so it survives virtualization
1075
+ const [expandedEventIds, setExpandedEventIds] = useState<Set<string>>(
1076
+ () => new Set()
1077
+ );
1078
+ const toggleEventExpanded = useCallback((eventId: string) => {
1079
+ setExpandedEventIds((prev) => {
1080
+ const next = new Set(prev);
1081
+ if (next.has(eventId)) {
1082
+ next.delete(eventId);
1083
+ } else {
1084
+ next.add(eventId);
1085
+ }
1086
+ return next;
1087
+ });
1088
+ }, []);
1089
+
1090
+ // Event data cache — ref avoids re-renders when cache updates
1091
+ const eventDataCacheRef = useRef<Map<string, unknown>>(new Map());
1092
+ const cacheEventData = useCallback((eventId: string, data: unknown) => {
1093
+ eventDataCacheRef.current.set(eventId, data);
1094
+ }, []);
1095
+
1096
+ // Lookup from eventId → groupKey for efficient collapse filtering
1097
+ const eventGroupKeyMap = useMemo(() => {
1098
+ const map = new Map<string, string>();
1099
+ for (const ev of sortedEvents) {
1100
+ const gk = isRunLevel(ev.eventType)
1101
+ ? '__run__'
1102
+ : (ev.correlationId ?? '');
1103
+ if (gk) map.set(ev.eventId, gk);
1104
+ }
1105
+ return map;
1106
+ }, [sortedEvents]);
1107
+
1108
+ // Collapse expanded events that don't belong to the newly selected group
1109
+ useEffect(() => {
1110
+ if (selectedGroupKey === undefined) return;
1111
+ setExpandedEventIds((prev) => {
1112
+ if (prev.size === 0) return prev;
1113
+ let changed = false;
1114
+ const next = new Set<string>();
1115
+ for (const eventId of prev) {
1116
+ if (eventGroupKeyMap.get(eventId) === selectedGroupKey) {
1117
+ next.add(eventId);
1118
+ } else {
1119
+ changed = true;
1120
+ }
1121
+ }
1122
+ return changed ? next : prev;
1123
+ });
1124
+ }, [selectedGroupKey, eventGroupKeyMap]);
1125
+
1004
1126
  // Compute the row-index range for the active group's connecting lane line.
1005
1127
  // Only applies to non-run groups (step/hook/wait correlations).
1006
1128
  const selectedGroupRange = useMemo(() => {
@@ -1021,24 +1143,34 @@ export function EventListView({
1021
1143
 
1022
1144
  const searchIndex = useMemo(() => {
1023
1145
  const entries: {
1024
- text: string;
1146
+ fields: string[];
1025
1147
  groupKey?: string;
1026
1148
  eventId: string;
1027
1149
  index: number;
1028
1150
  }[] = [];
1029
1151
  for (let i = 0; i < sortedEvents.length; i++) {
1030
1152
  const ev = sortedEvents[i];
1153
+ const isRun = isRunLevel(ev.eventType);
1154
+ const name = isRun
1155
+ ? (workflowName ?? '')
1156
+ : ev.correlationId
1157
+ ? (correlationNameMap.get(ev.correlationId) ?? '')
1158
+ : '';
1031
1159
  entries.push({
1032
- text: [ev.eventId, ev.correlationId ?? ''].join(' ').toLowerCase(),
1033
- groupKey:
1034
- ev.correlationId ??
1035
- (isRunLevel(ev.eventType) ? '__run__' : undefined),
1160
+ fields: [
1161
+ ev.eventId,
1162
+ ev.correlationId ?? '',
1163
+ ev.eventType,
1164
+ formatEventType(ev.eventType),
1165
+ name,
1166
+ ].map((f) => f.toLowerCase()),
1167
+ groupKey: ev.correlationId ?? (isRun ? '__run__' : undefined),
1036
1168
  eventId: ev.eventId,
1037
1169
  index: i,
1038
1170
  });
1039
1171
  }
1040
1172
  return entries;
1041
- }, [sortedEvents]);
1173
+ }, [sortedEvents, correlationNameMap, workflowName]);
1042
1174
 
1043
1175
  useEffect(() => {
1044
1176
  const q = searchQuery.trim().toLowerCase();
@@ -1046,11 +1178,23 @@ export function EventListView({
1046
1178
  setSelectedGroupKey(undefined);
1047
1179
  return;
1048
1180
  }
1049
- const match = searchIndex.find((entry) => entry.text.includes(q));
1050
- if (match) {
1051
- setSelectedGroupKey(match.groupKey);
1181
+ let bestMatch: (typeof searchIndex)[number] | null = null;
1182
+ let bestScore = 0;
1183
+ for (const entry of searchIndex) {
1184
+ for (const field of entry.fields) {
1185
+ if (field && field.includes(q)) {
1186
+ const score = q.length / field.length;
1187
+ if (score > bestScore) {
1188
+ bestScore = score;
1189
+ bestMatch = entry;
1190
+ }
1191
+ }
1192
+ }
1193
+ }
1194
+ if (bestMatch) {
1195
+ setSelectedGroupKey(bestMatch.groupKey);
1052
1196
  virtuosoRef.current?.scrollToIndex({
1053
- index: match.index,
1197
+ index: bestMatch.index,
1054
1198
  align: 'center',
1055
1199
  behavior: 'smooth',
1056
1200
  });
@@ -1058,6 +1202,51 @@ export function EventListView({
1058
1202
  }, [searchQuery, searchIndex]);
1059
1203
 
1060
1204
  if (!events || events.length === 0) {
1205
+ if (isLoading) {
1206
+ return (
1207
+ <div className="h-full flex flex-col overflow-hidden">
1208
+ {/* Skeleton search bar */}
1209
+ <div style={{ padding: 6 }}>
1210
+ <Skeleton style={{ height: 40, borderRadius: 6 }} />
1211
+ </div>
1212
+ {/* Skeleton header */}
1213
+ <div
1214
+ className="flex items-center gap-0 h-10 border-b flex-shrink-0 px-4"
1215
+ style={{ borderColor: 'var(--ds-gray-alpha-200)' }}
1216
+ >
1217
+ <Skeleton className="h-3" style={{ width: 60 }} />
1218
+ <div style={{ flex: 1 }} />
1219
+ <Skeleton className="h-3" style={{ width: 80 }} />
1220
+ <div style={{ flex: 1 }} />
1221
+ <Skeleton className="h-3" style={{ width: 50 }} />
1222
+ <div style={{ flex: 1 }} />
1223
+ <Skeleton className="h-3" style={{ width: 90 }} />
1224
+ <div style={{ flex: 1 }} />
1225
+ <Skeleton className="h-3" style={{ width: 70 }} />
1226
+ </div>
1227
+ {/* Skeleton rows */}
1228
+ <div className="flex-1 overflow-hidden">
1229
+ {Array.from({ length: 8 }, (_, i) => (
1230
+ <div
1231
+ key={i}
1232
+ className="flex items-center gap-3 px-4"
1233
+ style={{ height: 40 }}
1234
+ >
1235
+ <Skeleton
1236
+ className="h-2 w-2 flex-shrink-0"
1237
+ style={{ borderRadius: '50%' }}
1238
+ />
1239
+ <Skeleton className="h-3" style={{ width: 90 }} />
1240
+ <Skeleton className="h-3" style={{ width: 100 }} />
1241
+ <Skeleton className="h-3" style={{ width: 80 }} />
1242
+ <Skeleton className="h-3 flex-1" />
1243
+ <Skeleton className="h-3 flex-1" />
1244
+ </div>
1245
+ ))}
1246
+ </div>
1247
+ </div>
1248
+ );
1249
+ }
1061
1250
  return (
1062
1251
  <div
1063
1252
  className="flex items-center justify-center h-full text-sm"
@@ -1071,8 +1260,15 @@ export function EventListView({
1071
1260
  return (
1072
1261
  <div className="h-full flex flex-col overflow-hidden">
1073
1262
  <style>{`@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}`}</style>
1074
- {/* Search bar */}
1075
- <div style={{ padding: 6, backgroundColor: 'var(--ds-background-100)' }}>
1263
+ {/* Search bar + sort */}
1264
+ <div
1265
+ style={{
1266
+ padding: 6,
1267
+ backgroundColor: 'var(--ds-background-100)',
1268
+ display: 'flex',
1269
+ gap: 6,
1270
+ }}
1271
+ >
1076
1272
  <label
1077
1273
  style={{
1078
1274
  display: 'flex',
@@ -1082,6 +1278,8 @@ export function EventListView({
1082
1278
  boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
1083
1279
  background: 'var(--ds-background-100)',
1084
1280
  height: 40,
1281
+ flex: 1,
1282
+ minWidth: 0,
1085
1283
  }}
1086
1284
  >
1087
1285
  <div
@@ -1120,7 +1318,7 @@ export function EventListView({
1120
1318
  </div>
1121
1319
  <input
1122
1320
  type="search"
1123
- placeholder="Search by event ID or correlation ID…"
1321
+ placeholder="Search by name, event type, or ID…"
1124
1322
  value={searchQuery}
1125
1323
  onChange={(e) => setSearchQuery(e.target.value)}
1126
1324
  style={{
@@ -1136,11 +1334,16 @@ export function EventListView({
1136
1334
  }}
1137
1335
  />
1138
1336
  </label>
1337
+ <MenuDropdown
1338
+ options={SORT_OPTIONS}
1339
+ value={effectiveSortOrder}
1340
+ onChange={handleSortOrderChange}
1341
+ />
1139
1342
  </div>
1140
1343
 
1141
1344
  {/* Header */}
1142
1345
  <div
1143
- className="flex items-center gap-0 text-sm font-medium h-10 border-b flex-shrink-0"
1346
+ className="flex items-center gap-0 text-[13px] font-medium h-10 border-b flex-shrink-0"
1144
1347
  style={{
1145
1348
  borderColor: 'var(--ds-gray-alpha-200)',
1146
1349
  color: 'var(--ds-gray-900)',
@@ -1149,11 +1352,21 @@ export function EventListView({
1149
1352
  >
1150
1353
  <div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
1151
1354
  <div className="w-5 flex-shrink-0" />
1152
- <div className="flex-1 min-w-0 px-4">Time</div>
1153
- <div className="flex-1 min-w-0 px-4">Event Type</div>
1154
- <div className="flex-1 min-w-0 px-4">Name</div>
1155
- <div className="flex-1 min-w-0 px-4">Correlation ID</div>
1156
- <div className="flex-1 min-w-0 px-4">Event ID</div>
1355
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1356
+ Time
1357
+ </div>
1358
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1359
+ Event Type
1360
+ </div>
1361
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1362
+ Name
1363
+ </div>
1364
+ <div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
1365
+ Correlation ID
1366
+ </div>
1367
+ <div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
1368
+ Event ID
1369
+ </div>
1157
1370
  </div>
1158
1371
 
1159
1372
  {/* Virtualized event rows */}
@@ -1169,12 +1382,15 @@ export function EventListView({
1169
1382
  void onLoadMoreEvents?.();
1170
1383
  }}
1171
1384
  itemContent={(index: number) => {
1385
+ const ev = sortedEvents[index];
1172
1386
  return (
1173
1387
  <EventRow
1174
- event={sortedEvents[index]}
1388
+ event={ev}
1175
1389
  index={index}
1176
1390
  isFirst={index === 0}
1177
1391
  isLast={index === sortedEvents.length - 1}
1392
+ isExpanded={expandedEventIds.has(ev.eventId)}
1393
+ onToggleExpand={toggleEventExpanded}
1178
1394
  activeGroupKey={activeGroupKey}
1179
1395
  selectedGroupKey={selectedGroupKey}
1180
1396
  selectedGroupRange={selectedGroupRange}
@@ -1184,14 +1400,17 @@ export function EventListView({
1184
1400
  onSelectGroup={onSelectGroup}
1185
1401
  onHoverGroup={onHoverGroup}
1186
1402
  onLoadEventData={onLoadEventData}
1403
+ cachedEventData={
1404
+ eventDataCacheRef.current.get(ev.eventId) ?? null
1405
+ }
1406
+ onCacheEventData={cacheEventData}
1187
1407
  encryptionKey={encryptionKey}
1188
1408
  />
1189
1409
  );
1190
1410
  }}
1191
1411
  components={{
1192
- Footer: () => (
1193
- <>
1194
- {hasMoreEvents && (
1412
+ Footer: hasMoreEvents
1413
+ ? () => (
1195
1414
  <div className="px-3 pt-3 flex justify-center">
1196
1415
  <button
1197
1416
  type="button"
@@ -1209,22 +1428,24 @@ export function EventListView({
1209
1428
  : 'Load more'}
1210
1429
  </button>
1211
1430
  </div>
1212
- )}
1213
- <div
1214
- className="mt-4 pt-3 border-t text-xs px-3"
1215
- style={{
1216
- borderColor: 'var(--ds-gray-alpha-200)',
1217
- color: 'var(--ds-gray-900)',
1218
- }}
1219
- >
1220
- {sortedEvents.length} event
1221
- {sortedEvents.length !== 1 ? 's' : ''} total
1222
- </div>
1223
- </>
1224
- ),
1431
+ )
1432
+ : undefined,
1225
1433
  }}
1226
1434
  style={{ flex: 1, minHeight: 0 }}
1227
1435
  />
1436
+
1437
+ {/* Fixed footer — always at the bottom of the visible area */}
1438
+ <div
1439
+ className="flex-shrink-0 border-t text-xs px-3 py-2"
1440
+ style={{
1441
+ borderColor: 'var(--ds-gray-alpha-200)',
1442
+ color: 'var(--ds-gray-900)',
1443
+ backgroundColor: 'var(--ds-background-100)',
1444
+ }}
1445
+ >
1446
+ {sortedEvents.length} event
1447
+ {sortedEvents.length !== 1 ? 's' : ''} total
1448
+ </div>
1228
1449
  </div>
1229
1450
  );
1230
1451
  }