From 30526a1893524cb3b7bfc62c5a950a9b66913c53 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Wed, 11 Mar 2026 12:44:21 -0700 Subject: [PATCH 1/6] Fix connector line showing for run events --- packages/web-shared/src/components/event-list-view.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web-shared/src/components/event-list-view.tsx b/packages/web-shared/src/components/event-list-view.tsx index a45120a231..2f003f6c0d 100644 --- a/packages/web-shared/src/components/event-list-view.tsx +++ b/packages/web-shared/src/components/event-list-view.tsx @@ -637,9 +637,9 @@ function EventRow({ const [loadError, setLoadError] = useState(null); const [hasAttemptedLoad, setHasAttemptedLoad] = useState(false); - const rowGroupKey = - event.correlationId ?? - (isRunLevel(event.eventType) ? '__run__' : undefined); + const rowGroupKey = isRunLevel(event.eventType) + ? '__run__' + : (event.correlationId ?? undefined); // Collapse when a different group gets selected useEffect(() => { From 7fb396f36fccca4c2115975fa04343692cfbc044 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Wed, 11 Mar 2026 13:16:21 -0700 Subject: [PATCH 2/6] Improve row toggle --- .../src/components/event-list-view.tsx | 243 ++++++++++++------ 1 file changed, 171 insertions(+), 72 deletions(-) diff --git a/packages/web-shared/src/components/event-list-view.tsx b/packages/web-shared/src/components/event-list-view.tsx index 2f003f6c0d..e1ebeccbd9 100644 --- a/packages/web-shared/src/components/event-list-view.tsx +++ b/packages/web-shared/src/components/event-list-view.tsx @@ -378,9 +378,11 @@ function TreeGutter({ function CopyableCell({ value, className, + style: styleProp, }: { value: string; className?: string; + style?: React.CSSProperties; }): ReactNode { const [copied, setCopied] = useState(false); const resetCopiedTimeoutRef = useRef(null); @@ -412,7 +414,8 @@ function CopyableCell({ return (
{value || '-'} @@ -605,6 +608,8 @@ function EventRow({ index, isFirst, isLast, + isExpanded, + onToggleExpand, activeGroupKey, selectedGroupKey, selectedGroupRange, @@ -614,12 +619,16 @@ function EventRow({ onSelectGroup, onHoverGroup, onLoadEventData, + cachedEventData, + onCacheEventData, encryptionKey, }: { event: Event; index: number; isFirst: boolean; isLast: boolean; + isExpanded: boolean; + onToggleExpand: (eventId: string) => void; activeGroupKey?: string; selectedGroupKey?: string; selectedGroupRange: { first: number; last: number } | null; @@ -629,25 +638,23 @@ function EventRow({ onSelectGroup: (groupKey: string | undefined) => void; onHoverGroup: (groupKey: string | undefined) => void; onLoadEventData?: (event: Event) => Promise; + cachedEventData: unknown | null; + onCacheEventData: (eventId: string, data: unknown) => void; encryptionKey?: Uint8Array; }) { - const [isExpanded, setIsExpanded] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [loadedEventData, setLoadedEventData] = useState(null); + const [loadedEventData, setLoadedEventData] = useState( + cachedEventData + ); const [loadError, setLoadError] = useState(null); - const [hasAttemptedLoad, setHasAttemptedLoad] = useState(false); + const [hasAttemptedLoad, setHasAttemptedLoad] = useState( + cachedEventData !== null + ); const rowGroupKey = isRunLevel(event.eventType) ? '__run__' : (event.correlationId ?? undefined); - // Collapse when a different group gets selected - useEffect(() => { - if (selectedGroupKey !== undefined && selectedGroupKey !== rowGroupKey) { - setIsExpanded(false); - } - }, [selectedGroupKey, rowGroupKey]); - const statusDotColor = getStatusDotColor(event.eventType); const createdAt = new Date(event.createdAt); const hasExistingEventData = 'eventData' in event && event.eventData != null; @@ -688,9 +695,10 @@ function EventRow({ setLoadError('Event details unavailable'); return; } - const eventData = await onLoadEventData(event); - if (eventData !== null && eventData !== undefined) { - setLoadedEventData(eventData); + const data = await onLoadEventData(event); + if (data !== null && data !== undefined) { + setLoadedEventData(data); + onCacheEventData(event.eventId, data); } } catch (err) { setLoadError( @@ -700,7 +708,27 @@ function EventRow({ setIsLoading(false); setHasAttemptedLoad(true); } - }, [event, loadedEventData, hasExistingEventData, onLoadEventData]); + }, [ + event, + loadedEventData, + hasExistingEventData, + onLoadEventData, + onCacheEventData, + ]); + + // Auto-load event data when remounting in expanded state without cached data + useEffect(() => { + if ( + isExpanded && + loadedEventData === null && + !hasExistingEventData && + !isLoading && + !hasAttemptedLoad + ) { + loadEventDetails(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // When encryption key changes and this event was previously loaded, // re-load to get decrypted data @@ -712,6 +740,7 @@ function EventRow({ .then((data) => { if (data !== null && data !== undefined) { setLoadedEventData(data); + onCacheEventData(event.eventId, data); } setHasAttemptedLoad(true); }) @@ -722,25 +751,23 @@ function EventRow({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [encryptionKey]); - const handleExpandToggle = useCallback( - (e: ReactMouseEvent) => { - e.stopPropagation(); - const newExpanded = !isExpanded; - setIsExpanded(newExpanded); - if (newExpanded && loadedEventData === null && !hasExistingEventData) { - loadEventDetails(); - } - }, - [isExpanded, loadedEventData, hasExistingEventData, loadEventDetails] - ); - const handleRowClick = useCallback(() => { - if (selectedGroupKey === rowGroupKey) { - onSelectGroup(undefined); - } else { - onSelectGroup(rowGroupKey); + onSelectGroup(rowGroupKey === selectedGroupKey ? undefined : rowGroupKey); + onToggleExpand(event.eventId); + if (!isExpanded && loadedEventData === null && !hasExistingEventData) { + loadEventDetails(); } - }, [selectedGroupKey, rowGroupKey, onSelectGroup]); + }, [ + selectedGroupKey, + rowGroupKey, + onSelectGroup, + onToggleExpand, + event.eventId, + isExpanded, + loadedEventData, + hasExistingEventData, + loadEventDetails, + ]); const eventData = hasExistingEventData ? (event as Event & { eventData: unknown }).eventData @@ -762,7 +789,7 @@ function EventRow({ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleRowClick(); }} - className="w-full text-left flex items-center gap-0 text-sm hover:bg-[var(--ds-gray-alpha-100)] transition-colors cursor-pointer" + className="w-full text-left flex items-center gap-0 text-[13px] hover:bg-[var(--ds-gray-alpha-100)] transition-colors cursor-pointer" style={{ minHeight: 40 }} > - {/* Expand chevron button */} - +
{/* Time */}
{formatEventTime(createdAt)}
{/* Event Type */} -
+
{eventName} @@ -863,11 +887,16 @@ function EventRow({ {/* Correlation ID */} {/* Event ID */} - +
@@ -1008,6 +1037,58 @@ export function EventListView({ const activeGroupKey = selectedGroupKey ?? hoveredGroupKey; + // Expanded state lifted out of EventRow so it survives virtualization + const [expandedEventIds, setExpandedEventIds] = useState>( + () => new Set() + ); + const toggleEventExpanded = useCallback((eventId: string) => { + setExpandedEventIds((prev) => { + const next = new Set(prev); + if (next.has(eventId)) { + next.delete(eventId); + } else { + next.add(eventId); + } + return next; + }); + }, []); + + // Event data cache — ref avoids re-renders when cache updates + const eventDataCacheRef = useRef>(new Map()); + const cacheEventData = useCallback((eventId: string, data: unknown) => { + eventDataCacheRef.current.set(eventId, data); + }, []); + + // Lookup from eventId → groupKey for efficient collapse filtering + const eventGroupKeyMap = useMemo(() => { + const map = new Map(); + for (const ev of sortedEvents) { + const gk = isRunLevel(ev.eventType) + ? '__run__' + : (ev.correlationId ?? ''); + if (gk) map.set(ev.eventId, gk); + } + return map; + }, [sortedEvents]); + + // Collapse expanded events that don't belong to the newly selected group + useEffect(() => { + if (selectedGroupKey === undefined) return; + setExpandedEventIds((prev) => { + if (prev.size === 0) return prev; + let changed = false; + const next = new Set(); + for (const eventId of prev) { + if (eventGroupKeyMap.get(eventId) === selectedGroupKey) { + next.add(eventId); + } else { + changed = true; + } + } + return changed ? next : prev; + }); + }, [selectedGroupKey, eventGroupKeyMap]); + // Compute the row-index range for the active group's connecting lane line. // Only applies to non-run groups (step/hook/wait correlations). const selectedGroupRange = useMemo(() => { @@ -1147,7 +1228,7 @@ export function EventListView({ {/* Header */}
-
Time
-
Event Type
-
Name
-
Correlation ID
-
Event ID
+
+ Time +
+
+ Event Type +
+
+ Name +
+
+ Correlation ID +
+
+ Event ID +
{/* Virtualized event rows */} @@ -1176,12 +1267,15 @@ export function EventListView({ void onLoadMoreEvents?.(); }} itemContent={(index: number) => { + const ev = sortedEvents[index]; return ( ); }} components={{ - Footer: () => ( - <> - {hasMoreEvents && ( + Footer: hasMoreEvents + ? () => (
- )} -
- {sortedEvents.length} event - {sortedEvents.length !== 1 ? 's' : ''} total -
- - ), + ) + : undefined, }} style={{ flex: 1, minHeight: 0 }} /> + + {/* Fixed footer — always at the bottom of the visible area */} +
+ {sortedEvents.length} event + {sortedEvents.length !== 1 ? 's' : ''} total +
); } From c8ca3d7d24278665f566c843cd71578547bd829f Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Wed, 11 Mar 2026 13:49:20 -0700 Subject: [PATCH 3/6] improve error reporting --- .../src/components/ui/error-stack-block.tsx | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/web-shared/src/components/ui/error-stack-block.tsx b/packages/web-shared/src/components/ui/error-stack-block.tsx index c9e1066580..f0924cbe56 100644 --- a/packages/web-shared/src/components/ui/error-stack-block.tsx +++ b/packages/web-shared/src/components/ui/error-stack-block.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Copy } from 'lucide-react'; +import { AlertCircle, Copy } from 'lucide-react'; import { toast } from 'sonner'; /** @@ -19,10 +19,9 @@ export function isStructuredErrorWithStack( } /** - * Renders an error with a `stack` field as readable pre-formatted text, - * styled to match the CopyableDataBlock component. The error message is - * displayed at the top with a visual separator from the stack trace. - * The entire block is copyable via a copy button. + * Renders an error with a `stack` field as a visually distinct error block. + * Shows the error message with an alert icon at the top, separated from + * the stack trace below. */ export function ErrorStackBlock({ value, @@ -35,15 +34,22 @@ export function ErrorStackBlock({ return (
{message && ( -

- {message} -

+ +

{message}

+
)}
         {stack}
       
From 5b07aa527731dcedd871e22000a8e5fb4ad3847d Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Wed, 11 Mar 2026 15:50:32 -0700 Subject: [PATCH 4/6] add sorting for events --- .../src/components/event-list-view.tsx | 105 +++++++++++++++- packages/web-shared/src/components/index.ts | 1 + .../src/components/ui/menu-dropdown.tsx | 114 ++++++++++++++++++ 3 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 packages/web-shared/src/components/ui/menu-dropdown.tsx diff --git a/packages/web-shared/src/components/event-list-view.tsx b/packages/web-shared/src/components/event-list-view.tsx index e1ebeccbd9..d4a20207d2 100644 --- a/packages/web-shared/src/components/event-list-view.tsx +++ b/packages/web-shared/src/components/event-list-view.tsx @@ -12,6 +12,7 @@ import { ErrorStackBlock, isStructuredErrorWithStack, } from './ui/error-stack-block'; +import { MenuDropdown } from './ui/menu-dropdown'; import { Skeleton } from './ui/skeleton'; /** @@ -586,6 +587,15 @@ function PayloadBlock({ ); } +// ────────────────────────────────────────────────────────────────────────── +// Sort options for the events list +// ────────────────────────────────────────────────────────────────────────── + +const SORT_OPTIONS = [ + { value: 'desc' as const, label: 'Newest' }, + { value: 'asc' as const, label: 'Oldest' }, +]; + // ────────────────────────────────────────────────────────────────────────── // Event row // ────────────────────────────────────────────────────────────────────────── @@ -601,6 +611,11 @@ interface EventsListProps { encryptionKey?: Uint8Array; /** When true, shows a loading state instead of "No events found" for empty lists */ isLoading?: boolean; + /** Sort order for events. Defaults to 'asc'. */ + sortOrder?: 'asc' | 'desc'; + /** Called when the user changes sort order. When provided, the sort dropdown is shown + * and the parent is expected to refetch from the API with the new order. */ + onSortOrderChange?: (order: 'asc' | 'desc') => void; } function EventRow({ @@ -1003,14 +1018,33 @@ export function EventListView({ onLoadMoreEvents, encryptionKey, isLoading = false, + sortOrder: sortOrderProp, + onSortOrderChange, }: EventsListProps) { + const [internalSortOrder, setInternalSortOrder] = useState<'asc' | 'desc'>( + 'asc' + ); + const effectiveSortOrder = sortOrderProp ?? internalSortOrder; + const handleSortOrderChange = useCallback( + (order: 'asc' | 'desc') => { + if (onSortOrderChange) { + onSortOrderChange(order); + } else { + setInternalSortOrder(order); + } + }, + [onSortOrderChange] + ); + const sortedEvents = useMemo(() => { if (!events || events.length === 0) return []; + const dir = effectiveSortOrder === 'desc' ? -1 : 1; return [...events].sort( (a, b) => - new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + dir * + (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) ); - }, [events]); + }, [events, effectiveSortOrder]); const { correlationNameMap, workflowName } = useMemo( () => buildNameMaps(events ?? null, run ?? null), @@ -1146,12 +1180,57 @@ export function EventListView({ }, [searchQuery, searchIndex]); if (!events || events.length === 0) { + if (isLoading) { + return ( +
+ {/* Skeleton search bar */} +
+ +
+ {/* Skeleton header */} +
+ +
+ +
+ +
+ +
+ +
+ {/* Skeleton rows */} +
+ {Array.from({ length: 8 }, (_, i) => ( +
+ + + + + + +
+ ))} +
+
+ ); + } return (
- {isLoading ? 'Loading events…' : 'No events found'} + No events found
); } @@ -1159,8 +1238,15 @@ export function EventListView({ return (
- {/* Search bar */} -
+ {/* Search bar + sort */} +