@@ -6,12 +6,15 @@ import { Check, ChevronRight, Copy } from 'lucide-react';
66import type { MouseEvent as ReactMouseEvent , ReactNode } from 'react' ;
77import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
88import { Virtuoso , type VirtuosoHandle } from 'react-virtuoso' ;
9+ import { isEncryptedMarker } from '../lib/hydration' ;
10+ import { DecryptButton } from './ui/decrypt-button' ;
911import { formatDuration } from '../lib/utils' ;
1012import { DataInspector } from './ui/data-inspector' ;
1113import {
1214 ErrorStackBlock ,
1315 isStructuredErrorWithStack ,
1416} from './ui/error-stack-block' ;
17+ import { LoadMoreButton } from './ui/load-more-button' ;
1518import { MenuDropdown } from './ui/menu-dropdown' ;
1619import { Skeleton } from './ui/skeleton' ;
1720
@@ -596,6 +599,30 @@ const SORT_OPTIONS = [
596599 { value : 'asc' as const , label : 'Oldest' } ,
597600] ;
598601
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+
599626// ──────────────────────────────────────────────────────────────────────────
600627// Event row
601628// ──────────────────────────────────────────────────────────────────────────
@@ -616,6 +643,10 @@ interface EventsListProps {
616643 /** Called when the user changes sort order. When provided, the sort dropdown is shown
617644 * and the parent is expected to refetch from the API with the new order. */
618645 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 ;
619650}
620651
621652function EventRow ( {
@@ -1020,6 +1051,8 @@ export function EventListView({
10201051 isLoading = false ,
10211052 sortOrder : sortOrderProp ,
10221053 onSortOrderChange,
1054+ onDecrypt,
1055+ isDecrypting = false ,
10231056} : EventsListProps ) {
10241057 const [ internalSortOrder , setInternalSortOrder ] = useState < 'asc' | 'desc' > (
10251058 'asc'
@@ -1046,6 +1079,22 @@ export function EventListView({
10461079 ) ;
10471080 } , [ events , effectiveSortOrder ] ) ;
10481081
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 ;
1096+ } , [ events ] ) ;
1097+
10491098 const { correlationNameMap, workflowName } = useMemo (
10501099 ( ) => buildNameMaps ( events ?? null , run ?? null ) ,
10511100 [ events , run ]
@@ -1201,52 +1250,43 @@ export function EventListView({
12011250 }
12021251 } , [ searchQuery , searchIndex ] ) ;
12031252
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 >
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 } } />
12471268 </ div >
1248- ) ;
1249- }
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 ) ) {
12501290 return (
12511291 < div
12521292 className = "flex items-center justify-center h-full text-sm"
@@ -1339,6 +1379,13 @@ export function EventListView({
13391379 value = { effectiveSortOrder }
13401380 onChange = { handleSortOrderChange }
13411381 />
1382+ { ( hasEncryptedData || encryptionKey ) && onDecrypt && (
1383+ < DecryptButton
1384+ decrypted = { ! ! encryptionKey }
1385+ loading = { isDecrypting }
1386+ onClick = { onDecrypt }
1387+ />
1388+ ) }
13421389 </ div >
13431390
13441391 { /* Header */ }
@@ -1369,82 +1416,75 @@ export function EventListView({
13691416 </ div >
13701417 </ div >
13711418
1372- { /* Virtualized event rows */ }
1373- < Virtuoso
1374- ref = { virtuosoRef }
1375- totalCount = { sortedEvents . length }
1376- overscan = { 20 }
1377- defaultItemHeight = { 40 }
1378- endReached = { ( ) => {
1379- if ( ! hasMoreEvents || isLoadingMoreEvents ) {
1380- return ;
1381- }
1382- void onLoadMoreEvents ?.( ) ;
1383- } }
1384- itemContent = { ( index : number ) => {
1385- const ev = sortedEvents [ index ] ;
1386- return (
1387- < EventRow
1388- event = { ev }
1389- index = { index }
1390- isFirst = { index === 0 }
1391- isLast = { index === sortedEvents . length - 1 }
1392- isExpanded = { expandedEventIds . has ( ev . eventId ) }
1393- onToggleExpand = { toggleEventExpanded }
1394- activeGroupKey = { activeGroupKey }
1395- selectedGroupKey = { selectedGroupKey }
1396- selectedGroupRange = { selectedGroupRange }
1397- correlationNameMap = { correlationNameMap }
1398- workflowName = { workflowName }
1399- durationMap = { durationMap }
1400- onSelectGroup = { onSelectGroup }
1401- onHoverGroup = { onHoverGroup }
1402- onLoadEventData = { onLoadEventData }
1403- cachedEventData = {
1404- eventDataCacheRef . current . get ( ev . eventId ) ?? null
1405- }
1406- onCacheEventData = { cacheEventData }
1407- encryptionKey = { encryptionKey }
1408- />
1409- ) ;
1410- } }
1411- components = { {
1412- Footer : hasMoreEvents
1413- ? ( ) => (
1414- < div className = "px-3 pt-3 flex justify-center" >
1415- < button
1416- type = "button"
1417- onClick = { ( ) => void onLoadMoreEvents ?.( ) }
1418- disabled = { isLoadingMoreEvents }
1419- className = "h-8 px-3 text-xs rounded-md border transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
1420- style = { {
1421- borderColor : 'var(--ds-gray-alpha-400)' ,
1422- color : 'var(--ds-gray-900)' ,
1423- backgroundColor : 'var(--ds-background-100)' ,
1424- } }
1425- >
1426- { isLoadingMoreEvents
1427- ? 'Loading more events...'
1428- : 'Load more' }
1429- </ button >
1430- </ div >
1431- )
1432- : undefined ,
1433- } }
1434- style = { { flex : 1 , minHeight : 0 } }
1435- />
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+ ) }
14361464
1437- { /* Fixed footer — always at the bottom of the visible area */ }
1465+ { /* Fixed footer — count + load more */ }
14381466 < div
1439- className = "flex-shrink-0 border-t text-xs px-3 py-2 "
1467+ className = "relative flex-shrink-0 flex items-center h-10 border-t px-4 text-xs "
14401468 style = { {
14411469 borderColor : 'var(--ds-gray-alpha-200)' ,
14421470 color : 'var(--ds-gray-900)' ,
14431471 backgroundColor : 'var(--ds-background-100)' ,
14441472 } }
14451473 >
1446- { sortedEvents . length } event
1447- { sortedEvents . length !== 1 ? 's' : '' } total
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+ ) }
14481488 </ div >
14491489 </ div >
14501490 ) ;
0 commit comments