@workflow/web-shared 4.1.0-beta.63 → 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.
- package/README.md +4 -0
- package/dist/components/event-list-view.d.ts +8 -1
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +210 -89
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/ui/error-stack-block.d.ts +3 -4
- package/dist/components/ui/error-stack-block.d.ts.map +1 -1
- package/dist/components/ui/error-stack-block.js +18 -9
- package/dist/components/ui/error-stack-block.js.map +1 -1
- package/dist/components/ui/menu-dropdown.d.ts +16 -0
- package/dist/components/ui/menu-dropdown.d.ts.map +1 -0
- package/dist/components/ui/menu-dropdown.js +50 -0
- package/dist/components/ui/menu-dropdown.js.map +1 -0
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +3 -3
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/package.json +3 -3
- package/src/components/event-list-view.tsx +307 -90
- package/src/components/index.ts +1 -0
- package/src/components/ui/error-stack-block.tsx +26 -16
- package/src/components/ui/menu-dropdown.tsx +114 -0
- package/src/components/workflow-trace-view.tsx +9 -3
|
@@ -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
|
/**
|
|
@@ -378,9 +379,11 @@ function TreeGutter({
|
|
|
378
379
|
function CopyableCell({
|
|
379
380
|
value,
|
|
380
381
|
className,
|
|
382
|
+
style: styleProp,
|
|
381
383
|
}: {
|
|
382
384
|
value: string;
|
|
383
385
|
className?: string;
|
|
386
|
+
style?: React.CSSProperties;
|
|
384
387
|
}): ReactNode {
|
|
385
388
|
const [copied, setCopied] = useState(false);
|
|
386
389
|
const resetCopiedTimeoutRef = useRef<number | null>(null);
|
|
@@ -412,7 +415,8 @@ function CopyableCell({
|
|
|
412
415
|
|
|
413
416
|
return (
|
|
414
417
|
<div
|
|
415
|
-
className={`group/copy flex items-center gap-1
|
|
418
|
+
className={`group/copy flex items-center gap-1 min-w-0 px-4 ${className ?? ''}`}
|
|
419
|
+
style={styleProp}
|
|
416
420
|
>
|
|
417
421
|
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
|
|
418
422
|
{value || '-'}
|
|
@@ -583,6 +587,15 @@ function PayloadBlock({
|
|
|
583
587
|
);
|
|
584
588
|
}
|
|
585
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
|
+
|
|
586
599
|
// ──────────────────────────────────────────────────────────────────────────
|
|
587
600
|
// Event row
|
|
588
601
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -596,6 +609,13 @@ interface EventsListProps {
|
|
|
596
609
|
onLoadMoreEvents?: () => Promise<void> | void;
|
|
597
610
|
/** When provided, signals that decryption is active (triggers re-load of expanded events) */
|
|
598
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;
|
|
599
619
|
}
|
|
600
620
|
|
|
601
621
|
function EventRow({
|
|
@@ -603,6 +623,8 @@ function EventRow({
|
|
|
603
623
|
index,
|
|
604
624
|
isFirst,
|
|
605
625
|
isLast,
|
|
626
|
+
isExpanded,
|
|
627
|
+
onToggleExpand,
|
|
606
628
|
activeGroupKey,
|
|
607
629
|
selectedGroupKey,
|
|
608
630
|
selectedGroupRange,
|
|
@@ -612,12 +634,16 @@ function EventRow({
|
|
|
612
634
|
onSelectGroup,
|
|
613
635
|
onHoverGroup,
|
|
614
636
|
onLoadEventData,
|
|
637
|
+
cachedEventData,
|
|
638
|
+
onCacheEventData,
|
|
615
639
|
encryptionKey,
|
|
616
640
|
}: {
|
|
617
641
|
event: Event;
|
|
618
642
|
index: number;
|
|
619
643
|
isFirst: boolean;
|
|
620
644
|
isLast: boolean;
|
|
645
|
+
isExpanded: boolean;
|
|
646
|
+
onToggleExpand: (eventId: string) => void;
|
|
621
647
|
activeGroupKey?: string;
|
|
622
648
|
selectedGroupKey?: string;
|
|
623
649
|
selectedGroupRange: { first: number; last: number } | null;
|
|
@@ -627,24 +653,22 @@ function EventRow({
|
|
|
627
653
|
onSelectGroup: (groupKey: string | undefined) => void;
|
|
628
654
|
onHoverGroup: (groupKey: string | undefined) => void;
|
|
629
655
|
onLoadEventData?: (event: Event) => Promise<unknown | null>;
|
|
656
|
+
cachedEventData: unknown | null;
|
|
657
|
+
onCacheEventData: (eventId: string, data: unknown) => void;
|
|
630
658
|
encryptionKey?: Uint8Array;
|
|
631
659
|
}) {
|
|
632
|
-
const [isExpanded, setIsExpanded] = useState(false);
|
|
633
660
|
const [isLoading, setIsLoading] = useState(false);
|
|
634
|
-
const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
|
|
661
|
+
const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
|
|
662
|
+
cachedEventData
|
|
663
|
+
);
|
|
635
664
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
636
|
-
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
event.correlationId ??
|
|
640
|
-
(isRunLevel(event.eventType) ? '__run__' : undefined);
|
|
665
|
+
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(
|
|
666
|
+
cachedEventData !== null
|
|
667
|
+
);
|
|
641
668
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
setIsExpanded(false);
|
|
646
|
-
}
|
|
647
|
-
}, [selectedGroupKey, rowGroupKey]);
|
|
669
|
+
const rowGroupKey = isRunLevel(event.eventType)
|
|
670
|
+
? '__run__'
|
|
671
|
+
: (event.correlationId ?? undefined);
|
|
648
672
|
|
|
649
673
|
const statusDotColor = getStatusDotColor(event.eventType);
|
|
650
674
|
const createdAt = new Date(event.createdAt);
|
|
@@ -686,9 +710,10 @@ function EventRow({
|
|
|
686
710
|
setLoadError('Event details unavailable');
|
|
687
711
|
return;
|
|
688
712
|
}
|
|
689
|
-
const
|
|
690
|
-
if (
|
|
691
|
-
setLoadedEventData(
|
|
713
|
+
const data = await onLoadEventData(event);
|
|
714
|
+
if (data !== null && data !== undefined) {
|
|
715
|
+
setLoadedEventData(data);
|
|
716
|
+
onCacheEventData(event.eventId, data);
|
|
692
717
|
}
|
|
693
718
|
} catch (err) {
|
|
694
719
|
setLoadError(
|
|
@@ -698,7 +723,27 @@ function EventRow({
|
|
|
698
723
|
setIsLoading(false);
|
|
699
724
|
setHasAttemptedLoad(true);
|
|
700
725
|
}
|
|
701
|
-
}, [
|
|
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
|
+
}, []);
|
|
702
747
|
|
|
703
748
|
// When encryption key changes and this event was previously loaded,
|
|
704
749
|
// re-load to get decrypted data
|
|
@@ -710,6 +755,7 @@ function EventRow({
|
|
|
710
755
|
.then((data) => {
|
|
711
756
|
if (data !== null && data !== undefined) {
|
|
712
757
|
setLoadedEventData(data);
|
|
758
|
+
onCacheEventData(event.eventId, data);
|
|
713
759
|
}
|
|
714
760
|
setHasAttemptedLoad(true);
|
|
715
761
|
})
|
|
@@ -720,25 +766,23 @@ function EventRow({
|
|
|
720
766
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
721
767
|
}, [encryptionKey]);
|
|
722
768
|
|
|
723
|
-
const handleExpandToggle = useCallback(
|
|
724
|
-
(e: ReactMouseEvent) => {
|
|
725
|
-
e.stopPropagation();
|
|
726
|
-
const newExpanded = !isExpanded;
|
|
727
|
-
setIsExpanded(newExpanded);
|
|
728
|
-
if (newExpanded && loadedEventData === null && !hasExistingEventData) {
|
|
729
|
-
loadEventDetails();
|
|
730
|
-
}
|
|
731
|
-
},
|
|
732
|
-
[isExpanded, loadedEventData, hasExistingEventData, loadEventDetails]
|
|
733
|
-
);
|
|
734
|
-
|
|
735
769
|
const handleRowClick = useCallback(() => {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
770
|
+
onSelectGroup(rowGroupKey === selectedGroupKey ? undefined : rowGroupKey);
|
|
771
|
+
onToggleExpand(event.eventId);
|
|
772
|
+
if (!isExpanded && loadedEventData === null && !hasExistingEventData) {
|
|
773
|
+
loadEventDetails();
|
|
740
774
|
}
|
|
741
|
-
}, [
|
|
775
|
+
}, [
|
|
776
|
+
selectedGroupKey,
|
|
777
|
+
rowGroupKey,
|
|
778
|
+
onSelectGroup,
|
|
779
|
+
onToggleExpand,
|
|
780
|
+
event.eventId,
|
|
781
|
+
isExpanded,
|
|
782
|
+
loadedEventData,
|
|
783
|
+
hasExistingEventData,
|
|
784
|
+
loadEventDetails,
|
|
785
|
+
]);
|
|
742
786
|
|
|
743
787
|
const eventData = hasExistingEventData
|
|
744
788
|
? (event as Event & { eventData: unknown }).eventData
|
|
@@ -760,7 +804,7 @@ function EventRow({
|
|
|
760
804
|
onKeyDown={(e) => {
|
|
761
805
|
if (e.key === 'Enter' || e.key === ' ') handleRowClick();
|
|
762
806
|
}}
|
|
763
|
-
className="w-full text-left flex items-center gap-0 text-
|
|
807
|
+
className="w-full text-left flex items-center gap-0 text-[13px] hover:bg-[var(--ds-gray-alpha-100)] transition-colors cursor-pointer"
|
|
764
808
|
style={{ minHeight: 40 }}
|
|
765
809
|
>
|
|
766
810
|
<TreeGutter
|
|
@@ -781,36 +825,32 @@ function EventRow({
|
|
|
781
825
|
className="flex items-center flex-1 min-w-0"
|
|
782
826
|
style={{ opacity: contentOpacity, transition: 'opacity 150ms' }}
|
|
783
827
|
>
|
|
784
|
-
{/* Expand chevron
|
|
785
|
-
<
|
|
786
|
-
|
|
787
|
-
onClick={handleExpandToggle}
|
|
788
|
-
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"
|
|
789
831
|
style={{
|
|
790
|
-
|
|
791
|
-
border: '1px solid var(--ds-gray-alpha-400)',
|
|
832
|
+
border: '1px solid var(--ds-gray-400)',
|
|
792
833
|
}}
|
|
793
|
-
aria-label={isExpanded ? 'Collapse details' : 'Expand details'}
|
|
794
834
|
>
|
|
795
835
|
<ChevronRight
|
|
796
836
|
className="h-3 w-3 transition-transform"
|
|
797
837
|
style={{
|
|
798
|
-
color: 'var(--ds-gray-
|
|
838
|
+
color: 'var(--ds-gray-900)',
|
|
799
839
|
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
800
840
|
}}
|
|
801
841
|
/>
|
|
802
|
-
</
|
|
842
|
+
</div>
|
|
803
843
|
|
|
804
844
|
{/* Time */}
|
|
805
845
|
<div
|
|
806
|
-
className="
|
|
807
|
-
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%' }}
|
|
808
848
|
>
|
|
809
849
|
{formatEventTime(createdAt)}
|
|
810
850
|
</div>
|
|
811
851
|
|
|
812
852
|
{/* Event Type */}
|
|
813
|
-
<div className="
|
|
853
|
+
<div className="font-medium min-w-0 px-4" style={{ flex: '2 1 0%' }}>
|
|
814
854
|
<span
|
|
815
855
|
className="inline-flex items-center gap-1.5"
|
|
816
856
|
style={{ color: 'var(--ds-gray-900)' }}
|
|
@@ -852,7 +892,8 @@ function EventRow({
|
|
|
852
892
|
|
|
853
893
|
{/* Name */}
|
|
854
894
|
<div
|
|
855
|
-
className="
|
|
895
|
+
className="min-w-0 px-4 overflow-hidden text-ellipsis whitespace-nowrap"
|
|
896
|
+
style={{ flex: '2 1 0%' }}
|
|
856
897
|
title={eventName !== '-' ? eventName : undefined}
|
|
857
898
|
>
|
|
858
899
|
{eventName}
|
|
@@ -861,11 +902,16 @@ function EventRow({
|
|
|
861
902
|
{/* Correlation ID */}
|
|
862
903
|
<CopyableCell
|
|
863
904
|
value={event.correlationId || ''}
|
|
864
|
-
className="font-mono
|
|
905
|
+
className="font-mono"
|
|
906
|
+
style={{ flex: '3 1 0%' }}
|
|
865
907
|
/>
|
|
866
908
|
|
|
867
909
|
{/* Event ID */}
|
|
868
|
-
<CopyableCell
|
|
910
|
+
<CopyableCell
|
|
911
|
+
value={event.eventId}
|
|
912
|
+
className="font-mono"
|
|
913
|
+
style={{ flex: '3 1 0%' }}
|
|
914
|
+
/>
|
|
869
915
|
</div>
|
|
870
916
|
</div>
|
|
871
917
|
|
|
@@ -971,14 +1017,34 @@ export function EventListView({
|
|
|
971
1017
|
isLoadingMoreEvents = false,
|
|
972
1018
|
onLoadMoreEvents,
|
|
973
1019
|
encryptionKey,
|
|
1020
|
+
isLoading = false,
|
|
1021
|
+
sortOrder: sortOrderProp,
|
|
1022
|
+
onSortOrderChange,
|
|
974
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
|
+
|
|
975
1039
|
const sortedEvents = useMemo(() => {
|
|
976
1040
|
if (!events || events.length === 0) return [];
|
|
1041
|
+
const dir = effectiveSortOrder === 'desc' ? -1 : 1;
|
|
977
1042
|
return [...events].sort(
|
|
978
1043
|
(a, b) =>
|
|
979
|
-
|
|
1044
|
+
dir *
|
|
1045
|
+
(new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
|
980
1046
|
);
|
|
981
|
-
}, [events]);
|
|
1047
|
+
}, [events, effectiveSortOrder]);
|
|
982
1048
|
|
|
983
1049
|
const { correlationNameMap, workflowName } = useMemo(
|
|
984
1050
|
() => buildNameMaps(events ?? null, run ?? null),
|
|
@@ -1005,6 +1071,58 @@ export function EventListView({
|
|
|
1005
1071
|
|
|
1006
1072
|
const activeGroupKey = selectedGroupKey ?? hoveredGroupKey;
|
|
1007
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
|
+
|
|
1008
1126
|
// Compute the row-index range for the active group's connecting lane line.
|
|
1009
1127
|
// Only applies to non-run groups (step/hook/wait correlations).
|
|
1010
1128
|
const selectedGroupRange = useMemo(() => {
|
|
@@ -1025,24 +1143,34 @@ export function EventListView({
|
|
|
1025
1143
|
|
|
1026
1144
|
const searchIndex = useMemo(() => {
|
|
1027
1145
|
const entries: {
|
|
1028
|
-
|
|
1146
|
+
fields: string[];
|
|
1029
1147
|
groupKey?: string;
|
|
1030
1148
|
eventId: string;
|
|
1031
1149
|
index: number;
|
|
1032
1150
|
}[] = [];
|
|
1033
1151
|
for (let i = 0; i < sortedEvents.length; i++) {
|
|
1034
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
|
+
: '';
|
|
1035
1159
|
entries.push({
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
ev.correlationId ??
|
|
1039
|
-
|
|
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),
|
|
1040
1168
|
eventId: ev.eventId,
|
|
1041
1169
|
index: i,
|
|
1042
1170
|
});
|
|
1043
1171
|
}
|
|
1044
1172
|
return entries;
|
|
1045
|
-
}, [sortedEvents]);
|
|
1173
|
+
}, [sortedEvents, correlationNameMap, workflowName]);
|
|
1046
1174
|
|
|
1047
1175
|
useEffect(() => {
|
|
1048
1176
|
const q = searchQuery.trim().toLowerCase();
|
|
@@ -1050,11 +1178,23 @@ export function EventListView({
|
|
|
1050
1178
|
setSelectedGroupKey(undefined);
|
|
1051
1179
|
return;
|
|
1052
1180
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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);
|
|
1056
1196
|
virtuosoRef.current?.scrollToIndex({
|
|
1057
|
-
index:
|
|
1197
|
+
index: bestMatch.index,
|
|
1058
1198
|
align: 'center',
|
|
1059
1199
|
behavior: 'smooth',
|
|
1060
1200
|
});
|
|
@@ -1062,6 +1202,51 @@ export function EventListView({
|
|
|
1062
1202
|
}, [searchQuery, searchIndex]);
|
|
1063
1203
|
|
|
1064
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
|
+
}
|
|
1065
1250
|
return (
|
|
1066
1251
|
<div
|
|
1067
1252
|
className="flex items-center justify-center h-full text-sm"
|
|
@@ -1075,8 +1260,15 @@ export function EventListView({
|
|
|
1075
1260
|
return (
|
|
1076
1261
|
<div className="h-full flex flex-col overflow-hidden">
|
|
1077
1262
|
<style>{`@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}`}</style>
|
|
1078
|
-
{/* Search bar */}
|
|
1079
|
-
<div
|
|
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
|
+
>
|
|
1080
1272
|
<label
|
|
1081
1273
|
style={{
|
|
1082
1274
|
display: 'flex',
|
|
@@ -1086,6 +1278,8 @@ export function EventListView({
|
|
|
1086
1278
|
boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
|
|
1087
1279
|
background: 'var(--ds-background-100)',
|
|
1088
1280
|
height: 40,
|
|
1281
|
+
flex: 1,
|
|
1282
|
+
minWidth: 0,
|
|
1089
1283
|
}}
|
|
1090
1284
|
>
|
|
1091
1285
|
<div
|
|
@@ -1124,7 +1318,7 @@ export function EventListView({
|
|
|
1124
1318
|
</div>
|
|
1125
1319
|
<input
|
|
1126
1320
|
type="search"
|
|
1127
|
-
placeholder="Search by event
|
|
1321
|
+
placeholder="Search by name, event type, or ID…"
|
|
1128
1322
|
value={searchQuery}
|
|
1129
1323
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
1130
1324
|
style={{
|
|
@@ -1140,11 +1334,16 @@ export function EventListView({
|
|
|
1140
1334
|
}}
|
|
1141
1335
|
/>
|
|
1142
1336
|
</label>
|
|
1337
|
+
<MenuDropdown
|
|
1338
|
+
options={SORT_OPTIONS}
|
|
1339
|
+
value={effectiveSortOrder}
|
|
1340
|
+
onChange={handleSortOrderChange}
|
|
1341
|
+
/>
|
|
1143
1342
|
</div>
|
|
1144
1343
|
|
|
1145
1344
|
{/* Header */}
|
|
1146
1345
|
<div
|
|
1147
|
-
className="flex items-center gap-0 text-
|
|
1346
|
+
className="flex items-center gap-0 text-[13px] font-medium h-10 border-b flex-shrink-0"
|
|
1148
1347
|
style={{
|
|
1149
1348
|
borderColor: 'var(--ds-gray-alpha-200)',
|
|
1150
1349
|
color: 'var(--ds-gray-900)',
|
|
@@ -1153,11 +1352,21 @@ export function EventListView({
|
|
|
1153
1352
|
>
|
|
1154
1353
|
<div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
|
|
1155
1354
|
<div className="w-5 flex-shrink-0" />
|
|
1156
|
-
<div className="
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
<div className="
|
|
1160
|
-
|
|
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>
|
|
1161
1370
|
</div>
|
|
1162
1371
|
|
|
1163
1372
|
{/* Virtualized event rows */}
|
|
@@ -1173,12 +1382,15 @@ export function EventListView({
|
|
|
1173
1382
|
void onLoadMoreEvents?.();
|
|
1174
1383
|
}}
|
|
1175
1384
|
itemContent={(index: number) => {
|
|
1385
|
+
const ev = sortedEvents[index];
|
|
1176
1386
|
return (
|
|
1177
1387
|
<EventRow
|
|
1178
|
-
event={
|
|
1388
|
+
event={ev}
|
|
1179
1389
|
index={index}
|
|
1180
1390
|
isFirst={index === 0}
|
|
1181
1391
|
isLast={index === sortedEvents.length - 1}
|
|
1392
|
+
isExpanded={expandedEventIds.has(ev.eventId)}
|
|
1393
|
+
onToggleExpand={toggleEventExpanded}
|
|
1182
1394
|
activeGroupKey={activeGroupKey}
|
|
1183
1395
|
selectedGroupKey={selectedGroupKey}
|
|
1184
1396
|
selectedGroupRange={selectedGroupRange}
|
|
@@ -1188,14 +1400,17 @@ export function EventListView({
|
|
|
1188
1400
|
onSelectGroup={onSelectGroup}
|
|
1189
1401
|
onHoverGroup={onHoverGroup}
|
|
1190
1402
|
onLoadEventData={onLoadEventData}
|
|
1403
|
+
cachedEventData={
|
|
1404
|
+
eventDataCacheRef.current.get(ev.eventId) ?? null
|
|
1405
|
+
}
|
|
1406
|
+
onCacheEventData={cacheEventData}
|
|
1191
1407
|
encryptionKey={encryptionKey}
|
|
1192
1408
|
/>
|
|
1193
1409
|
);
|
|
1194
1410
|
}}
|
|
1195
1411
|
components={{
|
|
1196
|
-
Footer:
|
|
1197
|
-
|
|
1198
|
-
{hasMoreEvents && (
|
|
1412
|
+
Footer: hasMoreEvents
|
|
1413
|
+
? () => (
|
|
1199
1414
|
<div className="px-3 pt-3 flex justify-center">
|
|
1200
1415
|
<button
|
|
1201
1416
|
type="button"
|
|
@@ -1213,22 +1428,24 @@ export function EventListView({
|
|
|
1213
1428
|
: 'Load more'}
|
|
1214
1429
|
</button>
|
|
1215
1430
|
</div>
|
|
1216
|
-
)
|
|
1217
|
-
|
|
1218
|
-
className="mt-4 pt-3 border-t text-xs px-3"
|
|
1219
|
-
style={{
|
|
1220
|
-
borderColor: 'var(--ds-gray-alpha-200)',
|
|
1221
|
-
color: 'var(--ds-gray-900)',
|
|
1222
|
-
}}
|
|
1223
|
-
>
|
|
1224
|
-
{sortedEvents.length} event
|
|
1225
|
-
{sortedEvents.length !== 1 ? 's' : ''} total
|
|
1226
|
-
</div>
|
|
1227
|
-
</>
|
|
1228
|
-
),
|
|
1431
|
+
)
|
|
1432
|
+
: undefined,
|
|
1229
1433
|
}}
|
|
1230
1434
|
style={{ flex: 1, minHeight: 0 }}
|
|
1231
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>
|
|
1232
1449
|
</div>
|
|
1233
1450
|
);
|
|
1234
1451
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -22,4 +22,5 @@ export type {
|
|
|
22
22
|
export { type StreamChunk, StreamViewer } from './stream-viewer';
|
|
23
23
|
export type { Span, SpanEvent } from './trace-viewer/types';
|
|
24
24
|
export { DataInspector, type DataInspectorProps } from './ui/data-inspector';
|
|
25
|
+
export { MenuDropdown, type MenuDropdownOption } from './ui/menu-dropdown';
|
|
25
26
|
export { WorkflowTraceViewer } from './workflow-trace-view';
|