@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.
- package/README.md +4 -0
- package/dist/components/event-list-view.d.ts +12 -1
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +233 -91
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +4 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +4 -14
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/stream-viewer.d.ts +3 -1
- package/dist/components/stream-viewer.d.ts.map +1 -1
- package/dist/components/stream-viewer.js +23 -28
- package/dist/components/stream-viewer.js.map +1 -1
- package/dist/components/ui/decrypt-button.d.ts +15 -0
- package/dist/components/ui/decrypt-button.d.ts.map +1 -0
- package/dist/components/ui/decrypt-button.js +12 -0
- package/dist/components/ui/decrypt-button.js.map +1 -0
- 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/load-more-button.d.ts +13 -0
- package/dist/components/ui/load-more-button.d.ts.map +1 -0
- package/dist/components/ui/load-more-button.js +12 -0
- package/dist/components/ui/load-more-button.js.map +1 -0
- 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 +46 -0
- package/dist/components/ui/menu-dropdown.js.map +1 -0
- package/dist/components/ui/spinner.d.ts +9 -0
- package/dist/components/ui/spinner.d.ts.map +1 -0
- package/dist/components/ui/spinner.js +57 -0
- package/dist/components/ui/spinner.js.map +1 -0
- package/dist/components/workflow-trace-view.d.ts +3 -1
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +7 -6
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/package.json +3 -3
- package/src/components/event-list-view.tsx +398 -141
- package/src/components/index.ts +4 -0
- package/src/components/sidebar/entity-detail-panel.tsx +9 -25
- package/src/components/stream-viewer.tsx +52 -63
- package/src/components/ui/decrypt-button.tsx +69 -0
- package/src/components/ui/error-stack-block.tsx +26 -16
- package/src/components/ui/load-more-button.tsx +38 -0
- package/src/components/ui/menu-dropdown.tsx +111 -0
- package/src/components/ui/spinner.tsx +76 -0
- package/src/components/workflow-trace-view.tsx +15 -22
|
@@ -6,12 +6,16 @@ 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';
|
|
8
8
|
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
|
|
9
|
+
import { isEncryptedMarker } from '../lib/hydration';
|
|
10
|
+
import { DecryptButton } from './ui/decrypt-button';
|
|
9
11
|
import { formatDuration } from '../lib/utils';
|
|
10
12
|
import { DataInspector } from './ui/data-inspector';
|
|
11
13
|
import {
|
|
12
14
|
ErrorStackBlock,
|
|
13
15
|
isStructuredErrorWithStack,
|
|
14
16
|
} from './ui/error-stack-block';
|
|
17
|
+
import { LoadMoreButton } from './ui/load-more-button';
|
|
18
|
+
import { MenuDropdown } from './ui/menu-dropdown';
|
|
15
19
|
import { Skeleton } from './ui/skeleton';
|
|
16
20
|
|
|
17
21
|
/**
|
|
@@ -378,9 +382,11 @@ function TreeGutter({
|
|
|
378
382
|
function CopyableCell({
|
|
379
383
|
value,
|
|
380
384
|
className,
|
|
385
|
+
style: styleProp,
|
|
381
386
|
}: {
|
|
382
387
|
value: string;
|
|
383
388
|
className?: string;
|
|
389
|
+
style?: React.CSSProperties;
|
|
384
390
|
}): ReactNode {
|
|
385
391
|
const [copied, setCopied] = useState(false);
|
|
386
392
|
const resetCopiedTimeoutRef = useRef<number | null>(null);
|
|
@@ -412,7 +418,8 @@ function CopyableCell({
|
|
|
412
418
|
|
|
413
419
|
return (
|
|
414
420
|
<div
|
|
415
|
-
className={`group/copy flex items-center gap-1
|
|
421
|
+
className={`group/copy flex items-center gap-1 min-w-0 px-4 ${className ?? ''}`}
|
|
422
|
+
style={styleProp}
|
|
416
423
|
>
|
|
417
424
|
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
|
|
418
425
|
{value || '-'}
|
|
@@ -583,6 +590,39 @@ function PayloadBlock({
|
|
|
583
590
|
);
|
|
584
591
|
}
|
|
585
592
|
|
|
593
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
594
|
+
// Sort options for the events list
|
|
595
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
596
|
+
|
|
597
|
+
const SORT_OPTIONS = [
|
|
598
|
+
{ value: 'desc' as const, label: 'Newest' },
|
|
599
|
+
{ value: 'asc' as const, label: 'Oldest' },
|
|
600
|
+
];
|
|
601
|
+
|
|
602
|
+
function RowsSkeleton() {
|
|
603
|
+
return (
|
|
604
|
+
<div className="flex-1 overflow-hidden">
|
|
605
|
+
{Array.from({ length: 8 }, (_, i) => (
|
|
606
|
+
<div
|
|
607
|
+
key={i}
|
|
608
|
+
className="flex items-center gap-3 px-4"
|
|
609
|
+
style={{ height: 40 }}
|
|
610
|
+
>
|
|
611
|
+
<Skeleton
|
|
612
|
+
className="h-2 w-2 flex-shrink-0"
|
|
613
|
+
style={{ borderRadius: '50%' }}
|
|
614
|
+
/>
|
|
615
|
+
<Skeleton className="h-3" style={{ width: 90 }} />
|
|
616
|
+
<Skeleton className="h-3" style={{ width: 100 }} />
|
|
617
|
+
<Skeleton className="h-3" style={{ width: 80 }} />
|
|
618
|
+
<Skeleton className="h-3 flex-1" />
|
|
619
|
+
<Skeleton className="h-3 flex-1" />
|
|
620
|
+
</div>
|
|
621
|
+
))}
|
|
622
|
+
</div>
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
586
626
|
// ──────────────────────────────────────────────────────────────────────────
|
|
587
627
|
// Event row
|
|
588
628
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -596,6 +636,17 @@ interface EventsListProps {
|
|
|
596
636
|
onLoadMoreEvents?: () => Promise<void> | void;
|
|
597
637
|
/** When provided, signals that decryption is active (triggers re-load of expanded events) */
|
|
598
638
|
encryptionKey?: Uint8Array;
|
|
639
|
+
/** When true, shows a loading state instead of "No events found" for empty lists */
|
|
640
|
+
isLoading?: boolean;
|
|
641
|
+
/** Sort order for events. Defaults to 'asc'. */
|
|
642
|
+
sortOrder?: 'asc' | 'desc';
|
|
643
|
+
/** Called when the user changes sort order. When provided, the sort dropdown is shown
|
|
644
|
+
* and the parent is expected to refetch from the API with the new order. */
|
|
645
|
+
onSortOrderChange?: (order: 'asc' | 'desc') => void;
|
|
646
|
+
/** Called when the user clicks the Decrypt button. */
|
|
647
|
+
onDecrypt?: () => void;
|
|
648
|
+
/** Whether the encryption key is currently being fetched. */
|
|
649
|
+
isDecrypting?: boolean;
|
|
599
650
|
}
|
|
600
651
|
|
|
601
652
|
function EventRow({
|
|
@@ -603,6 +654,8 @@ function EventRow({
|
|
|
603
654
|
index,
|
|
604
655
|
isFirst,
|
|
605
656
|
isLast,
|
|
657
|
+
isExpanded,
|
|
658
|
+
onToggleExpand,
|
|
606
659
|
activeGroupKey,
|
|
607
660
|
selectedGroupKey,
|
|
608
661
|
selectedGroupRange,
|
|
@@ -612,12 +665,16 @@ function EventRow({
|
|
|
612
665
|
onSelectGroup,
|
|
613
666
|
onHoverGroup,
|
|
614
667
|
onLoadEventData,
|
|
668
|
+
cachedEventData,
|
|
669
|
+
onCacheEventData,
|
|
615
670
|
encryptionKey,
|
|
616
671
|
}: {
|
|
617
672
|
event: Event;
|
|
618
673
|
index: number;
|
|
619
674
|
isFirst: boolean;
|
|
620
675
|
isLast: boolean;
|
|
676
|
+
isExpanded: boolean;
|
|
677
|
+
onToggleExpand: (eventId: string) => void;
|
|
621
678
|
activeGroupKey?: string;
|
|
622
679
|
selectedGroupKey?: string;
|
|
623
680
|
selectedGroupRange: { first: number; last: number } | null;
|
|
@@ -627,24 +684,22 @@ function EventRow({
|
|
|
627
684
|
onSelectGroup: (groupKey: string | undefined) => void;
|
|
628
685
|
onHoverGroup: (groupKey: string | undefined) => void;
|
|
629
686
|
onLoadEventData?: (event: Event) => Promise<unknown | null>;
|
|
687
|
+
cachedEventData: unknown | null;
|
|
688
|
+
onCacheEventData: (eventId: string, data: unknown) => void;
|
|
630
689
|
encryptionKey?: Uint8Array;
|
|
631
690
|
}) {
|
|
632
|
-
const [isExpanded, setIsExpanded] = useState(false);
|
|
633
691
|
const [isLoading, setIsLoading] = useState(false);
|
|
634
|
-
const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
|
|
692
|
+
const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
|
|
693
|
+
cachedEventData
|
|
694
|
+
);
|
|
635
695
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
636
|
-
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
event.correlationId ??
|
|
640
|
-
(isRunLevel(event.eventType) ? '__run__' : undefined);
|
|
696
|
+
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(
|
|
697
|
+
cachedEventData !== null
|
|
698
|
+
);
|
|
641
699
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
setIsExpanded(false);
|
|
646
|
-
}
|
|
647
|
-
}, [selectedGroupKey, rowGroupKey]);
|
|
700
|
+
const rowGroupKey = isRunLevel(event.eventType)
|
|
701
|
+
? '__run__'
|
|
702
|
+
: (event.correlationId ?? undefined);
|
|
648
703
|
|
|
649
704
|
const statusDotColor = getStatusDotColor(event.eventType);
|
|
650
705
|
const createdAt = new Date(event.createdAt);
|
|
@@ -686,9 +741,10 @@ function EventRow({
|
|
|
686
741
|
setLoadError('Event details unavailable');
|
|
687
742
|
return;
|
|
688
743
|
}
|
|
689
|
-
const
|
|
690
|
-
if (
|
|
691
|
-
setLoadedEventData(
|
|
744
|
+
const data = await onLoadEventData(event);
|
|
745
|
+
if (data !== null && data !== undefined) {
|
|
746
|
+
setLoadedEventData(data);
|
|
747
|
+
onCacheEventData(event.eventId, data);
|
|
692
748
|
}
|
|
693
749
|
} catch (err) {
|
|
694
750
|
setLoadError(
|
|
@@ -698,7 +754,27 @@ function EventRow({
|
|
|
698
754
|
setIsLoading(false);
|
|
699
755
|
setHasAttemptedLoad(true);
|
|
700
756
|
}
|
|
701
|
-
}, [
|
|
757
|
+
}, [
|
|
758
|
+
event,
|
|
759
|
+
loadedEventData,
|
|
760
|
+
hasExistingEventData,
|
|
761
|
+
onLoadEventData,
|
|
762
|
+
onCacheEventData,
|
|
763
|
+
]);
|
|
764
|
+
|
|
765
|
+
// Auto-load event data when remounting in expanded state without cached data
|
|
766
|
+
useEffect(() => {
|
|
767
|
+
if (
|
|
768
|
+
isExpanded &&
|
|
769
|
+
loadedEventData === null &&
|
|
770
|
+
!hasExistingEventData &&
|
|
771
|
+
!isLoading &&
|
|
772
|
+
!hasAttemptedLoad
|
|
773
|
+
) {
|
|
774
|
+
loadEventDetails();
|
|
775
|
+
}
|
|
776
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
777
|
+
}, []);
|
|
702
778
|
|
|
703
779
|
// When encryption key changes and this event was previously loaded,
|
|
704
780
|
// re-load to get decrypted data
|
|
@@ -710,6 +786,7 @@ function EventRow({
|
|
|
710
786
|
.then((data) => {
|
|
711
787
|
if (data !== null && data !== undefined) {
|
|
712
788
|
setLoadedEventData(data);
|
|
789
|
+
onCacheEventData(event.eventId, data);
|
|
713
790
|
}
|
|
714
791
|
setHasAttemptedLoad(true);
|
|
715
792
|
})
|
|
@@ -720,25 +797,23 @@ function EventRow({
|
|
|
720
797
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
721
798
|
}, [encryptionKey]);
|
|
722
799
|
|
|
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
800
|
const handleRowClick = useCallback(() => {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
801
|
+
onSelectGroup(rowGroupKey === selectedGroupKey ? undefined : rowGroupKey);
|
|
802
|
+
onToggleExpand(event.eventId);
|
|
803
|
+
if (!isExpanded && loadedEventData === null && !hasExistingEventData) {
|
|
804
|
+
loadEventDetails();
|
|
740
805
|
}
|
|
741
|
-
}, [
|
|
806
|
+
}, [
|
|
807
|
+
selectedGroupKey,
|
|
808
|
+
rowGroupKey,
|
|
809
|
+
onSelectGroup,
|
|
810
|
+
onToggleExpand,
|
|
811
|
+
event.eventId,
|
|
812
|
+
isExpanded,
|
|
813
|
+
loadedEventData,
|
|
814
|
+
hasExistingEventData,
|
|
815
|
+
loadEventDetails,
|
|
816
|
+
]);
|
|
742
817
|
|
|
743
818
|
const eventData = hasExistingEventData
|
|
744
819
|
? (event as Event & { eventData: unknown }).eventData
|
|
@@ -760,7 +835,7 @@ function EventRow({
|
|
|
760
835
|
onKeyDown={(e) => {
|
|
761
836
|
if (e.key === 'Enter' || e.key === ' ') handleRowClick();
|
|
762
837
|
}}
|
|
763
|
-
className="w-full text-left flex items-center gap-0 text-
|
|
838
|
+
className="w-full text-left flex items-center gap-0 text-[13px] hover:bg-[var(--ds-gray-alpha-100)] transition-colors cursor-pointer"
|
|
764
839
|
style={{ minHeight: 40 }}
|
|
765
840
|
>
|
|
766
841
|
<TreeGutter
|
|
@@ -781,36 +856,32 @@ function EventRow({
|
|
|
781
856
|
className="flex items-center flex-1 min-w-0"
|
|
782
857
|
style={{ opacity: contentOpacity, transition: 'opacity 150ms' }}
|
|
783
858
|
>
|
|
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"
|
|
859
|
+
{/* Expand chevron indicator */}
|
|
860
|
+
<div
|
|
861
|
+
className="flex items-center justify-center w-5 h-5 flex-shrink-0 rounded"
|
|
789
862
|
style={{
|
|
790
|
-
|
|
791
|
-
border: '1px solid var(--ds-gray-alpha-400)',
|
|
863
|
+
border: '1px solid var(--ds-gray-400)',
|
|
792
864
|
}}
|
|
793
|
-
aria-label={isExpanded ? 'Collapse details' : 'Expand details'}
|
|
794
865
|
>
|
|
795
866
|
<ChevronRight
|
|
796
867
|
className="h-3 w-3 transition-transform"
|
|
797
868
|
style={{
|
|
798
|
-
color: 'var(--ds-gray-
|
|
869
|
+
color: 'var(--ds-gray-900)',
|
|
799
870
|
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
800
871
|
}}
|
|
801
872
|
/>
|
|
802
|
-
</
|
|
873
|
+
</div>
|
|
803
874
|
|
|
804
875
|
{/* Time */}
|
|
805
876
|
<div
|
|
806
|
-
className="
|
|
807
|
-
style={{ color: 'var(--ds-gray-900)' }}
|
|
877
|
+
className="tabular-nums min-w-0 px-4"
|
|
878
|
+
style={{ color: 'var(--ds-gray-900)', flex: '2 1 0%' }}
|
|
808
879
|
>
|
|
809
880
|
{formatEventTime(createdAt)}
|
|
810
881
|
</div>
|
|
811
882
|
|
|
812
883
|
{/* Event Type */}
|
|
813
|
-
<div className="
|
|
884
|
+
<div className="font-medium min-w-0 px-4" style={{ flex: '2 1 0%' }}>
|
|
814
885
|
<span
|
|
815
886
|
className="inline-flex items-center gap-1.5"
|
|
816
887
|
style={{ color: 'var(--ds-gray-900)' }}
|
|
@@ -852,7 +923,8 @@ function EventRow({
|
|
|
852
923
|
|
|
853
924
|
{/* Name */}
|
|
854
925
|
<div
|
|
855
|
-
className="
|
|
926
|
+
className="min-w-0 px-4 overflow-hidden text-ellipsis whitespace-nowrap"
|
|
927
|
+
style={{ flex: '2 1 0%' }}
|
|
856
928
|
title={eventName !== '-' ? eventName : undefined}
|
|
857
929
|
>
|
|
858
930
|
{eventName}
|
|
@@ -861,11 +933,16 @@ function EventRow({
|
|
|
861
933
|
{/* Correlation ID */}
|
|
862
934
|
<CopyableCell
|
|
863
935
|
value={event.correlationId || ''}
|
|
864
|
-
className="font-mono
|
|
936
|
+
className="font-mono"
|
|
937
|
+
style={{ flex: '3 1 0%' }}
|
|
865
938
|
/>
|
|
866
939
|
|
|
867
940
|
{/* Event ID */}
|
|
868
|
-
<CopyableCell
|
|
941
|
+
<CopyableCell
|
|
942
|
+
value={event.eventId}
|
|
943
|
+
className="font-mono"
|
|
944
|
+
style={{ flex: '3 1 0%' }}
|
|
945
|
+
/>
|
|
869
946
|
</div>
|
|
870
947
|
</div>
|
|
871
948
|
|
|
@@ -971,13 +1048,51 @@ export function EventListView({
|
|
|
971
1048
|
isLoadingMoreEvents = false,
|
|
972
1049
|
onLoadMoreEvents,
|
|
973
1050
|
encryptionKey,
|
|
1051
|
+
isLoading = false,
|
|
1052
|
+
sortOrder: sortOrderProp,
|
|
1053
|
+
onSortOrderChange,
|
|
1054
|
+
onDecrypt,
|
|
1055
|
+
isDecrypting = false,
|
|
974
1056
|
}: EventsListProps) {
|
|
1057
|
+
const [internalSortOrder, setInternalSortOrder] = useState<'asc' | 'desc'>(
|
|
1058
|
+
'asc'
|
|
1059
|
+
);
|
|
1060
|
+
const effectiveSortOrder = sortOrderProp ?? internalSortOrder;
|
|
1061
|
+
const handleSortOrderChange = useCallback(
|
|
1062
|
+
(order: 'asc' | 'desc') => {
|
|
1063
|
+
if (onSortOrderChange) {
|
|
1064
|
+
onSortOrderChange(order);
|
|
1065
|
+
} else {
|
|
1066
|
+
setInternalSortOrder(order);
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
[onSortOrderChange]
|
|
1070
|
+
);
|
|
1071
|
+
|
|
975
1072
|
const sortedEvents = useMemo(() => {
|
|
976
1073
|
if (!events || events.length === 0) return [];
|
|
1074
|
+
const dir = effectiveSortOrder === 'desc' ? -1 : 1;
|
|
977
1075
|
return [...events].sort(
|
|
978
1076
|
(a, b) =>
|
|
979
|
-
|
|
1077
|
+
dir *
|
|
1078
|
+
(new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
|
980
1079
|
);
|
|
1080
|
+
}, [events, effectiveSortOrder]);
|
|
1081
|
+
|
|
1082
|
+
// Detect encrypted fields across all loaded events.
|
|
1083
|
+
// Only checks top-level eventData values (input, output, result, etc.) —
|
|
1084
|
+
// the current data model guarantees encrypted markers appear at this level.
|
|
1085
|
+
const hasEncryptedData = useMemo(() => {
|
|
1086
|
+
if (!events) return false;
|
|
1087
|
+
for (const event of events) {
|
|
1088
|
+
const ed = (event as Record<string, unknown>).eventData;
|
|
1089
|
+
if (!ed || typeof ed !== 'object') continue;
|
|
1090
|
+
const data = ed as Record<string, unknown>;
|
|
1091
|
+
for (const val of Object.values(data)) {
|
|
1092
|
+
if (isEncryptedMarker(val)) return true;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return false;
|
|
981
1096
|
}, [events]);
|
|
982
1097
|
|
|
983
1098
|
const { correlationNameMap, workflowName } = useMemo(
|
|
@@ -1005,6 +1120,58 @@ export function EventListView({
|
|
|
1005
1120
|
|
|
1006
1121
|
const activeGroupKey = selectedGroupKey ?? hoveredGroupKey;
|
|
1007
1122
|
|
|
1123
|
+
// Expanded state lifted out of EventRow so it survives virtualization
|
|
1124
|
+
const [expandedEventIds, setExpandedEventIds] = useState<Set<string>>(
|
|
1125
|
+
() => new Set()
|
|
1126
|
+
);
|
|
1127
|
+
const toggleEventExpanded = useCallback((eventId: string) => {
|
|
1128
|
+
setExpandedEventIds((prev) => {
|
|
1129
|
+
const next = new Set(prev);
|
|
1130
|
+
if (next.has(eventId)) {
|
|
1131
|
+
next.delete(eventId);
|
|
1132
|
+
} else {
|
|
1133
|
+
next.add(eventId);
|
|
1134
|
+
}
|
|
1135
|
+
return next;
|
|
1136
|
+
});
|
|
1137
|
+
}, []);
|
|
1138
|
+
|
|
1139
|
+
// Event data cache — ref avoids re-renders when cache updates
|
|
1140
|
+
const eventDataCacheRef = useRef<Map<string, unknown>>(new Map());
|
|
1141
|
+
const cacheEventData = useCallback((eventId: string, data: unknown) => {
|
|
1142
|
+
eventDataCacheRef.current.set(eventId, data);
|
|
1143
|
+
}, []);
|
|
1144
|
+
|
|
1145
|
+
// Lookup from eventId → groupKey for efficient collapse filtering
|
|
1146
|
+
const eventGroupKeyMap = useMemo(() => {
|
|
1147
|
+
const map = new Map<string, string>();
|
|
1148
|
+
for (const ev of sortedEvents) {
|
|
1149
|
+
const gk = isRunLevel(ev.eventType)
|
|
1150
|
+
? '__run__'
|
|
1151
|
+
: (ev.correlationId ?? '');
|
|
1152
|
+
if (gk) map.set(ev.eventId, gk);
|
|
1153
|
+
}
|
|
1154
|
+
return map;
|
|
1155
|
+
}, [sortedEvents]);
|
|
1156
|
+
|
|
1157
|
+
// Collapse expanded events that don't belong to the newly selected group
|
|
1158
|
+
useEffect(() => {
|
|
1159
|
+
if (selectedGroupKey === undefined) return;
|
|
1160
|
+
setExpandedEventIds((prev) => {
|
|
1161
|
+
if (prev.size === 0) return prev;
|
|
1162
|
+
let changed = false;
|
|
1163
|
+
const next = new Set<string>();
|
|
1164
|
+
for (const eventId of prev) {
|
|
1165
|
+
if (eventGroupKeyMap.get(eventId) === selectedGroupKey) {
|
|
1166
|
+
next.add(eventId);
|
|
1167
|
+
} else {
|
|
1168
|
+
changed = true;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return changed ? next : prev;
|
|
1172
|
+
});
|
|
1173
|
+
}, [selectedGroupKey, eventGroupKeyMap]);
|
|
1174
|
+
|
|
1008
1175
|
// Compute the row-index range for the active group's connecting lane line.
|
|
1009
1176
|
// Only applies to non-run groups (step/hook/wait correlations).
|
|
1010
1177
|
const selectedGroupRange = useMemo(() => {
|
|
@@ -1025,24 +1192,34 @@ export function EventListView({
|
|
|
1025
1192
|
|
|
1026
1193
|
const searchIndex = useMemo(() => {
|
|
1027
1194
|
const entries: {
|
|
1028
|
-
|
|
1195
|
+
fields: string[];
|
|
1029
1196
|
groupKey?: string;
|
|
1030
1197
|
eventId: string;
|
|
1031
1198
|
index: number;
|
|
1032
1199
|
}[] = [];
|
|
1033
1200
|
for (let i = 0; i < sortedEvents.length; i++) {
|
|
1034
1201
|
const ev = sortedEvents[i];
|
|
1202
|
+
const isRun = isRunLevel(ev.eventType);
|
|
1203
|
+
const name = isRun
|
|
1204
|
+
? (workflowName ?? '')
|
|
1205
|
+
: ev.correlationId
|
|
1206
|
+
? (correlationNameMap.get(ev.correlationId) ?? '')
|
|
1207
|
+
: '';
|
|
1035
1208
|
entries.push({
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
ev.correlationId ??
|
|
1039
|
-
|
|
1209
|
+
fields: [
|
|
1210
|
+
ev.eventId,
|
|
1211
|
+
ev.correlationId ?? '',
|
|
1212
|
+
ev.eventType,
|
|
1213
|
+
formatEventType(ev.eventType),
|
|
1214
|
+
name,
|
|
1215
|
+
].map((f) => f.toLowerCase()),
|
|
1216
|
+
groupKey: ev.correlationId ?? (isRun ? '__run__' : undefined),
|
|
1040
1217
|
eventId: ev.eventId,
|
|
1041
1218
|
index: i,
|
|
1042
1219
|
});
|
|
1043
1220
|
}
|
|
1044
1221
|
return entries;
|
|
1045
|
-
}, [sortedEvents]);
|
|
1222
|
+
}, [sortedEvents, correlationNameMap, workflowName]);
|
|
1046
1223
|
|
|
1047
1224
|
useEffect(() => {
|
|
1048
1225
|
const q = searchQuery.trim().toLowerCase();
|
|
@@ -1050,18 +1227,66 @@ export function EventListView({
|
|
|
1050
1227
|
setSelectedGroupKey(undefined);
|
|
1051
1228
|
return;
|
|
1052
1229
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1230
|
+
let bestMatch: (typeof searchIndex)[number] | null = null;
|
|
1231
|
+
let bestScore = 0;
|
|
1232
|
+
for (const entry of searchIndex) {
|
|
1233
|
+
for (const field of entry.fields) {
|
|
1234
|
+
if (field && field.includes(q)) {
|
|
1235
|
+
const score = q.length / field.length;
|
|
1236
|
+
if (score > bestScore) {
|
|
1237
|
+
bestScore = score;
|
|
1238
|
+
bestMatch = entry;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (bestMatch) {
|
|
1244
|
+
setSelectedGroupKey(bestMatch.groupKey);
|
|
1056
1245
|
virtuosoRef.current?.scrollToIndex({
|
|
1057
|
-
index:
|
|
1246
|
+
index: bestMatch.index,
|
|
1058
1247
|
align: 'center',
|
|
1059
1248
|
behavior: 'smooth',
|
|
1060
1249
|
});
|
|
1061
1250
|
}
|
|
1062
1251
|
}, [searchQuery, searchIndex]);
|
|
1063
1252
|
|
|
1064
|
-
|
|
1253
|
+
// Track whether we've ever had events to distinguish initial load from refetch
|
|
1254
|
+
const hasHadEventsRef = useRef(false);
|
|
1255
|
+
if (sortedEvents.length > 0) {
|
|
1256
|
+
hasHadEventsRef.current = true;
|
|
1257
|
+
}
|
|
1258
|
+
const isInitialLoad = isLoading && !hasHadEventsRef.current;
|
|
1259
|
+
const isRefetching =
|
|
1260
|
+
isLoading && hasHadEventsRef.current && sortedEvents.length === 0;
|
|
1261
|
+
|
|
1262
|
+
if (isInitialLoad) {
|
|
1263
|
+
return (
|
|
1264
|
+
<div className="h-full flex flex-col overflow-hidden">
|
|
1265
|
+
{/* Skeleton search bar */}
|
|
1266
|
+
<div style={{ padding: 6 }}>
|
|
1267
|
+
<Skeleton style={{ height: 40, borderRadius: 6 }} />
|
|
1268
|
+
</div>
|
|
1269
|
+
{/* Skeleton header */}
|
|
1270
|
+
<div
|
|
1271
|
+
className="flex items-center gap-0 h-10 border-b flex-shrink-0 px-4"
|
|
1272
|
+
style={{ borderColor: 'var(--ds-gray-alpha-200)' }}
|
|
1273
|
+
>
|
|
1274
|
+
<Skeleton className="h-3" style={{ width: 60 }} />
|
|
1275
|
+
<div style={{ flex: 1 }} />
|
|
1276
|
+
<Skeleton className="h-3" style={{ width: 80 }} />
|
|
1277
|
+
<div style={{ flex: 1 }} />
|
|
1278
|
+
<Skeleton className="h-3" style={{ width: 50 }} />
|
|
1279
|
+
<div style={{ flex: 1 }} />
|
|
1280
|
+
<Skeleton className="h-3" style={{ width: 90 }} />
|
|
1281
|
+
<div style={{ flex: 1 }} />
|
|
1282
|
+
<Skeleton className="h-3" style={{ width: 70 }} />
|
|
1283
|
+
</div>
|
|
1284
|
+
<RowsSkeleton />
|
|
1285
|
+
</div>
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (!isLoading && (!events || events.length === 0)) {
|
|
1065
1290
|
return (
|
|
1066
1291
|
<div
|
|
1067
1292
|
className="flex items-center justify-center h-full text-sm"
|
|
@@ -1075,8 +1300,15 @@ export function EventListView({
|
|
|
1075
1300
|
return (
|
|
1076
1301
|
<div className="h-full flex flex-col overflow-hidden">
|
|
1077
1302
|
<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
|
|
1303
|
+
{/* Search bar + sort */}
|
|
1304
|
+
<div
|
|
1305
|
+
style={{
|
|
1306
|
+
padding: 6,
|
|
1307
|
+
backgroundColor: 'var(--ds-background-100)',
|
|
1308
|
+
display: 'flex',
|
|
1309
|
+
gap: 6,
|
|
1310
|
+
}}
|
|
1311
|
+
>
|
|
1080
1312
|
<label
|
|
1081
1313
|
style={{
|
|
1082
1314
|
display: 'flex',
|
|
@@ -1086,6 +1318,8 @@ export function EventListView({
|
|
|
1086
1318
|
boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
|
|
1087
1319
|
background: 'var(--ds-background-100)',
|
|
1088
1320
|
height: 40,
|
|
1321
|
+
flex: 1,
|
|
1322
|
+
minWidth: 0,
|
|
1089
1323
|
}}
|
|
1090
1324
|
>
|
|
1091
1325
|
<div
|
|
@@ -1124,7 +1358,7 @@ export function EventListView({
|
|
|
1124
1358
|
</div>
|
|
1125
1359
|
<input
|
|
1126
1360
|
type="search"
|
|
1127
|
-
placeholder="Search by event
|
|
1361
|
+
placeholder="Search by name, event type, or ID…"
|
|
1128
1362
|
value={searchQuery}
|
|
1129
1363
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
1130
1364
|
style={{
|
|
@@ -1140,11 +1374,23 @@ export function EventListView({
|
|
|
1140
1374
|
}}
|
|
1141
1375
|
/>
|
|
1142
1376
|
</label>
|
|
1377
|
+
<MenuDropdown
|
|
1378
|
+
options={SORT_OPTIONS}
|
|
1379
|
+
value={effectiveSortOrder}
|
|
1380
|
+
onChange={handleSortOrderChange}
|
|
1381
|
+
/>
|
|
1382
|
+
{(hasEncryptedData || encryptionKey) && onDecrypt && (
|
|
1383
|
+
<DecryptButton
|
|
1384
|
+
decrypted={!!encryptionKey}
|
|
1385
|
+
loading={isDecrypting}
|
|
1386
|
+
onClick={onDecrypt}
|
|
1387
|
+
/>
|
|
1388
|
+
)}
|
|
1143
1389
|
</div>
|
|
1144
1390
|
|
|
1145
1391
|
{/* Header */}
|
|
1146
1392
|
<div
|
|
1147
|
-
className="flex items-center gap-0 text-
|
|
1393
|
+
className="flex items-center gap-0 text-[13px] font-medium h-10 border-b flex-shrink-0"
|
|
1148
1394
|
style={{
|
|
1149
1395
|
borderColor: 'var(--ds-gray-alpha-200)',
|
|
1150
1396
|
color: 'var(--ds-gray-900)',
|
|
@@ -1153,82 +1399,93 @@ export function EventListView({
|
|
|
1153
1399
|
>
|
|
1154
1400
|
<div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
|
|
1155
1401
|
<div className="w-5 flex-shrink-0" />
|
|
1156
|
-
<div className="
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
<div className="
|
|
1160
|
-
|
|
1402
|
+
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
|
|
1403
|
+
Time
|
|
1404
|
+
</div>
|
|
1405
|
+
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
|
|
1406
|
+
Event Type
|
|
1407
|
+
</div>
|
|
1408
|
+
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
|
|
1409
|
+
Name
|
|
1410
|
+
</div>
|
|
1411
|
+
<div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
|
|
1412
|
+
Correlation ID
|
|
1413
|
+
</div>
|
|
1414
|
+
<div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
|
|
1415
|
+
Event ID
|
|
1416
|
+
</div>
|
|
1161
1417
|
</div>
|
|
1162
1418
|
|
|
1163
|
-
{/* Virtualized event rows */}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
)}
|
|
1217
|
-
<div
|
|
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
|
-
),
|
|
1419
|
+
{/* Virtualized event rows or refetching skeleton */}
|
|
1420
|
+
{isRefetching ? (
|
|
1421
|
+
<RowsSkeleton />
|
|
1422
|
+
) : (
|
|
1423
|
+
<Virtuoso
|
|
1424
|
+
ref={virtuosoRef}
|
|
1425
|
+
totalCount={sortedEvents.length}
|
|
1426
|
+
overscan={20}
|
|
1427
|
+
defaultItemHeight={40}
|
|
1428
|
+
endReached={() => {
|
|
1429
|
+
if (!hasMoreEvents || isLoadingMoreEvents) {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
void onLoadMoreEvents?.();
|
|
1433
|
+
}}
|
|
1434
|
+
itemContent={(index: number) => {
|
|
1435
|
+
const ev = sortedEvents[index];
|
|
1436
|
+
return (
|
|
1437
|
+
<EventRow
|
|
1438
|
+
event={ev}
|
|
1439
|
+
index={index}
|
|
1440
|
+
isFirst={index === 0}
|
|
1441
|
+
isLast={index === sortedEvents.length - 1}
|
|
1442
|
+
isExpanded={expandedEventIds.has(ev.eventId)}
|
|
1443
|
+
onToggleExpand={toggleEventExpanded}
|
|
1444
|
+
activeGroupKey={activeGroupKey}
|
|
1445
|
+
selectedGroupKey={selectedGroupKey}
|
|
1446
|
+
selectedGroupRange={selectedGroupRange}
|
|
1447
|
+
correlationNameMap={correlationNameMap}
|
|
1448
|
+
workflowName={workflowName}
|
|
1449
|
+
durationMap={durationMap}
|
|
1450
|
+
onSelectGroup={onSelectGroup}
|
|
1451
|
+
onHoverGroup={onHoverGroup}
|
|
1452
|
+
onLoadEventData={onLoadEventData}
|
|
1453
|
+
cachedEventData={
|
|
1454
|
+
eventDataCacheRef.current.get(ev.eventId) ?? null
|
|
1455
|
+
}
|
|
1456
|
+
onCacheEventData={cacheEventData}
|
|
1457
|
+
encryptionKey={encryptionKey}
|
|
1458
|
+
/>
|
|
1459
|
+
);
|
|
1460
|
+
}}
|
|
1461
|
+
style={{ flex: 1, minHeight: 0 }}
|
|
1462
|
+
/>
|
|
1463
|
+
)}
|
|
1464
|
+
|
|
1465
|
+
{/* Fixed footer — count + load more */}
|
|
1466
|
+
<div
|
|
1467
|
+
className="relative flex-shrink-0 flex items-center h-10 border-t px-4 text-xs"
|
|
1468
|
+
style={{
|
|
1469
|
+
borderColor: 'var(--ds-gray-alpha-200)',
|
|
1470
|
+
color: 'var(--ds-gray-900)',
|
|
1471
|
+
backgroundColor: 'var(--ds-background-100)',
|
|
1229
1472
|
}}
|
|
1230
|
-
|
|
1231
|
-
|
|
1473
|
+
>
|
|
1474
|
+
<span>
|
|
1475
|
+
{sortedEvents.length} event
|
|
1476
|
+
{sortedEvents.length !== 1 ? 's' : ''} loaded
|
|
1477
|
+
</span>
|
|
1478
|
+
{hasMoreEvents && (
|
|
1479
|
+
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
1480
|
+
<div className="pointer-events-auto">
|
|
1481
|
+
<LoadMoreButton
|
|
1482
|
+
loading={isLoadingMoreEvents}
|
|
1483
|
+
onClick={() => void onLoadMoreEvents?.()}
|
|
1484
|
+
/>
|
|
1485
|
+
</div>
|
|
1486
|
+
</div>
|
|
1487
|
+
)}
|
|
1488
|
+
</div>
|
|
1232
1489
|
</div>
|
|
1233
1490
|
);
|
|
1234
1491
|
}
|