@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
package/README.md
CHANGED
|
@@ -37,6 +37,10 @@ export default function MyRunDetailView({
|
|
|
37
37
|
Server actions and data fetching are intentionally **not** part of `web-shared`. Implement those in your app
|
|
38
38
|
and pass data + callbacks into these components. If you need world run helpers, use `@workflow/core/runtime`.
|
|
39
39
|
|
|
40
|
+
> **Security notice:** If you implement server-side data fetching using `@workflow/world-vercel` or similar backends,
|
|
41
|
+
> ensure that user-supplied IDs (runId, stepId, etc.) are validated before passing them to world functions.
|
|
42
|
+
> The server actions in `@workflow/web` do not include authentication — see that package's README for details on securing self-hosted deployments.
|
|
43
|
+
|
|
40
44
|
## Styling
|
|
41
45
|
|
|
42
46
|
In order for tailwind classes to be picked up correctly, you might need to configure your NextJS app
|
|
@@ -8,7 +8,18 @@ interface EventsListProps {
|
|
|
8
8
|
onLoadMoreEvents?: () => Promise<void> | void;
|
|
9
9
|
/** When provided, signals that decryption is active (triggers re-load of expanded events) */
|
|
10
10
|
encryptionKey?: Uint8Array;
|
|
11
|
+
/** When true, shows a loading state instead of "No events found" for empty lists */
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
/** Sort order for events. Defaults to 'asc'. */
|
|
14
|
+
sortOrder?: 'asc' | 'desc';
|
|
15
|
+
/** Called when the user changes sort order. When provided, the sort dropdown is shown
|
|
16
|
+
* and the parent is expected to refetch from the API with the new order. */
|
|
17
|
+
onSortOrderChange?: (order: 'asc' | 'desc') => void;
|
|
18
|
+
/** Called when the user clicks the Decrypt button. */
|
|
19
|
+
onDecrypt?: () => void;
|
|
20
|
+
/** Whether the encryption key is currently being fetched. */
|
|
21
|
+
isDecrypting?: boolean;
|
|
11
22
|
}
|
|
12
|
-
export declare function EventListView({ events, run, onLoadEventData, hasMoreEvents, isLoadingMoreEvents, onLoadMoreEvents, encryptionKey, }: EventsListProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export declare function EventListView({ events, run, onLoadEventData, hasMoreEvents, isLoadingMoreEvents, onLoadMoreEvents, encryptionKey, isLoading, sortOrder: sortOrderProp, onSortOrderChange, onDecrypt, isDecrypting, }: EventsListProps): import("react/jsx-runtime").JSX.Element;
|
|
13
24
|
export {};
|
|
14
25
|
//# sourceMappingURL=event-list-view.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-list-view.d.ts","sourceRoot":"","sources":["../../src/components/event-list-view.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"event-list-view.d.ts","sourceRoot":"","sources":["../../src/components/event-list-view.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAknB1D,UAAU,eAAe;IACvB,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACvB,GAAG,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC5D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9C,6FAA6F;IAC7F,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,oFAAoF;IACpF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gDAAgD;IAChD,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC3B;iFAC6E;IAC7E,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,KAAK,IAAI,CAAC;IACpD,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAyYD,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,GAAG,EACH,eAAe,EACf,aAAqB,EACrB,mBAA2B,EAC3B,gBAAgB,EAChB,aAAa,EACb,SAAiB,EACjB,SAAS,EAAE,aAAa,EACxB,iBAAiB,EACjB,SAAS,EACT,YAAoB,GACrB,EAAE,eAAe,2CAmbjB"}
|
|
@@ -4,9 +4,13 @@ import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
|
|
|
4
4
|
import { Check, ChevronRight, Copy } from 'lucide-react';
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { Virtuoso } from 'react-virtuoso';
|
|
7
|
+
import { isEncryptedMarker } from '../lib/hydration';
|
|
8
|
+
import { DecryptButton } from './ui/decrypt-button';
|
|
7
9
|
import { formatDuration } from '../lib/utils';
|
|
8
10
|
import { DataInspector } from './ui/data-inspector';
|
|
9
11
|
import { ErrorStackBlock, isStructuredErrorWithStack, } from './ui/error-stack-block';
|
|
12
|
+
import { LoadMoreButton } from './ui/load-more-button';
|
|
13
|
+
import { MenuDropdown } from './ui/menu-dropdown';
|
|
10
14
|
import { Skeleton } from './ui/skeleton';
|
|
11
15
|
/**
|
|
12
16
|
* Event types whose eventData contains an error field with a StructuredError.
|
|
@@ -240,7 +244,7 @@ function TreeGutter({ isFirst, isLast, isRunLevel: isRun, statusDotColor, pulse
|
|
|
240
244
|
// ──────────────────────────────────────────────────────────────────────────
|
|
241
245
|
// Copyable cell — shows a copy button on hover
|
|
242
246
|
// ──────────────────────────────────────────────────────────────────────────
|
|
243
|
-
function CopyableCell({ value, className, }) {
|
|
247
|
+
function CopyableCell({ value, className, style: styleProp, }) {
|
|
244
248
|
const [copied, setCopied] = useState(false);
|
|
245
249
|
const resetCopiedTimeoutRef = useRef(null);
|
|
246
250
|
useEffect(() => {
|
|
@@ -263,7 +267,7 @@ function CopyableCell({ value, className, }) {
|
|
|
263
267
|
}, 1500);
|
|
264
268
|
});
|
|
265
269
|
}, [value]);
|
|
266
|
-
return (_jsxs("div", { className: `group/copy flex items-center gap-1
|
|
270
|
+
return (_jsxs("div", { className: `group/copy flex items-center gap-1 min-w-0 px-4 ${className ?? ''}`, style: styleProp, children: [_jsx("span", { className: "overflow-hidden text-ellipsis whitespace-nowrap", children: value || '-' }), value ? (_jsx("button", { type: "button", onClick: handleCopy, className: "flex-shrink-0 opacity-0 group-hover/copy:opacity-100 transition-opacity p-0.5 rounded hover:bg-[var(--ds-gray-alpha-200)]", style: BUTTON_RESET_STYLE, "aria-label": `Copy ${value}`, children: copied ? (_jsx(Check, { className: "h-3 w-3", style: { color: 'var(--ds-green-700)' } })) : (_jsx(Copy, { className: "h-3 w-3", style: { color: 'var(--ds-gray-700)' } })) })) : null] }));
|
|
267
271
|
}
|
|
268
272
|
/** Recursively parse stringified JSON values so escaped slashes / quotes are cleaned up */
|
|
269
273
|
function deepParseJson(value) {
|
|
@@ -354,20 +358,24 @@ function PayloadBlock({ data, eventType, }) {
|
|
|
354
358
|
}
|
|
355
359
|
return (_jsxs("div", { className: "relative group/payload", children: [_jsx("div", { className: "overflow-x-auto p-2 text-[11px]", style: { color: 'var(--ds-gray-1000)' }, children: _jsx(DataInspector, { data: cleaned, expandLevel: 2 }) }), _jsx("button", { type: "button", onClick: handleCopy, className: "absolute bottom-2 right-2 opacity-0 group-hover/payload:opacity-100 transition-opacity flex items-center gap-1 px-2 py-1 rounded-md text-xs hover:bg-[var(--ds-gray-alpha-200)]", style: { ...BUTTON_RESET_STYLE, color: 'var(--ds-gray-700)' }, "aria-label": "Copy payload", children: copied ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-3 w-3", style: { color: 'var(--ds-green-700)' } }), _jsx("span", { style: { color: 'var(--ds-green-700)' }, children: "Copied" })] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "h-3 w-3" }), _jsx("span", { children: "Copy" })] })) })] }));
|
|
356
360
|
}
|
|
357
|
-
|
|
358
|
-
|
|
361
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
362
|
+
// Sort options for the events list
|
|
363
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
364
|
+
const SORT_OPTIONS = [
|
|
365
|
+
{ value: 'desc', label: 'Newest' },
|
|
366
|
+
{ value: 'asc', label: 'Oldest' },
|
|
367
|
+
];
|
|
368
|
+
function RowsSkeleton() {
|
|
369
|
+
return (_jsx("div", { className: "flex-1 overflow-hidden", children: Array.from({ length: 8 }, (_, i) => (_jsxs("div", { className: "flex items-center gap-3 px-4", style: { height: 40 }, children: [_jsx(Skeleton, { className: "h-2 w-2 flex-shrink-0", style: { borderRadius: '50%' } }), _jsx(Skeleton, { className: "h-3", style: { width: 90 } }), _jsx(Skeleton, { className: "h-3", style: { width: 100 } }), _jsx(Skeleton, { className: "h-3", style: { width: 80 } }), _jsx(Skeleton, { className: "h-3 flex-1" }), _jsx(Skeleton, { className: "h-3 flex-1" })] }, i))) }));
|
|
370
|
+
}
|
|
371
|
+
function EventRow({ event, index, isFirst, isLast, isExpanded, onToggleExpand, activeGroupKey, selectedGroupKey, selectedGroupRange, correlationNameMap, workflowName, durationMap, onSelectGroup, onHoverGroup, onLoadEventData, cachedEventData, onCacheEventData, encryptionKey, }) {
|
|
359
372
|
const [isLoading, setIsLoading] = useState(false);
|
|
360
|
-
const [loadedEventData, setLoadedEventData] = useState(
|
|
373
|
+
const [loadedEventData, setLoadedEventData] = useState(cachedEventData);
|
|
361
374
|
const [loadError, setLoadError] = useState(null);
|
|
362
|
-
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(
|
|
363
|
-
const rowGroupKey = event.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
useEffect(() => {
|
|
367
|
-
if (selectedGroupKey !== undefined && selectedGroupKey !== rowGroupKey) {
|
|
368
|
-
setIsExpanded(false);
|
|
369
|
-
}
|
|
370
|
-
}, [selectedGroupKey, rowGroupKey]);
|
|
375
|
+
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(cachedEventData !== null);
|
|
376
|
+
const rowGroupKey = isRunLevel(event.eventType)
|
|
377
|
+
? '__run__'
|
|
378
|
+
: (event.correlationId ?? undefined);
|
|
371
379
|
const statusDotColor = getStatusDotColor(event.eventType);
|
|
372
380
|
const createdAt = new Date(event.createdAt);
|
|
373
381
|
const hasExistingEventData = 'eventData' in event && event.eventData != null;
|
|
@@ -401,9 +409,10 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
|
|
|
401
409
|
setLoadError('Event details unavailable');
|
|
402
410
|
return;
|
|
403
411
|
}
|
|
404
|
-
const
|
|
405
|
-
if (
|
|
406
|
-
setLoadedEventData(
|
|
412
|
+
const data = await onLoadEventData(event);
|
|
413
|
+
if (data !== null && data !== undefined) {
|
|
414
|
+
setLoadedEventData(data);
|
|
415
|
+
onCacheEventData(event.eventId, data);
|
|
407
416
|
}
|
|
408
417
|
}
|
|
409
418
|
catch (err) {
|
|
@@ -413,7 +422,24 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
|
|
|
413
422
|
setIsLoading(false);
|
|
414
423
|
setHasAttemptedLoad(true);
|
|
415
424
|
}
|
|
416
|
-
}, [
|
|
425
|
+
}, [
|
|
426
|
+
event,
|
|
427
|
+
loadedEventData,
|
|
428
|
+
hasExistingEventData,
|
|
429
|
+
onLoadEventData,
|
|
430
|
+
onCacheEventData,
|
|
431
|
+
]);
|
|
432
|
+
// Auto-load event data when remounting in expanded state without cached data
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
if (isExpanded &&
|
|
435
|
+
loadedEventData === null &&
|
|
436
|
+
!hasExistingEventData &&
|
|
437
|
+
!isLoading &&
|
|
438
|
+
!hasAttemptedLoad) {
|
|
439
|
+
loadEventDetails();
|
|
440
|
+
}
|
|
441
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
442
|
+
}, []);
|
|
417
443
|
// When encryption key changes and this event was previously loaded,
|
|
418
444
|
// re-load to get decrypted data
|
|
419
445
|
useEffect(() => {
|
|
@@ -424,6 +450,7 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
|
|
|
424
450
|
.then((data) => {
|
|
425
451
|
if (data !== null && data !== undefined) {
|
|
426
452
|
setLoadedEventData(data);
|
|
453
|
+
onCacheEventData(event.eventId, data);
|
|
427
454
|
}
|
|
428
455
|
setHasAttemptedLoad(true);
|
|
429
456
|
})
|
|
@@ -433,22 +460,23 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
|
|
|
433
460
|
}
|
|
434
461
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
435
462
|
}, [encryptionKey]);
|
|
436
|
-
const handleExpandToggle = useCallback((e) => {
|
|
437
|
-
e.stopPropagation();
|
|
438
|
-
const newExpanded = !isExpanded;
|
|
439
|
-
setIsExpanded(newExpanded);
|
|
440
|
-
if (newExpanded && loadedEventData === null && !hasExistingEventData) {
|
|
441
|
-
loadEventDetails();
|
|
442
|
-
}
|
|
443
|
-
}, [isExpanded, loadedEventData, hasExistingEventData, loadEventDetails]);
|
|
444
463
|
const handleRowClick = useCallback(() => {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
onSelectGroup(rowGroupKey);
|
|
464
|
+
onSelectGroup(rowGroupKey === selectedGroupKey ? undefined : rowGroupKey);
|
|
465
|
+
onToggleExpand(event.eventId);
|
|
466
|
+
if (!isExpanded && loadedEventData === null && !hasExistingEventData) {
|
|
467
|
+
loadEventDetails();
|
|
450
468
|
}
|
|
451
|
-
}, [
|
|
469
|
+
}, [
|
|
470
|
+
selectedGroupKey,
|
|
471
|
+
rowGroupKey,
|
|
472
|
+
onSelectGroup,
|
|
473
|
+
onToggleExpand,
|
|
474
|
+
event.eventId,
|
|
475
|
+
isExpanded,
|
|
476
|
+
loadedEventData,
|
|
477
|
+
hasExistingEventData,
|
|
478
|
+
loadEventDetails,
|
|
479
|
+
]);
|
|
452
480
|
const eventData = hasExistingEventData
|
|
453
481
|
? event.eventData
|
|
454
482
|
: loadedEventData;
|
|
@@ -456,13 +484,12 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
|
|
|
456
484
|
return (_jsxs("div", { "data-event-id": event.eventId, onMouseEnter: () => onHoverGroup(rowGroupKey), onMouseLeave: () => onHoverGroup(undefined), children: [_jsxs("div", { role: "button", tabIndex: 0, onClick: handleRowClick, onKeyDown: (e) => {
|
|
457
485
|
if (e.key === 'Enter' || e.key === ' ')
|
|
458
486
|
handleRowClick();
|
|
459
|
-
}, className: "w-full text-left flex items-center gap-0 text-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
color: 'var(--ds-gray-700)',
|
|
487
|
+
}, 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 }, children: [_jsx(TreeGutter, { isFirst: isFirst, isLast: isLast && !isExpanded, isRunLevel: isRun, statusDotColor: statusDotColor, pulse: isPulsing, hasSelection: hasActive, showBranch: showBranch, showLaneLine: showLaneLine, isLaneStart: isLaneStart, isLaneEnd: isLaneEnd }), _jsxs("div", { className: "flex items-center flex-1 min-w-0", style: { opacity: contentOpacity, transition: 'opacity 150ms' }, children: [_jsx("div", { className: "flex items-center justify-center w-5 h-5 flex-shrink-0 rounded", style: {
|
|
488
|
+
border: '1px solid var(--ds-gray-400)',
|
|
489
|
+
}, children: _jsx(ChevronRight, { className: "h-3 w-3 transition-transform", style: {
|
|
490
|
+
color: 'var(--ds-gray-900)',
|
|
464
491
|
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
465
|
-
} }) }), _jsx("div", { className: "
|
|
492
|
+
} }) }), _jsx("div", { className: "tabular-nums min-w-0 px-4", style: { color: 'var(--ds-gray-900)', flex: '2 1 0%' }, children: formatEventTime(createdAt) }), _jsx("div", { className: "font-medium min-w-0 px-4", style: { flex: '2 1 0%' }, children: _jsxs("span", { className: "inline-flex items-center gap-1.5", style: { color: 'var(--ds-gray-900)' }, children: [_jsxs("span", { style: {
|
|
466
493
|
position: 'relative',
|
|
467
494
|
display: 'inline-flex',
|
|
468
495
|
width: 6,
|
|
@@ -481,7 +508,7 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
|
|
|
481
508
|
height: 6,
|
|
482
509
|
borderRadius: '50%',
|
|
483
510
|
backgroundColor: statusDotColor,
|
|
484
|
-
} })] }), formatEventType(event.eventType)] }) }), _jsx("div", { className: "
|
|
511
|
+
} })] }), formatEventType(event.eventType)] }) }), _jsx("div", { className: "min-w-0 px-4 overflow-hidden text-ellipsis whitespace-nowrap", style: { flex: '2 1 0%' }, title: eventName !== '-' ? eventName : undefined, children: eventName }), _jsx(CopyableCell, { value: event.correlationId || '', className: "font-mono", style: { flex: '3 1 0%' } }), _jsx(CopyableCell, { value: event.eventId, className: "font-mono", style: { flex: '3 1 0%' } })] })] }), isExpanded && (_jsxs("div", { className: "flex", children: [_jsx(TreeGutter, { isFirst: false, isLast: isLast, isRunLevel: isRun, hasSelection: hasActive, showBranch: false, showLaneLine: showLaneLine && !isLaneEnd, isLaneStart: false, isLaneEnd: false, continuationOnly: true }), _jsx("div", { className: "w-5 flex-shrink-0" }), _jsxs("div", { className: "flex-1 my-1.5 mr-3 ml-2 py-2 rounded-md border overflow-hidden", style: {
|
|
485
512
|
borderColor: 'var(--ds-gray-alpha-200)',
|
|
486
513
|
opacity: contentOpacity,
|
|
487
514
|
transition: 'opacity 150ms',
|
|
@@ -499,11 +526,41 @@ function EventRow({ event, index, isFirst, isLast, activeGroupKey, selectedGroup
|
|
|
499
526
|
// ──────────────────────────────────────────────────────────────────────────
|
|
500
527
|
// Main component
|
|
501
528
|
// ──────────────────────────────────────────────────────────────────────────
|
|
502
|
-
export function EventListView({ events, run, onLoadEventData, hasMoreEvents = false, isLoadingMoreEvents = false, onLoadMoreEvents, encryptionKey, }) {
|
|
529
|
+
export function EventListView({ events, run, onLoadEventData, hasMoreEvents = false, isLoadingMoreEvents = false, onLoadMoreEvents, encryptionKey, isLoading = false, sortOrder: sortOrderProp, onSortOrderChange, onDecrypt, isDecrypting = false, }) {
|
|
530
|
+
const [internalSortOrder, setInternalSortOrder] = useState('asc');
|
|
531
|
+
const effectiveSortOrder = sortOrderProp ?? internalSortOrder;
|
|
532
|
+
const handleSortOrderChange = useCallback((order) => {
|
|
533
|
+
if (onSortOrderChange) {
|
|
534
|
+
onSortOrderChange(order);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
setInternalSortOrder(order);
|
|
538
|
+
}
|
|
539
|
+
}, [onSortOrderChange]);
|
|
503
540
|
const sortedEvents = useMemo(() => {
|
|
504
541
|
if (!events || events.length === 0)
|
|
505
542
|
return [];
|
|
506
|
-
|
|
543
|
+
const dir = effectiveSortOrder === 'desc' ? -1 : 1;
|
|
544
|
+
return [...events].sort((a, b) => dir *
|
|
545
|
+
(new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()));
|
|
546
|
+
}, [events, effectiveSortOrder]);
|
|
547
|
+
// Detect encrypted fields across all loaded events.
|
|
548
|
+
// Only checks top-level eventData values (input, output, result, etc.) —
|
|
549
|
+
// the current data model guarantees encrypted markers appear at this level.
|
|
550
|
+
const hasEncryptedData = useMemo(() => {
|
|
551
|
+
if (!events)
|
|
552
|
+
return false;
|
|
553
|
+
for (const event of events) {
|
|
554
|
+
const ed = event.eventData;
|
|
555
|
+
if (!ed || typeof ed !== 'object')
|
|
556
|
+
continue;
|
|
557
|
+
const data = ed;
|
|
558
|
+
for (const val of Object.values(data)) {
|
|
559
|
+
if (isEncryptedMarker(val))
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return false;
|
|
507
564
|
}, [events]);
|
|
508
565
|
const { correlationNameMap, workflowName } = useMemo(() => buildNameMaps(events ?? null, run ?? null), [events, run]);
|
|
509
566
|
const durationMap = useMemo(() => buildDurationMap(sortedEvents), [sortedEvents]);
|
|
@@ -516,6 +573,57 @@ export function EventListView({ events, run, onLoadEventData, hasMoreEvents = fa
|
|
|
516
573
|
setHoveredGroupKey(groupKey);
|
|
517
574
|
}, []);
|
|
518
575
|
const activeGroupKey = selectedGroupKey ?? hoveredGroupKey;
|
|
576
|
+
// Expanded state lifted out of EventRow so it survives virtualization
|
|
577
|
+
const [expandedEventIds, setExpandedEventIds] = useState(() => new Set());
|
|
578
|
+
const toggleEventExpanded = useCallback((eventId) => {
|
|
579
|
+
setExpandedEventIds((prev) => {
|
|
580
|
+
const next = new Set(prev);
|
|
581
|
+
if (next.has(eventId)) {
|
|
582
|
+
next.delete(eventId);
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
next.add(eventId);
|
|
586
|
+
}
|
|
587
|
+
return next;
|
|
588
|
+
});
|
|
589
|
+
}, []);
|
|
590
|
+
// Event data cache — ref avoids re-renders when cache updates
|
|
591
|
+
const eventDataCacheRef = useRef(new Map());
|
|
592
|
+
const cacheEventData = useCallback((eventId, data) => {
|
|
593
|
+
eventDataCacheRef.current.set(eventId, data);
|
|
594
|
+
}, []);
|
|
595
|
+
// Lookup from eventId → groupKey for efficient collapse filtering
|
|
596
|
+
const eventGroupKeyMap = useMemo(() => {
|
|
597
|
+
const map = new Map();
|
|
598
|
+
for (const ev of sortedEvents) {
|
|
599
|
+
const gk = isRunLevel(ev.eventType)
|
|
600
|
+
? '__run__'
|
|
601
|
+
: (ev.correlationId ?? '');
|
|
602
|
+
if (gk)
|
|
603
|
+
map.set(ev.eventId, gk);
|
|
604
|
+
}
|
|
605
|
+
return map;
|
|
606
|
+
}, [sortedEvents]);
|
|
607
|
+
// Collapse expanded events that don't belong to the newly selected group
|
|
608
|
+
useEffect(() => {
|
|
609
|
+
if (selectedGroupKey === undefined)
|
|
610
|
+
return;
|
|
611
|
+
setExpandedEventIds((prev) => {
|
|
612
|
+
if (prev.size === 0)
|
|
613
|
+
return prev;
|
|
614
|
+
let changed = false;
|
|
615
|
+
const next = new Set();
|
|
616
|
+
for (const eventId of prev) {
|
|
617
|
+
if (eventGroupKeyMap.get(eventId) === selectedGroupKey) {
|
|
618
|
+
next.add(eventId);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
changed = true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return changed ? next : prev;
|
|
625
|
+
});
|
|
626
|
+
}, [selectedGroupKey, eventGroupKeyMap]);
|
|
519
627
|
// Compute the row-index range for the active group's connecting lane line.
|
|
520
628
|
// Only applies to non-run groups (step/hook/wait correlations).
|
|
521
629
|
const selectedGroupRange = useMemo(() => {
|
|
@@ -538,83 +646,117 @@ export function EventListView({ events, run, onLoadEventData, hasMoreEvents = fa
|
|
|
538
646
|
const entries = [];
|
|
539
647
|
for (let i = 0; i < sortedEvents.length; i++) {
|
|
540
648
|
const ev = sortedEvents[i];
|
|
649
|
+
const isRun = isRunLevel(ev.eventType);
|
|
650
|
+
const name = isRun
|
|
651
|
+
? (workflowName ?? '')
|
|
652
|
+
: ev.correlationId
|
|
653
|
+
? (correlationNameMap.get(ev.correlationId) ?? '')
|
|
654
|
+
: '';
|
|
541
655
|
entries.push({
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
656
|
+
fields: [
|
|
657
|
+
ev.eventId,
|
|
658
|
+
ev.correlationId ?? '',
|
|
659
|
+
ev.eventType,
|
|
660
|
+
formatEventType(ev.eventType),
|
|
661
|
+
name,
|
|
662
|
+
].map((f) => f.toLowerCase()),
|
|
663
|
+
groupKey: ev.correlationId ?? (isRun ? '__run__' : undefined),
|
|
545
664
|
eventId: ev.eventId,
|
|
546
665
|
index: i,
|
|
547
666
|
});
|
|
548
667
|
}
|
|
549
668
|
return entries;
|
|
550
|
-
}, [sortedEvents]);
|
|
669
|
+
}, [sortedEvents, correlationNameMap, workflowName]);
|
|
551
670
|
useEffect(() => {
|
|
552
671
|
const q = searchQuery.trim().toLowerCase();
|
|
553
672
|
if (!q) {
|
|
554
673
|
setSelectedGroupKey(undefined);
|
|
555
674
|
return;
|
|
556
675
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
676
|
+
let bestMatch = null;
|
|
677
|
+
let bestScore = 0;
|
|
678
|
+
for (const entry of searchIndex) {
|
|
679
|
+
for (const field of entry.fields) {
|
|
680
|
+
if (field && field.includes(q)) {
|
|
681
|
+
const score = q.length / field.length;
|
|
682
|
+
if (score > bestScore) {
|
|
683
|
+
bestScore = score;
|
|
684
|
+
bestMatch = entry;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (bestMatch) {
|
|
690
|
+
setSelectedGroupKey(bestMatch.groupKey);
|
|
560
691
|
virtuosoRef.current?.scrollToIndex({
|
|
561
|
-
index:
|
|
692
|
+
index: bestMatch.index,
|
|
562
693
|
align: 'center',
|
|
563
694
|
behavior: 'smooth',
|
|
564
695
|
});
|
|
565
696
|
}
|
|
566
697
|
}, [searchQuery, searchIndex]);
|
|
567
|
-
|
|
698
|
+
// Track whether we've ever had events to distinguish initial load from refetch
|
|
699
|
+
const hasHadEventsRef = useRef(false);
|
|
700
|
+
if (sortedEvents.length > 0) {
|
|
701
|
+
hasHadEventsRef.current = true;
|
|
702
|
+
}
|
|
703
|
+
const isInitialLoad = isLoading && !hasHadEventsRef.current;
|
|
704
|
+
const isRefetching = isLoading && hasHadEventsRef.current && sortedEvents.length === 0;
|
|
705
|
+
if (isInitialLoad) {
|
|
706
|
+
return (_jsxs("div", { className: "h-full flex flex-col overflow-hidden", children: [_jsx("div", { style: { padding: 6 }, children: _jsx(Skeleton, { style: { height: 40, borderRadius: 6 } }) }), _jsxs("div", { className: "flex items-center gap-0 h-10 border-b flex-shrink-0 px-4", style: { borderColor: 'var(--ds-gray-alpha-200)' }, children: [_jsx(Skeleton, { className: "h-3", style: { width: 60 } }), _jsx("div", { style: { flex: 1 } }), _jsx(Skeleton, { className: "h-3", style: { width: 80 } }), _jsx("div", { style: { flex: 1 } }), _jsx(Skeleton, { className: "h-3", style: { width: 50 } }), _jsx("div", { style: { flex: 1 } }), _jsx(Skeleton, { className: "h-3", style: { width: 90 } }), _jsx("div", { style: { flex: 1 } }), _jsx(Skeleton, { className: "h-3", style: { width: 70 } })] }), _jsx(RowsSkeleton, {})] }));
|
|
707
|
+
}
|
|
708
|
+
if (!isLoading && (!events || events.length === 0)) {
|
|
568
709
|
return (_jsx("div", { className: "flex items-center justify-center h-full text-sm", style: { color: 'var(--ds-gray-700)' }, children: "No events found" }));
|
|
569
710
|
}
|
|
570
|
-
return (_jsxs("div", { className: "h-full flex flex-col overflow-hidden", children: [_jsx("style", { children: `@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}` }),
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
711
|
+
return (_jsxs("div", { className: "h-full flex flex-col overflow-hidden", children: [_jsx("style", { children: `@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}` }), _jsxs("div", { style: {
|
|
712
|
+
padding: 6,
|
|
713
|
+
backgroundColor: 'var(--ds-background-100)',
|
|
714
|
+
display: 'flex',
|
|
715
|
+
gap: 6,
|
|
716
|
+
}, children: [_jsxs("label", { style: {
|
|
717
|
+
display: 'flex',
|
|
718
|
+
alignItems: 'center',
|
|
719
|
+
justifyContent: 'center',
|
|
720
|
+
borderRadius: 6,
|
|
721
|
+
boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
|
|
722
|
+
background: 'var(--ds-background-100)',
|
|
723
|
+
height: 40,
|
|
724
|
+
flex: 1,
|
|
725
|
+
minWidth: 0,
|
|
726
|
+
}, children: [_jsx("div", { style: {
|
|
727
|
+
width: 40,
|
|
728
|
+
height: 40,
|
|
729
|
+
display: 'flex',
|
|
730
|
+
alignItems: 'center',
|
|
731
|
+
justifyContent: 'center',
|
|
732
|
+
color: 'var(--ds-gray-800)',
|
|
733
|
+
flexShrink: 0,
|
|
734
|
+
}, children: _jsxs("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", focusable: "false", children: [_jsx("circle", { cx: "7", cy: "7", r: "4.5", stroke: "currentColor", strokeWidth: "1.5" }), _jsx("path", { d: "M11.5 11.5L14 14", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })] }) }), _jsx("input", { type: "search", placeholder: "Search by name, event type, or ID\u2026", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), style: {
|
|
735
|
+
marginLeft: -16,
|
|
736
|
+
paddingInline: 12,
|
|
737
|
+
fontFamily: 'inherit',
|
|
738
|
+
fontSize: 14,
|
|
739
|
+
background: 'transparent',
|
|
740
|
+
border: 'none',
|
|
741
|
+
outline: 'none',
|
|
742
|
+
height: 40,
|
|
743
|
+
width: '100%',
|
|
744
|
+
} })] }), _jsx(MenuDropdown, { options: SORT_OPTIONS, value: effectiveSortOrder, onChange: handleSortOrderChange }), (hasEncryptedData || encryptionKey) && onDecrypt && (_jsx(DecryptButton, { decrypted: !!encryptionKey, loading: isDecrypting, onClick: onDecrypt }))] }), _jsxs("div", { className: "flex items-center gap-0 text-[13px] font-medium h-10 border-b flex-shrink-0", style: {
|
|
597
745
|
borderColor: 'var(--ds-gray-alpha-200)',
|
|
598
746
|
color: 'var(--ds-gray-900)',
|
|
599
747
|
backgroundColor: 'var(--ds-background-100)',
|
|
600
|
-
}, children: [_jsx("div", { className: "flex-shrink-0", style: { width: GUTTER_WIDTH } }), _jsx("div", { className: "w-5 flex-shrink-0" }), _jsx("div", { className: "
|
|
748
|
+
}, children: [_jsx("div", { className: "flex-shrink-0", style: { width: GUTTER_WIDTH } }), _jsx("div", { className: "w-5 flex-shrink-0" }), _jsx("div", { className: "min-w-0 px-4", style: { flex: '2 1 0%' }, children: "Time" }), _jsx("div", { className: "min-w-0 px-4", style: { flex: '2 1 0%' }, children: "Event Type" }), _jsx("div", { className: "min-w-0 px-4", style: { flex: '2 1 0%' }, children: "Name" }), _jsx("div", { className: "min-w-0 px-4", style: { flex: '3 1 0%' }, children: "Correlation ID" }), _jsx("div", { className: "min-w-0 px-4", style: { flex: '3 1 0%' }, children: "Event ID" })] }), isRefetching ? (_jsx(RowsSkeleton, {})) : (_jsx(Virtuoso, { ref: virtuosoRef, totalCount: sortedEvents.length, overscan: 20, defaultItemHeight: 40, endReached: () => {
|
|
601
749
|
if (!hasMoreEvents || isLoadingMoreEvents) {
|
|
602
750
|
return;
|
|
603
751
|
}
|
|
604
752
|
void onLoadMoreEvents?.();
|
|
605
753
|
}, itemContent: (index) => {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
? 'Loading more events...'
|
|
614
|
-
: 'Load more' }) })), _jsxs("div", { className: "mt-4 pt-3 border-t text-xs px-3", style: {
|
|
615
|
-
borderColor: 'var(--ds-gray-alpha-200)',
|
|
616
|
-
color: 'var(--ds-gray-900)',
|
|
617
|
-
}, children: [sortedEvents.length, " event", sortedEvents.length !== 1 ? 's' : '', " total"] })] })),
|
|
618
|
-
}, style: { flex: 1, minHeight: 0 } })] }));
|
|
754
|
+
const ev = sortedEvents[index];
|
|
755
|
+
return (_jsx(EventRow, { event: ev, index: index, isFirst: index === 0, isLast: index === sortedEvents.length - 1, isExpanded: expandedEventIds.has(ev.eventId), onToggleExpand: toggleEventExpanded, activeGroupKey: activeGroupKey, selectedGroupKey: selectedGroupKey, selectedGroupRange: selectedGroupRange, correlationNameMap: correlationNameMap, workflowName: workflowName, durationMap: durationMap, onSelectGroup: onSelectGroup, onHoverGroup: onHoverGroup, onLoadEventData: onLoadEventData, cachedEventData: eventDataCacheRef.current.get(ev.eventId) ?? null, onCacheEventData: cacheEventData, encryptionKey: encryptionKey }));
|
|
756
|
+
}, style: { flex: 1, minHeight: 0 } })), _jsxs("div", { className: "relative flex-shrink-0 flex items-center h-10 border-t px-4 text-xs", style: {
|
|
757
|
+
borderColor: 'var(--ds-gray-alpha-200)',
|
|
758
|
+
color: 'var(--ds-gray-900)',
|
|
759
|
+
backgroundColor: 'var(--ds-background-100)',
|
|
760
|
+
}, children: [_jsxs("span", { children: [sortedEvents.length, " event", sortedEvents.length !== 1 ? 's' : '', " loaded"] }), hasMoreEvents && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: _jsx("div", { className: "pointer-events-auto", children: _jsx(LoadMoreButton, { loading: isLoadingMoreEvents, onClick: () => void onLoadMoreEvents?.() }) }) }))] })] }));
|
|
619
761
|
}
|
|
620
762
|
//# sourceMappingURL=event-list-view.js.map
|