@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.
- package/README.md +4 -0
- package/dist/components/event-list-view.d.ts +9 -3
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +222 -98
- 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/run-trace-view.d.ts +1 -3
- package/dist/components/run-trace-view.d.ts.map +1 -1
- package/dist/components/run-trace-view.js +2 -2
- package/dist/components/run-trace-view.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +11 -1
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/detail-card.d.ts.map +1 -1
- package/dist/components/sidebar/detail-card.js +4 -2
- package/dist/components/sidebar/detail-card.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts +3 -3
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +43 -26
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.d.ts +7 -1
- package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.js +36 -11
- package/dist/components/trace-viewer/trace-viewer.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 +3 -3
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +31 -129
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts +18 -5
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +65 -18
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/event-materialization.d.ts +72 -0
- package/dist/lib/event-materialization.d.ts.map +1 -0
- package/dist/lib/event-materialization.js +171 -0
- package/dist/lib/event-materialization.js.map +1 -0
- package/dist/lib/trace-builder.d.ts +32 -0
- package/dist/lib/trace-builder.d.ts.map +1 -0
- package/dist/lib/trace-builder.js +129 -0
- package/dist/lib/trace-builder.js.map +1 -0
- package/package.json +3 -3
- package/src/components/event-list-view.tsx +324 -103
- package/src/components/index.ts +1 -0
- package/src/components/run-trace-view.tsx +0 -6
- package/src/components/sidebar/attribute-panel.tsx +17 -2
- package/src/components/sidebar/detail-card.tsx +10 -2
- package/src/components/sidebar/entity-detail-panel.tsx +59 -21
- package/src/components/trace-viewer/trace-viewer.tsx +47 -2
- 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 +95 -195
- package/src/components/workflow-traces/trace-span-construction.ts +85 -32
- package/src/index.ts +13 -0
- package/src/lib/event-materialization.ts +243 -0
- 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,
|
|
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
|
|
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
|
-
|
|
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 (
|
|
120
|
-
for (const
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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>(
|
|
661
|
+
const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
|
|
662
|
+
cachedEventData
|
|
663
|
+
);
|
|
630
664
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
631
|
-
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(
|
|
665
|
+
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(
|
|
666
|
+
cachedEventData !== null
|
|
667
|
+
);
|
|
632
668
|
|
|
633
|
-
const rowGroupKey =
|
|
634
|
-
|
|
635
|
-
(
|
|
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
|
|
685
|
-
if (
|
|
686
|
-
setLoadedEventData(
|
|
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
|
-
}, [
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
770
|
+
onSelectGroup(rowGroupKey === selectedGroupKey ? undefined : rowGroupKey);
|
|
771
|
+
onToggleExpand(event.eventId);
|
|
772
|
+
if (!isExpanded && loadedEventData === null && !hasExistingEventData) {
|
|
773
|
+
loadEventDetails();
|
|
735
774
|
}
|
|
736
|
-
}, [
|
|
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-
|
|
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
|
|
780
|
-
<
|
|
781
|
-
|
|
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
|
-
|
|
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-
|
|
838
|
+
color: 'var(--ds-gray-900)',
|
|
794
839
|
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
795
840
|
}}
|
|
796
841
|
/>
|
|
797
|
-
</
|
|
842
|
+
</div>
|
|
798
843
|
|
|
799
844
|
{/* Time */}
|
|
800
845
|
<div
|
|
801
|
-
className="
|
|
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="
|
|
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="
|
|
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
|
|
905
|
+
className="font-mono"
|
|
906
|
+
style={{ flex: '3 1 0%' }}
|
|
860
907
|
/>
|
|
861
908
|
|
|
862
909
|
{/* Event ID */}
|
|
863
|
-
<CopyableCell
|
|
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
|
-
|
|
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(
|
|
981
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
ev.correlationId ??
|
|
1035
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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:
|
|
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
|
|
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
|
|
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-
|
|
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="
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
<div className="
|
|
1156
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
}
|