@workflow/web-shared 4.1.0-beta.52 → 4.1.0-beta.53
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/dist/components/error-boundary.d.ts +15 -20
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +17 -31
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/event-list-view.d.ts +7 -6
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +492 -109
- 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 +2 -1
- 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 +2 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +53 -142
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
- package/dist/components/sidebar/conversation-view.js +3 -17
- package/dist/components/sidebar/conversation-view.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 +63 -10
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +4 -8
- package/dist/components/sidebar/events-list.js.map +1 -1
- package/dist/components/sidebar/resolve-hook-modal.d.ts +3 -0
- package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
- package/dist/components/sidebar/resolve-hook-modal.js +152 -3
- package/dist/components/sidebar/resolve-hook-modal.js.map +1 -1
- package/dist/components/stream-viewer.d.ts +7 -5
- package/dist/components/stream-viewer.d.ts.map +1 -1
- package/dist/components/stream-viewer.js +54 -22
- package/dist/components/stream-viewer.js.map +1 -1
- package/dist/components/trace-viewer/components/markers.d.ts +2 -1
- package/dist/components/trace-viewer/components/markers.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/markers.js +59 -20
- package/dist/components/trace-viewer/components/markers.js.map +1 -1
- package/dist/components/trace-viewer/components/node.d.ts +5 -1
- package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/node.js +250 -68
- package/dist/components/trace-viewer/components/node.js.map +1 -1
- package/dist/components/trace-viewer/components/span-content.d.ts +19 -0
- package/dist/components/trace-viewer/components/span-content.d.ts.map +1 -0
- package/dist/components/trace-viewer/components/span-content.js +137 -0
- package/dist/components/trace-viewer/components/span-content.js.map +1 -0
- package/dist/components/trace-viewer/components/span-detail-panel.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/span-detail-panel.js +3 -2
- package/dist/components/trace-viewer/components/span-detail-panel.js.map +1 -1
- package/dist/components/trace-viewer/components/span-segments.d.ts +50 -0
- package/dist/components/trace-viewer/components/span-segments.d.ts.map +1 -0
- package/dist/components/trace-viewer/components/span-segments.js +392 -0
- package/dist/components/trace-viewer/components/span-segments.js.map +1 -0
- package/dist/components/trace-viewer/components/span-strategies.d.ts +46 -0
- package/dist/components/trace-viewer/components/span-strategies.d.ts.map +1 -0
- package/dist/components/trace-viewer/components/span-strategies.js +108 -0
- package/dist/components/trace-viewer/components/span-strategies.js.map +1 -0
- package/dist/components/trace-viewer/context.d.ts +7 -6
- package/dist/components/trace-viewer/context.d.ts.map +1 -1
- package/dist/components/trace-viewer/context.js +47 -18
- package/dist/components/trace-viewer/context.js.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.d.ts +5 -1
- package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.js +87 -11
- package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.module.css +179 -6
- package/dist/components/trace-viewer/util/timing.d.ts +5 -0
- package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
- package/dist/components/trace-viewer/util/timing.js +12 -0
- package/dist/components/trace-viewer/util/timing.js.map +1 -1
- package/dist/components/trace-viewer/util/use-streaming-spans.d.ts +1 -1
- package/dist/components/trace-viewer/util/use-streaming-spans.d.ts.map +1 -1
- package/dist/components/trace-viewer/util/use-streaming-spans.js +29 -17
- package/dist/components/trace-viewer/util/use-streaming-spans.js.map +1 -1
- package/dist/components/trace-viewer/worker.js +3 -1
- package/dist/components/trace-viewer/worker.js.map +1 -1
- package/dist/components/ui/alert.js +3 -3
- package/dist/components/ui/alert.js.map +1 -1
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/card.js +2 -2
- package/dist/components/ui/card.js.map +1 -1
- package/dist/components/ui/data-inspector.d.ts +17 -0
- package/dist/components/ui/data-inspector.d.ts.map +1 -0
- package/dist/components/ui/data-inspector.js +184 -0
- package/dist/components/ui/data-inspector.js.map +1 -0
- package/dist/components/ui/error-card.d.ts.map +1 -1
- package/dist/components/ui/error-card.js +4 -1
- package/dist/components/ui/error-card.js.map +1 -1
- package/dist/components/ui/inspector-theme.d.ts +39 -24
- package/dist/components/ui/inspector-theme.d.ts.map +1 -1
- package/dist/components/ui/inspector-theme.js +90 -38
- package/dist/components/ui/inspector-theme.js.map +1 -1
- package/dist/components/ui/skeleton.d.ts +1 -1
- package/dist/components/ui/skeleton.d.ts.map +1 -1
- package/dist/components/ui/skeleton.js +2 -2
- package/dist/components/ui/skeleton.js.map +1 -1
- 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 +435 -21
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +2 -2
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +17 -3
- package/dist/lib/hydration.js.map +1 -1
- package/dist/styles.css +186 -0
- package/package.json +8 -7
- package/src/components/error-boundary.tsx +29 -40
- package/src/components/event-list-view.tsx +1000 -287
- package/src/components/index.ts +1 -0
- package/src/components/run-trace-view.tsx +3 -0
- package/src/components/sidebar/attribute-panel.tsx +58 -258
- package/src/components/sidebar/conversation-view.tsx +30 -27
- package/src/components/sidebar/entity-detail-panel.tsx +86 -20
- package/src/components/sidebar/events-list.tsx +4 -11
- package/src/components/sidebar/resolve-hook-modal.tsx +206 -47
- package/src/components/stream-viewer.tsx +138 -61
- package/src/components/trace-viewer/components/markers.tsx +69 -21
- package/src/components/trace-viewer/components/node.tsx +346 -100
- package/src/components/trace-viewer/components/span-content.tsx +247 -0
- package/src/components/trace-viewer/components/span-detail-panel.tsx +7 -2
- package/src/components/trace-viewer/components/span-segments.ts +516 -0
- package/src/components/trace-viewer/components/span-strategies.ts +205 -0
- package/src/components/trace-viewer/context.tsx +92 -40
- package/src/components/trace-viewer/trace-viewer.module.css +179 -6
- package/src/components/trace-viewer/trace-viewer.tsx +115 -11
- package/src/components/trace-viewer/util/timing.ts +13 -0
- package/src/components/trace-viewer/util/use-streaming-spans.ts +28 -17
- package/src/components/trace-viewer/worker.ts +4 -1
- package/src/components/ui/alert.tsx +3 -3
- package/src/components/ui/card.tsx +3 -5
- package/src/components/ui/data-inspector.tsx +318 -0
- package/src/components/ui/error-card.tsx +17 -6
- package/src/components/ui/inspector-theme.ts +127 -39
- package/src/components/ui/skeleton.tsx +3 -1
- package/src/components/workflow-trace-view.tsx +625 -26
- package/src/components/workflow-traces/trace-span-construction.ts +3 -2
- package/src/lib/hydration.ts +17 -8
- package/src/styles.css +186 -0
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
3
|
+
import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
|
|
4
|
+
import type { Event, Step, WorkflowRun } from '@workflow/world';
|
|
5
|
+
import { Check, ChevronRight, Copy } from 'lucide-react';
|
|
6
|
+
import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
+
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
|
|
9
|
+
import { formatDuration } from '../lib/utils';
|
|
10
|
+
import { DataInspector } from './ui/data-inspector';
|
|
11
|
+
import { Skeleton } from './ui/skeleton';
|
|
12
|
+
|
|
13
|
+
const BUTTON_RESET_STYLE: React.CSSProperties = {
|
|
14
|
+
appearance: 'none',
|
|
15
|
+
WebkitAppearance: 'none',
|
|
16
|
+
border: 'none',
|
|
17
|
+
background: 'transparent',
|
|
18
|
+
};
|
|
19
|
+
const DOT_PULSE_ANIMATION =
|
|
20
|
+
'workflow-dot-pulse 1.25s cubic-bezier(0, 0, 0.2, 1) infinite';
|
|
21
|
+
|
|
22
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// Helpers
|
|
24
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
10
25
|
|
|
11
|
-
/**
|
|
12
|
-
* Format a date to a human-readable local time string with milliseconds
|
|
13
|
-
*/
|
|
14
26
|
function formatEventTime(date: Date): string {
|
|
15
27
|
return (
|
|
16
28
|
date.toLocaleTimeString('en-US', {
|
|
@@ -24,72 +36,586 @@ function formatEventTime(date: Date): string {
|
|
|
24
36
|
);
|
|
25
37
|
}
|
|
26
38
|
|
|
39
|
+
function formatEventType(eventType: Event['eventType']): string {
|
|
40
|
+
return eventType
|
|
41
|
+
.split('_')
|
|
42
|
+
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
43
|
+
.join(' ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
47
|
+
// Event type → status color (small dot only)
|
|
48
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/** Returns a CSS color using Geist design tokens for the status dot. */
|
|
51
|
+
function getStatusDotColor(eventType: string): string {
|
|
52
|
+
// Failed → red
|
|
53
|
+
if (
|
|
54
|
+
eventType === 'step_failed' ||
|
|
55
|
+
eventType === 'run_failed' ||
|
|
56
|
+
eventType === 'workflow_failed'
|
|
57
|
+
) {
|
|
58
|
+
return 'var(--ds-red-700)';
|
|
59
|
+
}
|
|
60
|
+
// Cancelled → amber
|
|
61
|
+
if (eventType === 'run_cancelled') {
|
|
62
|
+
return 'var(--ds-amber-700)';
|
|
63
|
+
}
|
|
64
|
+
// Retrying → amber
|
|
65
|
+
if (eventType === 'step_retrying') {
|
|
66
|
+
return 'var(--ds-amber-700)';
|
|
67
|
+
}
|
|
68
|
+
// Completed/succeeded → green
|
|
69
|
+
if (
|
|
70
|
+
eventType === 'step_completed' ||
|
|
71
|
+
eventType === 'run_completed' ||
|
|
72
|
+
eventType === 'workflow_completed' ||
|
|
73
|
+
eventType === 'hook_disposed' ||
|
|
74
|
+
eventType === 'wait_completed'
|
|
75
|
+
) {
|
|
76
|
+
return 'var(--ds-green-700)';
|
|
77
|
+
}
|
|
78
|
+
// Started/running → blue
|
|
79
|
+
if (
|
|
80
|
+
eventType === 'step_started' ||
|
|
81
|
+
eventType === 'run_started' ||
|
|
82
|
+
eventType === 'workflow_started' ||
|
|
83
|
+
eventType === 'hook_received'
|
|
84
|
+
) {
|
|
85
|
+
return 'var(--ds-blue-700)';
|
|
86
|
+
}
|
|
87
|
+
// Created/pending → gray
|
|
88
|
+
return 'var(--ds-gray-600)';
|
|
89
|
+
}
|
|
90
|
+
|
|
27
91
|
/**
|
|
28
|
-
*
|
|
92
|
+
* Build a map from correlationId (stepId) → display name using step entities,
|
|
93
|
+
* and parse the workflow name from the run.
|
|
29
94
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
95
|
+
function buildNameMaps(
|
|
96
|
+
steps: Step[] | null,
|
|
97
|
+
run: WorkflowRun | null
|
|
98
|
+
): {
|
|
99
|
+
correlationNameMap: Map<string, string>;
|
|
100
|
+
workflowName: string | null;
|
|
101
|
+
} {
|
|
102
|
+
const correlationNameMap = new Map<string, string>();
|
|
103
|
+
|
|
104
|
+
// Map step correlationId (= stepId) → parsed step name
|
|
105
|
+
if (steps) {
|
|
106
|
+
for (const step of steps) {
|
|
107
|
+
const parsed = parseStepName(String(step.stepName));
|
|
108
|
+
correlationNameMap.set(step.stepId, parsed?.shortName ?? step.stepName);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Parse workflow name from run
|
|
113
|
+
const workflowName = run?.workflowName
|
|
114
|
+
? (parseWorkflowName(run.workflowName)?.shortName ?? run.workflowName)
|
|
115
|
+
: null;
|
|
116
|
+
|
|
117
|
+
return { correlationNameMap, workflowName };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface DurationInfo {
|
|
121
|
+
/** Time from created → started (ms) */
|
|
122
|
+
queued?: number;
|
|
123
|
+
/** Time from started → completed/failed/cancelled (ms) */
|
|
124
|
+
ran?: number;
|
|
40
125
|
}
|
|
41
126
|
|
|
42
127
|
/**
|
|
43
|
-
*
|
|
128
|
+
* Build a map from correlationId → duration info by diffing
|
|
129
|
+
* created ↔ started (queued) and started ↔ completed/failed/cancelled (ran).
|
|
130
|
+
* Also computes run-level durations under the key '__run__'.
|
|
44
131
|
*/
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
132
|
+
function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
|
|
133
|
+
const createdTimes = new Map<string, number>();
|
|
134
|
+
const startedTimes = new Map<string, number>();
|
|
135
|
+
const durations = new Map<string, DurationInfo>();
|
|
136
|
+
|
|
137
|
+
for (const event of events) {
|
|
138
|
+
const ts = new Date(event.createdAt).getTime();
|
|
139
|
+
const key = event.correlationId ?? '__run__';
|
|
140
|
+
const type: string = event.eventType;
|
|
141
|
+
|
|
142
|
+
// Track created times (first event for each correlation)
|
|
143
|
+
if (type === 'step_created' || type === 'run_created') {
|
|
144
|
+
createdTimes.set(key, ts);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Track started times & compute queued duration
|
|
148
|
+
if (
|
|
149
|
+
type === 'step_started' ||
|
|
150
|
+
type === 'run_started' ||
|
|
151
|
+
type === 'workflow_started'
|
|
152
|
+
) {
|
|
153
|
+
startedTimes.set(key, ts);
|
|
154
|
+
// If no explicit created event was seen, use the started time as created
|
|
155
|
+
if (!createdTimes.has(key)) {
|
|
156
|
+
createdTimes.set(key, ts);
|
|
157
|
+
}
|
|
158
|
+
const createdAt = createdTimes.get(key);
|
|
159
|
+
const info = durations.get(key) ?? {};
|
|
160
|
+
if (createdAt !== undefined) {
|
|
161
|
+
info.queued = ts - createdAt;
|
|
162
|
+
}
|
|
163
|
+
durations.set(key, info);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Compute ran duration on terminal events
|
|
167
|
+
if (
|
|
168
|
+
type === 'step_completed' ||
|
|
169
|
+
type === 'step_failed' ||
|
|
170
|
+
type === 'run_completed' ||
|
|
171
|
+
type === 'run_failed' ||
|
|
172
|
+
type === 'run_cancelled' ||
|
|
173
|
+
type === 'workflow_completed' ||
|
|
174
|
+
type === 'workflow_failed' ||
|
|
175
|
+
type === 'wait_completed' ||
|
|
176
|
+
type === 'hook_disposed'
|
|
177
|
+
) {
|
|
178
|
+
const startedAt = startedTimes.get(key);
|
|
179
|
+
const info = durations.get(key) ?? {};
|
|
180
|
+
if (startedAt !== undefined) {
|
|
181
|
+
info.ran = ts - startedAt;
|
|
182
|
+
}
|
|
183
|
+
durations.set(key, info);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return durations;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isRunLevel(eventType: string): boolean {
|
|
191
|
+
return (
|
|
192
|
+
eventType === 'run_created' ||
|
|
193
|
+
eventType === 'run_started' ||
|
|
194
|
+
eventType === 'run_completed' ||
|
|
195
|
+
eventType === 'run_failed' ||
|
|
196
|
+
eventType === 'run_cancelled' ||
|
|
197
|
+
eventType === 'workflow_started' ||
|
|
198
|
+
eventType === 'workflow_completed' ||
|
|
199
|
+
eventType === 'workflow_failed'
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
204
|
+
// Tree gutter — fixed-width, shows branch lines only for the selected group
|
|
205
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/** Fixed gutter width: 20px root area + 16px for one branch lane */
|
|
208
|
+
const GUTTER_WIDTH = 36;
|
|
209
|
+
/** X position of the single branch lane line */
|
|
210
|
+
const LANE_X = 20;
|
|
211
|
+
const ROOT_LINE_COLOR = 'var(--ds-gray-500)';
|
|
212
|
+
|
|
213
|
+
function TreeGutter({
|
|
214
|
+
isFirst,
|
|
215
|
+
isLast,
|
|
216
|
+
isRunLevel: isRun,
|
|
217
|
+
statusDotColor,
|
|
218
|
+
pulse = false,
|
|
219
|
+
hasSelection,
|
|
220
|
+
showBranch,
|
|
221
|
+
showLaneLine,
|
|
222
|
+
isLaneStart,
|
|
223
|
+
isLaneEnd,
|
|
224
|
+
continuationOnly = false,
|
|
225
|
+
}: {
|
|
226
|
+
isFirst: boolean;
|
|
227
|
+
isLast: boolean;
|
|
228
|
+
isRunLevel: boolean;
|
|
229
|
+
statusDotColor?: string;
|
|
230
|
+
pulse?: boolean;
|
|
231
|
+
/** Whether any group is currently active (selected or hovered) */
|
|
232
|
+
hasSelection: boolean;
|
|
233
|
+
/** Whether to show a horizontal branch line for this row (event belongs to active group) */
|
|
234
|
+
showBranch: boolean;
|
|
235
|
+
/** Whether the vertical lane line passes through this row */
|
|
236
|
+
showLaneLine: boolean;
|
|
237
|
+
/** Whether the vertical lane line starts at this row (top clipped to 50%) */
|
|
238
|
+
isLaneStart: boolean;
|
|
239
|
+
/** Whether the vertical lane line ends at this row (bottom clipped to 50%) */
|
|
240
|
+
isLaneEnd: boolean;
|
|
241
|
+
continuationOnly?: boolean;
|
|
242
|
+
}) {
|
|
243
|
+
const dotSize = isRun ? 8 : 6;
|
|
244
|
+
const dotLeft = isRun ? 5 : 6;
|
|
245
|
+
const dotOpacity = hasSelection && !showBranch && !isRun ? 0.3 : 1;
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div
|
|
249
|
+
className="relative flex-shrink-0 self-stretch"
|
|
250
|
+
style={{
|
|
251
|
+
width: GUTTER_WIDTH,
|
|
252
|
+
minHeight: continuationOnly ? 0 : undefined,
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
{/* Root vertical line (leftmost, always visible) */}
|
|
256
|
+
<div
|
|
257
|
+
style={{
|
|
258
|
+
position: 'absolute',
|
|
259
|
+
left: 8,
|
|
260
|
+
top: continuationOnly ? 0 : isFirst ? '50%' : 0,
|
|
261
|
+
bottom: continuationOnly ? 0 : isLast ? '50%' : 0,
|
|
262
|
+
width: 2,
|
|
263
|
+
backgroundColor: ROOT_LINE_COLOR,
|
|
264
|
+
zIndex: 0,
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
267
|
+
|
|
268
|
+
{!continuationOnly && (
|
|
269
|
+
<>
|
|
270
|
+
{/* Status dot on the root line for every event */}
|
|
271
|
+
<div
|
|
272
|
+
style={{
|
|
273
|
+
position: 'absolute',
|
|
274
|
+
left: dotLeft,
|
|
275
|
+
top: '50%',
|
|
276
|
+
transform: 'translateY(-50%)',
|
|
277
|
+
width: dotSize,
|
|
278
|
+
height: dotSize,
|
|
279
|
+
zIndex: 2,
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
{/* Opaque backdrop ensures gutter lines never visually cut through dots */}
|
|
283
|
+
<div
|
|
284
|
+
style={{
|
|
285
|
+
position: 'absolute',
|
|
286
|
+
inset: 0,
|
|
287
|
+
borderRadius: '50%',
|
|
288
|
+
backgroundColor: 'var(--ds-background-100)',
|
|
289
|
+
zIndex: 0,
|
|
290
|
+
}}
|
|
291
|
+
/>
|
|
292
|
+
{pulse && (
|
|
293
|
+
<div
|
|
294
|
+
style={{
|
|
295
|
+
position: 'absolute',
|
|
296
|
+
inset: 0,
|
|
297
|
+
borderRadius: '50%',
|
|
298
|
+
backgroundColor: statusDotColor,
|
|
299
|
+
opacity: 0.75 * dotOpacity,
|
|
300
|
+
animation: DOT_PULSE_ANIMATION,
|
|
301
|
+
zIndex: 1,
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
)}
|
|
305
|
+
<div
|
|
306
|
+
style={{
|
|
307
|
+
position: 'relative',
|
|
308
|
+
width: '100%',
|
|
309
|
+
height: '100%',
|
|
310
|
+
borderRadius: '50%',
|
|
311
|
+
backgroundColor: statusDotColor,
|
|
312
|
+
opacity: dotOpacity,
|
|
313
|
+
transition: 'opacity 150ms',
|
|
314
|
+
zIndex: 2,
|
|
315
|
+
}}
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{/* Horizontal branch from root to gutter edge (selected group events only) */}
|
|
320
|
+
{showBranch && (
|
|
321
|
+
<div
|
|
322
|
+
style={{
|
|
323
|
+
position: 'absolute',
|
|
324
|
+
left: 9,
|
|
325
|
+
top: '50%',
|
|
326
|
+
width: GUTTER_WIDTH - 9,
|
|
327
|
+
height: 2,
|
|
328
|
+
backgroundColor: ROOT_LINE_COLOR,
|
|
329
|
+
zIndex: 0,
|
|
330
|
+
}}
|
|
331
|
+
/>
|
|
332
|
+
)}
|
|
333
|
+
</>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
{/* Vertical lane line connecting the selected group's events */}
|
|
337
|
+
{showLaneLine && (
|
|
338
|
+
<div
|
|
339
|
+
style={{
|
|
340
|
+
position: 'absolute',
|
|
341
|
+
left: LANE_X,
|
|
342
|
+
top: continuationOnly ? 0 : isLaneStart ? '50%' : 0,
|
|
343
|
+
bottom: continuationOnly ? 0 : isLaneEnd ? '50%' : 0,
|
|
344
|
+
width: 2,
|
|
345
|
+
backgroundColor: ROOT_LINE_COLOR,
|
|
346
|
+
zIndex: 0,
|
|
347
|
+
}}
|
|
348
|
+
/>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
355
|
+
// Copyable cell — shows a copy button on hover
|
|
356
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
function CopyableCell({
|
|
359
|
+
value,
|
|
360
|
+
className,
|
|
361
|
+
}: {
|
|
362
|
+
value: string;
|
|
363
|
+
className?: string;
|
|
364
|
+
}): ReactNode {
|
|
365
|
+
const [copied, setCopied] = useState(false);
|
|
366
|
+
const resetCopiedTimeoutRef = useRef<number | null>(null);
|
|
367
|
+
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
return () => {
|
|
370
|
+
if (resetCopiedTimeoutRef.current !== null) {
|
|
371
|
+
window.clearTimeout(resetCopiedTimeoutRef.current);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
}, []);
|
|
375
|
+
|
|
376
|
+
const handleCopy = useCallback(
|
|
377
|
+
(e: ReactMouseEvent) => {
|
|
378
|
+
e.stopPropagation();
|
|
379
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
380
|
+
setCopied(true);
|
|
381
|
+
if (resetCopiedTimeoutRef.current !== null) {
|
|
382
|
+
window.clearTimeout(resetCopiedTimeoutRef.current);
|
|
383
|
+
}
|
|
384
|
+
resetCopiedTimeoutRef.current = window.setTimeout(() => {
|
|
385
|
+
setCopied(false);
|
|
386
|
+
resetCopiedTimeoutRef.current = null;
|
|
387
|
+
}, 1500);
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
[value]
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<div
|
|
395
|
+
className={`group/copy flex items-center gap-1 flex-1 min-w-0 px-4 ${className ?? ''}`}
|
|
396
|
+
>
|
|
397
|
+
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
|
|
398
|
+
{value || '-'}
|
|
399
|
+
</span>
|
|
400
|
+
{value ? (
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
onClick={handleCopy}
|
|
404
|
+
className="flex-shrink-0 opacity-0 group-hover/copy:opacity-100 transition-opacity p-0.5 rounded hover:bg-[var(--ds-gray-alpha-200)]"
|
|
405
|
+
style={BUTTON_RESET_STYLE}
|
|
406
|
+
aria-label={`Copy ${value}`}
|
|
407
|
+
>
|
|
408
|
+
{copied ? (
|
|
409
|
+
<Check
|
|
410
|
+
className="h-3 w-3"
|
|
411
|
+
style={{ color: 'var(--ds-green-700)' }}
|
|
412
|
+
/>
|
|
413
|
+
) : (
|
|
414
|
+
<Copy className="h-3 w-3" style={{ color: 'var(--ds-gray-700)' }} />
|
|
415
|
+
)}
|
|
416
|
+
</button>
|
|
417
|
+
) : null}
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Recursively parse stringified JSON values so escaped slashes / quotes are cleaned up */
|
|
423
|
+
function deepParseJson(value: unknown): unknown {
|
|
424
|
+
if (typeof value === 'string') {
|
|
425
|
+
const trimmed = value.trim();
|
|
426
|
+
if (
|
|
427
|
+
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
428
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']')) ||
|
|
429
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
430
|
+
) {
|
|
431
|
+
try {
|
|
432
|
+
return deepParseJson(JSON.parse(trimmed));
|
|
433
|
+
} catch {
|
|
434
|
+
return value;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
if (Array.isArray(value)) {
|
|
440
|
+
return value.map(deepParseJson);
|
|
441
|
+
}
|
|
442
|
+
if (value !== null && typeof value === 'object') {
|
|
443
|
+
const result: Record<string, unknown> = {};
|
|
444
|
+
for (const [k, v] of Object.entries(value)) {
|
|
445
|
+
result[k] = deepParseJson(v);
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function PayloadBlock({ data }: { data: unknown }): ReactNode {
|
|
453
|
+
const [copied, setCopied] = useState(false);
|
|
454
|
+
const resetCopiedTimeoutRef = useRef<number | null>(null);
|
|
455
|
+
const cleaned = useMemo(() => deepParseJson(data), [data]);
|
|
456
|
+
|
|
457
|
+
useEffect(() => {
|
|
458
|
+
return () => {
|
|
459
|
+
if (resetCopiedTimeoutRef.current !== null) {
|
|
460
|
+
window.clearTimeout(resetCopiedTimeoutRef.current);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}, []);
|
|
464
|
+
|
|
465
|
+
const formatted = useMemo(() => {
|
|
466
|
+
try {
|
|
467
|
+
return JSON.stringify(cleaned, null, 2);
|
|
468
|
+
} catch {
|
|
469
|
+
return String(cleaned);
|
|
470
|
+
}
|
|
471
|
+
}, [cleaned]);
|
|
472
|
+
|
|
473
|
+
const handleCopy = useCallback(
|
|
474
|
+
(e: ReactMouseEvent) => {
|
|
475
|
+
e.stopPropagation();
|
|
476
|
+
navigator.clipboard.writeText(formatted).then(() => {
|
|
477
|
+
setCopied(true);
|
|
478
|
+
if (resetCopiedTimeoutRef.current !== null) {
|
|
479
|
+
window.clearTimeout(resetCopiedTimeoutRef.current);
|
|
480
|
+
}
|
|
481
|
+
resetCopiedTimeoutRef.current = window.setTimeout(() => {
|
|
482
|
+
setCopied(false);
|
|
483
|
+
resetCopiedTimeoutRef.current = null;
|
|
484
|
+
}, 1500);
|
|
485
|
+
});
|
|
486
|
+
},
|
|
487
|
+
[formatted]
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<div className="relative group/payload">
|
|
492
|
+
<div
|
|
493
|
+
className="overflow-x-auto p-2 text-[11px]"
|
|
494
|
+
style={{ color: 'var(--ds-gray-1000)' }}
|
|
495
|
+
>
|
|
496
|
+
<DataInspector data={cleaned} expandLevel={2} />
|
|
497
|
+
</div>
|
|
498
|
+
<button
|
|
499
|
+
type="button"
|
|
500
|
+
onClick={handleCopy}
|
|
501
|
+
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)]"
|
|
502
|
+
style={{ ...BUTTON_RESET_STYLE, color: 'var(--ds-gray-700)' }}
|
|
503
|
+
aria-label="Copy payload"
|
|
504
|
+
>
|
|
505
|
+
{copied ? (
|
|
506
|
+
<>
|
|
507
|
+
<Check
|
|
508
|
+
className="h-3 w-3"
|
|
509
|
+
style={{ color: 'var(--ds-green-700)' }}
|
|
510
|
+
/>
|
|
511
|
+
<span style={{ color: 'var(--ds-green-700)' }}>Copied</span>
|
|
512
|
+
</>
|
|
513
|
+
) : (
|
|
514
|
+
<>
|
|
515
|
+
<Copy className="h-3 w-3" />
|
|
516
|
+
<span>Copy</span>
|
|
517
|
+
</>
|
|
518
|
+
)}
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
);
|
|
50
522
|
}
|
|
51
523
|
|
|
524
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
525
|
+
// Event row
|
|
526
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
527
|
+
|
|
52
528
|
interface EventsListProps {
|
|
53
529
|
events: Event[] | null;
|
|
530
|
+
steps?: Step[] | null;
|
|
531
|
+
run?: WorkflowRun | null;
|
|
54
532
|
onLoadEventData?: (event: Event) => Promise<unknown | null>;
|
|
533
|
+
hasMoreEvents?: boolean;
|
|
534
|
+
isLoadingMoreEvents?: boolean;
|
|
535
|
+
onLoadMoreEvents?: () => Promise<void> | void;
|
|
55
536
|
}
|
|
56
537
|
|
|
57
|
-
/**
|
|
58
|
-
* Single event row component with expandable details
|
|
59
|
-
*/
|
|
60
538
|
function EventRow({
|
|
61
539
|
event,
|
|
540
|
+
index,
|
|
541
|
+
isFirst,
|
|
542
|
+
isLast,
|
|
543
|
+
activeGroupKey,
|
|
544
|
+
selectedGroupKey,
|
|
545
|
+
selectedGroupRange,
|
|
546
|
+
correlationNameMap,
|
|
547
|
+
workflowName,
|
|
548
|
+
durationMap,
|
|
549
|
+
onSelectGroup,
|
|
550
|
+
onHoverGroup,
|
|
62
551
|
onLoadEventData,
|
|
63
552
|
}: {
|
|
64
553
|
event: Event;
|
|
554
|
+
index: number;
|
|
555
|
+
isFirst: boolean;
|
|
556
|
+
isLast: boolean;
|
|
557
|
+
activeGroupKey?: string;
|
|
558
|
+
selectedGroupKey?: string;
|
|
559
|
+
selectedGroupRange: { first: number; last: number } | null;
|
|
560
|
+
correlationNameMap: Map<string, string>;
|
|
561
|
+
workflowName: string | null;
|
|
562
|
+
durationMap: Map<string, DurationInfo>;
|
|
563
|
+
onSelectGroup: (groupKey: string | undefined) => void;
|
|
564
|
+
onHoverGroup: (groupKey: string | undefined) => void;
|
|
65
565
|
onLoadEventData?: (event: Event) => Promise<unknown | null>;
|
|
66
566
|
}) {
|
|
67
|
-
const isDark = useDarkMode();
|
|
68
567
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
69
568
|
const [isLoading, setIsLoading] = useState(false);
|
|
70
569
|
const [loadedEventData, setLoadedEventData] = useState<unknown | null>(null);
|
|
71
570
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
571
|
+
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(false);
|
|
72
572
|
|
|
73
|
-
const
|
|
74
|
-
|
|
573
|
+
const rowGroupKey =
|
|
574
|
+
event.correlationId ??
|
|
575
|
+
(isRunLevel(event.eventType) ? '__run__' : undefined);
|
|
576
|
+
|
|
577
|
+
// Collapse when a different group gets selected
|
|
578
|
+
useEffect(() => {
|
|
579
|
+
if (selectedGroupKey !== undefined && selectedGroupKey !== rowGroupKey) {
|
|
580
|
+
setIsExpanded(false);
|
|
581
|
+
}
|
|
582
|
+
}, [selectedGroupKey, rowGroupKey]);
|
|
75
583
|
|
|
76
|
-
|
|
584
|
+
const statusDotColor = getStatusDotColor(event.eventType);
|
|
585
|
+
const createdAt = new Date(event.createdAt);
|
|
77
586
|
const hasExistingEventData = 'eventData' in event && event.eventData != null;
|
|
587
|
+
const isRun = isRunLevel(event.eventType);
|
|
588
|
+
const eventName = isRun
|
|
589
|
+
? (workflowName ?? '-')
|
|
590
|
+
: event.correlationId
|
|
591
|
+
? (correlationNameMap.get(event.correlationId) ?? '-')
|
|
592
|
+
: '-';
|
|
593
|
+
|
|
594
|
+
const durationKey = event.correlationId ?? (isRun ? '__run__' : '');
|
|
595
|
+
const durationInfo = durationKey ? durationMap.get(durationKey) : undefined;
|
|
596
|
+
|
|
597
|
+
const hasActive = activeGroupKey !== undefined;
|
|
598
|
+
const isRelated = rowGroupKey !== undefined && rowGroupKey === activeGroupKey;
|
|
599
|
+
const isDimmed = hasActive && !isRelated;
|
|
600
|
+
const isPulsing = hasActive && isRelated;
|
|
601
|
+
|
|
602
|
+
// Gutter state derived from selectedGroupRange
|
|
603
|
+
const showBranch = hasActive && isRelated && !isRun;
|
|
604
|
+
const showLaneLine =
|
|
605
|
+
selectedGroupRange !== null &&
|
|
606
|
+
index >= selectedGroupRange.first &&
|
|
607
|
+
index <= selectedGroupRange.last;
|
|
608
|
+
const isLaneStart =
|
|
609
|
+
selectedGroupRange !== null && index === selectedGroupRange.first;
|
|
610
|
+
const isLaneEnd =
|
|
611
|
+
selectedGroupRange !== null && index === selectedGroupRange.last;
|
|
78
612
|
|
|
79
|
-
// Load full event details when expanding
|
|
80
613
|
const loadEventDetails = useCallback(async () => {
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
loadedEventData !== null ||
|
|
84
|
-
hasExistingEventData ||
|
|
85
|
-
!event.correlationId
|
|
86
|
-
) {
|
|
614
|
+
if (loadedEventData !== null || hasExistingEventData) {
|
|
87
615
|
return;
|
|
88
616
|
}
|
|
89
|
-
|
|
90
617
|
setIsLoading(true);
|
|
91
618
|
setLoadError(null);
|
|
92
|
-
|
|
93
619
|
try {
|
|
94
620
|
if (!onLoadEventData) {
|
|
95
621
|
setLoadError('Event details unavailable');
|
|
@@ -105,163 +631,215 @@ function EventRow({
|
|
|
105
631
|
);
|
|
106
632
|
} finally {
|
|
107
633
|
setIsLoading(false);
|
|
634
|
+
setHasAttemptedLoad(true);
|
|
108
635
|
}
|
|
109
|
-
}, [
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
636
|
+
}, [event, loadedEventData, hasExistingEventData, onLoadEventData]);
|
|
637
|
+
|
|
638
|
+
const handleExpandToggle = useCallback(
|
|
639
|
+
(e: ReactMouseEvent) => {
|
|
640
|
+
e.stopPropagation();
|
|
641
|
+
const newExpanded = !isExpanded;
|
|
642
|
+
setIsExpanded(newExpanded);
|
|
643
|
+
if (newExpanded && loadedEventData === null && !hasExistingEventData) {
|
|
644
|
+
loadEventDetails();
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
[isExpanded, loadedEventData, hasExistingEventData, loadEventDetails]
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const handleRowClick = useCallback(() => {
|
|
651
|
+
if (selectedGroupKey === rowGroupKey) {
|
|
652
|
+
onSelectGroup(undefined);
|
|
653
|
+
} else {
|
|
654
|
+
onSelectGroup(rowGroupKey);
|
|
124
655
|
}
|
|
125
|
-
}, [
|
|
656
|
+
}, [selectedGroupKey, rowGroupKey, onSelectGroup]);
|
|
126
657
|
|
|
127
|
-
// Get the event data to display (either from initial fetch, loaded data, or null)
|
|
128
658
|
const eventData = hasExistingEventData
|
|
129
659
|
? (event as Event & { eventData: unknown }).eventData
|
|
130
660
|
: loadedEventData;
|
|
131
661
|
|
|
662
|
+
const contentOpacity = isDimmed ? 0.3 : 1;
|
|
663
|
+
|
|
132
664
|
return (
|
|
133
665
|
<div
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
borderColor: colors.border,
|
|
138
|
-
borderLeftWidth: '1px',
|
|
139
|
-
borderLeftColor: colors.color,
|
|
140
|
-
}}
|
|
666
|
+
data-event-id={event.eventId}
|
|
667
|
+
onMouseEnter={() => onHoverGroup(rowGroupKey)}
|
|
668
|
+
onMouseLeave={() => onHoverGroup(undefined)}
|
|
141
669
|
>
|
|
142
|
-
{/*
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
gridTemplateColumns: '24px 100px minmax(120px, auto) 1fr 1fr',
|
|
670
|
+
{/* Row */}
|
|
671
|
+
<div
|
|
672
|
+
role="button"
|
|
673
|
+
tabIndex={0}
|
|
674
|
+
onClick={handleRowClick}
|
|
675
|
+
onKeyDown={(e) => {
|
|
676
|
+
if (e.key === 'Enter' || e.key === ' ') handleRowClick();
|
|
150
677
|
}}
|
|
678
|
+
className="w-full text-left flex items-center gap-0 text-sm hover:bg-[var(--ds-gray-alpha-100)] transition-colors cursor-pointer"
|
|
679
|
+
style={{ minHeight: 40 }}
|
|
151
680
|
>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
681
|
+
<TreeGutter
|
|
682
|
+
isFirst={isFirst}
|
|
683
|
+
isLast={isLast && !isExpanded}
|
|
684
|
+
isRunLevel={isRun}
|
|
685
|
+
statusDotColor={statusDotColor}
|
|
686
|
+
pulse={isPulsing}
|
|
687
|
+
hasSelection={hasActive}
|
|
688
|
+
showBranch={showBranch}
|
|
689
|
+
showLaneLine={showLaneLine}
|
|
690
|
+
isLaneStart={isLaneStart}
|
|
691
|
+
isLaneEnd={isLaneEnd}
|
|
692
|
+
/>
|
|
162
693
|
|
|
163
|
-
{/*
|
|
694
|
+
{/* Content area — dims when unrelated */}
|
|
164
695
|
<div
|
|
165
|
-
className="
|
|
166
|
-
style={{
|
|
696
|
+
className="flex items-center flex-1 min-w-0"
|
|
697
|
+
style={{ opacity: contentOpacity, transition: 'opacity 150ms' }}
|
|
167
698
|
>
|
|
168
|
-
{
|
|
169
|
-
|
|
699
|
+
{/* Expand chevron button */}
|
|
700
|
+
<button
|
|
701
|
+
type="button"
|
|
702
|
+
onClick={handleExpandToggle}
|
|
703
|
+
className="flex items-center justify-center w-5 h-5 flex-shrink-0 rounded hover:bg-[var(--ds-gray-alpha-200)] transition-colors"
|
|
704
|
+
style={{
|
|
705
|
+
...BUTTON_RESET_STYLE,
|
|
706
|
+
border: '1px solid var(--ds-gray-alpha-400)',
|
|
707
|
+
}}
|
|
708
|
+
aria-label={isExpanded ? 'Collapse details' : 'Expand details'}
|
|
709
|
+
>
|
|
710
|
+
<ChevronRight
|
|
711
|
+
className="h-3 w-3 transition-transform"
|
|
712
|
+
style={{
|
|
713
|
+
color: 'var(--ds-gray-700)',
|
|
714
|
+
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
715
|
+
}}
|
|
716
|
+
/>
|
|
717
|
+
</button>
|
|
718
|
+
|
|
719
|
+
{/* Time */}
|
|
720
|
+
<div
|
|
721
|
+
className="text-xs tabular-nums flex-1 min-w-0 px-4"
|
|
722
|
+
style={{ color: 'var(--ds-gray-900)' }}
|
|
723
|
+
>
|
|
724
|
+
{formatEventTime(createdAt)}
|
|
725
|
+
</div>
|
|
170
726
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<span className="inline-flex items-center gap-1.5">
|
|
727
|
+
{/* Event Type */}
|
|
728
|
+
<div className="text-xs font-medium flex-1 min-w-0 px-4">
|
|
174
729
|
<span
|
|
175
|
-
className="
|
|
176
|
-
style={{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
730
|
+
className="inline-flex items-center gap-1.5"
|
|
731
|
+
style={{ color: 'var(--ds-gray-900)' }}
|
|
732
|
+
>
|
|
733
|
+
<span
|
|
734
|
+
style={{
|
|
735
|
+
position: 'relative',
|
|
736
|
+
display: 'inline-flex',
|
|
737
|
+
width: 6,
|
|
738
|
+
height: 6,
|
|
739
|
+
flexShrink: 0,
|
|
740
|
+
}}
|
|
741
|
+
>
|
|
742
|
+
{isPulsing && (
|
|
743
|
+
<span
|
|
744
|
+
style={{
|
|
745
|
+
position: 'absolute',
|
|
746
|
+
inset: 0,
|
|
747
|
+
borderRadius: '50%',
|
|
748
|
+
backgroundColor: statusDotColor,
|
|
749
|
+
opacity: 0.75,
|
|
750
|
+
animation: DOT_PULSE_ANIMATION,
|
|
751
|
+
}}
|
|
752
|
+
/>
|
|
753
|
+
)}
|
|
754
|
+
<span
|
|
755
|
+
style={{
|
|
756
|
+
position: 'relative',
|
|
757
|
+
width: 6,
|
|
758
|
+
height: 6,
|
|
759
|
+
borderRadius: '50%',
|
|
760
|
+
backgroundColor: statusDotColor,
|
|
761
|
+
}}
|
|
762
|
+
/>
|
|
763
|
+
</span>
|
|
764
|
+
{formatEventType(event.eventType)}
|
|
765
|
+
</span>
|
|
766
|
+
</div>
|
|
181
767
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
</div>
|
|
768
|
+
{/* Name */}
|
|
769
|
+
<div
|
|
770
|
+
className="text-xs flex-1 min-w-0 px-4 overflow-hidden text-ellipsis whitespace-nowrap"
|
|
771
|
+
title={eventName !== '-' ? eventName : undefined}
|
|
772
|
+
>
|
|
773
|
+
{eventName}
|
|
774
|
+
</div>
|
|
190
775
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
{
|
|
776
|
+
{/* Correlation ID */}
|
|
777
|
+
<CopyableCell
|
|
778
|
+
value={event.correlationId || ''}
|
|
779
|
+
className="font-mono text-xs"
|
|
780
|
+
/>
|
|
781
|
+
|
|
782
|
+
{/* Event ID */}
|
|
783
|
+
<CopyableCell value={event.eventId} className="font-mono text-xs" />
|
|
198
784
|
</div>
|
|
199
|
-
</
|
|
785
|
+
</div>
|
|
200
786
|
|
|
201
|
-
{/* Expanded details */}
|
|
787
|
+
{/* Expanded details — tree lines continue through this area */}
|
|
202
788
|
{isExpanded && (
|
|
203
|
-
<div
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
789
|
+
<div className="flex">
|
|
790
|
+
{/* Continuation gutter — lane line continues if not at lane end */}
|
|
791
|
+
<TreeGutter
|
|
792
|
+
isFirst={false}
|
|
793
|
+
isLast={isLast}
|
|
794
|
+
isRunLevel={isRun}
|
|
795
|
+
hasSelection={hasActive}
|
|
796
|
+
showBranch={false}
|
|
797
|
+
showLaneLine={showLaneLine && !isLaneEnd}
|
|
798
|
+
isLaneStart={false}
|
|
799
|
+
isLaneEnd={false}
|
|
800
|
+
continuationOnly
|
|
801
|
+
/>
|
|
802
|
+
{/* Spacer for chevron column */}
|
|
803
|
+
<div className="w-5 flex-shrink-0" />
|
|
211
804
|
<div
|
|
212
|
-
className="flex
|
|
805
|
+
className="flex-1 my-1.5 mr-3 ml-2 py-2 rounded-md border overflow-hidden"
|
|
213
806
|
style={{
|
|
214
|
-
borderColor: 'var(--ds-gray-
|
|
215
|
-
|
|
807
|
+
borderColor: 'var(--ds-gray-alpha-200)',
|
|
808
|
+
opacity: contentOpacity,
|
|
809
|
+
transition: 'opacity 150ms',
|
|
216
810
|
}}
|
|
217
811
|
>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
label="Correlation ID"
|
|
222
|
-
value={event.correlationId || '-'}
|
|
223
|
-
mono
|
|
224
|
-
/>
|
|
225
|
-
<AttributeRow label="Run ID" value={event.runId} mono />
|
|
226
|
-
<AttributeRow
|
|
227
|
-
label="Created At"
|
|
228
|
-
value={formatEventDateTime(createdAt)}
|
|
229
|
-
/>
|
|
230
|
-
</div>
|
|
231
|
-
|
|
232
|
-
{/* Event data section */}
|
|
233
|
-
<div className="mt-3">
|
|
234
|
-
<div
|
|
235
|
-
className="text-xs font-medium mb-1.5"
|
|
236
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
237
|
-
>
|
|
238
|
-
Event Data
|
|
239
|
-
</div>
|
|
240
|
-
|
|
241
|
-
{/* Loading state */}
|
|
242
|
-
{isLoading && (
|
|
812
|
+
{/* Duration info */}
|
|
813
|
+
{(durationInfo?.queued !== undefined ||
|
|
814
|
+
durationInfo?.ran !== undefined) && (
|
|
243
815
|
<div
|
|
244
|
-
className="
|
|
245
|
-
style={{
|
|
246
|
-
borderColor: 'var(--ds-gray-300)',
|
|
247
|
-
backgroundColor: 'var(--ds-gray-100)',
|
|
248
|
-
}}
|
|
816
|
+
className="px-2 pb-1.5 text-xs flex gap-3"
|
|
817
|
+
style={{ color: 'var(--ds-gray-900)' }}
|
|
249
818
|
>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
819
|
+
{durationInfo.queued !== undefined &&
|
|
820
|
+
durationInfo.queued > 0 && (
|
|
821
|
+
<span>
|
|
822
|
+
Queued for{' '}
|
|
823
|
+
<span className="font-mono tabular-nums">
|
|
824
|
+
{formatDuration(durationInfo.queued)}
|
|
825
|
+
</span>
|
|
826
|
+
</span>
|
|
827
|
+
)}
|
|
828
|
+
{durationInfo.ran !== undefined && (
|
|
829
|
+
<span>
|
|
830
|
+
Ran for{' '}
|
|
831
|
+
<span className="font-mono tabular-nums">
|
|
832
|
+
{formatDuration(durationInfo.ran)}
|
|
833
|
+
</span>
|
|
834
|
+
</span>
|
|
835
|
+
)}
|
|
260
836
|
</div>
|
|
261
837
|
)}
|
|
262
838
|
|
|
263
|
-
{/*
|
|
264
|
-
{
|
|
839
|
+
{/* Payload */}
|
|
840
|
+
{eventData != null ? (
|
|
841
|
+
<PayloadBlock data={eventData} />
|
|
842
|
+
) : loadError ? (
|
|
265
843
|
<div
|
|
266
844
|
className="rounded-md border p-3 text-xs"
|
|
267
845
|
style={{
|
|
@@ -272,59 +850,23 @@ function EventRow({
|
|
|
272
850
|
>
|
|
273
851
|
{loadError}
|
|
274
852
|
</div>
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
853
|
+
) : isLoading ||
|
|
854
|
+
(!hasExistingEventData &&
|
|
855
|
+
!hasAttemptedLoad &&
|
|
856
|
+
event.correlationId) ? (
|
|
857
|
+
<div className="flex flex-col gap-2 p-3">
|
|
858
|
+
<Skeleton className="h-3" style={{ width: '75%' }} />
|
|
859
|
+
<Skeleton className="h-3" style={{ width: '50%' }} />
|
|
860
|
+
<Skeleton className="h-3" style={{ width: '60%' }} />
|
|
861
|
+
</div>
|
|
862
|
+
) : (
|
|
279
863
|
<div
|
|
280
|
-
className="
|
|
281
|
-
style={{
|
|
864
|
+
className="p-2 text-xs"
|
|
865
|
+
style={{ color: 'var(--ds-gray-900)' }}
|
|
282
866
|
>
|
|
283
|
-
|
|
284
|
-
data={eventData}
|
|
285
|
-
// @ts-expect-error react-inspector accepts theme objects at runtime
|
|
286
|
-
// see https://github.com/storybookjs/react-inspector/blob/main/README.md#theme
|
|
287
|
-
theme={isDark ? inspectorThemeDark : inspectorThemeLight}
|
|
288
|
-
expandLevel={2}
|
|
289
|
-
/>
|
|
867
|
+
No data
|
|
290
868
|
</div>
|
|
291
869
|
)}
|
|
292
|
-
|
|
293
|
-
{/* No event data */}
|
|
294
|
-
{!isLoading &&
|
|
295
|
-
!loadError &&
|
|
296
|
-
eventData == null &&
|
|
297
|
-
!event.correlationId && (
|
|
298
|
-
<div
|
|
299
|
-
className="rounded-md border p-3 text-xs"
|
|
300
|
-
style={{
|
|
301
|
-
borderColor: 'var(--ds-gray-300)',
|
|
302
|
-
backgroundColor: 'var(--ds-gray-100)',
|
|
303
|
-
color: 'var(--ds-gray-700)',
|
|
304
|
-
}}
|
|
305
|
-
>
|
|
306
|
-
No event data available
|
|
307
|
-
</div>
|
|
308
|
-
)}
|
|
309
|
-
|
|
310
|
-
{/* No correlation ID - can't load data */}
|
|
311
|
-
{!isLoading &&
|
|
312
|
-
!loadError &&
|
|
313
|
-
eventData == null &&
|
|
314
|
-
event.correlationId &&
|
|
315
|
-
!hasExistingEventData &&
|
|
316
|
-
loadedEventData === null && (
|
|
317
|
-
<div
|
|
318
|
-
className="rounded-md border p-3 text-xs"
|
|
319
|
-
style={{
|
|
320
|
-
borderColor: 'var(--ds-gray-300)',
|
|
321
|
-
backgroundColor: 'var(--ds-gray-100)',
|
|
322
|
-
color: 'var(--ds-gray-700)',
|
|
323
|
-
}}
|
|
324
|
-
>
|
|
325
|
-
No event data for this event type
|
|
326
|
-
</div>
|
|
327
|
-
)}
|
|
328
870
|
</div>
|
|
329
871
|
</div>
|
|
330
872
|
)}
|
|
@@ -332,45 +874,19 @@ function EventRow({
|
|
|
332
874
|
);
|
|
333
875
|
}
|
|
334
876
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
function AttributeRow({
|
|
339
|
-
label,
|
|
340
|
-
value,
|
|
341
|
-
mono = false,
|
|
342
|
-
}: {
|
|
343
|
-
label: string;
|
|
344
|
-
value: string;
|
|
345
|
-
mono?: boolean;
|
|
346
|
-
}) {
|
|
347
|
-
return (
|
|
348
|
-
<div
|
|
349
|
-
className="flex items-center justify-between px-2.5 py-1.5"
|
|
350
|
-
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
351
|
-
>
|
|
352
|
-
<span
|
|
353
|
-
className="text-[11px] font-medium"
|
|
354
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
355
|
-
>
|
|
356
|
-
{label}
|
|
357
|
-
</span>
|
|
358
|
-
<span
|
|
359
|
-
className={`text-[11px] ${mono ? 'font-mono' : ''} text-right max-w-[70%] break-all`}
|
|
360
|
-
style={{ color: 'var(--ds-gray-1000)' }}
|
|
361
|
-
>
|
|
362
|
-
{value}
|
|
363
|
-
</span>
|
|
364
|
-
</div>
|
|
365
|
-
);
|
|
366
|
-
}
|
|
877
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
878
|
+
// Main component
|
|
879
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
367
880
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
881
|
+
export function EventListView({
|
|
882
|
+
events,
|
|
883
|
+
steps,
|
|
884
|
+
run,
|
|
885
|
+
onLoadEventData,
|
|
886
|
+
hasMoreEvents = false,
|
|
887
|
+
isLoadingMoreEvents = false,
|
|
888
|
+
onLoadMoreEvents,
|
|
889
|
+
}: EventsListProps) {
|
|
374
890
|
const sortedEvents = useMemo(() => {
|
|
375
891
|
if (!events || events.length === 0) return [];
|
|
376
892
|
return [...events].sort(
|
|
@@ -379,6 +895,87 @@ export function EventListView({ events, onLoadEventData }: EventsListProps) {
|
|
|
379
895
|
);
|
|
380
896
|
}, [events]);
|
|
381
897
|
|
|
898
|
+
const { correlationNameMap, workflowName } = useMemo(
|
|
899
|
+
() => buildNameMaps(steps ?? null, run ?? null),
|
|
900
|
+
[steps, run]
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
const durationMap = useMemo(
|
|
904
|
+
() => buildDurationMap(sortedEvents),
|
|
905
|
+
[sortedEvents]
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
const [selectedGroupKey, setSelectedGroupKey] = useState<string | undefined>(
|
|
909
|
+
undefined
|
|
910
|
+
);
|
|
911
|
+
const [hoveredGroupKey, setHoveredGroupKey] = useState<string | undefined>(
|
|
912
|
+
undefined
|
|
913
|
+
);
|
|
914
|
+
const onSelectGroup = useCallback((groupKey: string | undefined) => {
|
|
915
|
+
setSelectedGroupKey(groupKey);
|
|
916
|
+
}, []);
|
|
917
|
+
const onHoverGroup = useCallback((groupKey: string | undefined) => {
|
|
918
|
+
setHoveredGroupKey(groupKey);
|
|
919
|
+
}, []);
|
|
920
|
+
|
|
921
|
+
const activeGroupKey = selectedGroupKey ?? hoveredGroupKey;
|
|
922
|
+
|
|
923
|
+
// Compute the row-index range for the active group's connecting lane line.
|
|
924
|
+
// Only applies to non-run groups (step/hook/wait correlations).
|
|
925
|
+
const selectedGroupRange = useMemo(() => {
|
|
926
|
+
if (!activeGroupKey || activeGroupKey === '__run__') return null;
|
|
927
|
+
let first = -1;
|
|
928
|
+
let last = -1;
|
|
929
|
+
for (let i = 0; i < sortedEvents.length; i++) {
|
|
930
|
+
if (sortedEvents[i].correlationId === activeGroupKey) {
|
|
931
|
+
if (first === -1) first = i;
|
|
932
|
+
last = i;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return first >= 0 ? { first, last } : null;
|
|
936
|
+
}, [activeGroupKey, sortedEvents]);
|
|
937
|
+
|
|
938
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
939
|
+
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
|
940
|
+
|
|
941
|
+
const searchIndex = useMemo(() => {
|
|
942
|
+
const entries: {
|
|
943
|
+
text: string;
|
|
944
|
+
groupKey?: string;
|
|
945
|
+
eventId: string;
|
|
946
|
+
index: number;
|
|
947
|
+
}[] = [];
|
|
948
|
+
for (let i = 0; i < sortedEvents.length; i++) {
|
|
949
|
+
const ev = sortedEvents[i];
|
|
950
|
+
entries.push({
|
|
951
|
+
text: [ev.eventId, ev.correlationId ?? ''].join(' ').toLowerCase(),
|
|
952
|
+
groupKey:
|
|
953
|
+
ev.correlationId ??
|
|
954
|
+
(isRunLevel(ev.eventType) ? '__run__' : undefined),
|
|
955
|
+
eventId: ev.eventId,
|
|
956
|
+
index: i,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
return entries;
|
|
960
|
+
}, [sortedEvents]);
|
|
961
|
+
|
|
962
|
+
useEffect(() => {
|
|
963
|
+
const q = searchQuery.trim().toLowerCase();
|
|
964
|
+
if (!q) {
|
|
965
|
+
setSelectedGroupKey(undefined);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const match = searchIndex.find((entry) => entry.text.includes(q));
|
|
969
|
+
if (match) {
|
|
970
|
+
setSelectedGroupKey(match.groupKey);
|
|
971
|
+
virtuosoRef.current?.scrollToIndex({
|
|
972
|
+
index: match.index,
|
|
973
|
+
align: 'center',
|
|
974
|
+
behavior: 'smooth',
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}, [searchQuery, searchIndex]);
|
|
978
|
+
|
|
382
979
|
if (!events || events.length === 0) {
|
|
383
980
|
return (
|
|
384
981
|
<div
|
|
@@ -391,45 +988,161 @@ export function EventListView({ events, onLoadEventData }: EventsListProps) {
|
|
|
391
988
|
}
|
|
392
989
|
|
|
393
990
|
return (
|
|
394
|
-
<div className="h-full
|
|
395
|
-
{
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
991
|
+
<div className="h-full flex flex-col overflow-hidden">
|
|
992
|
+
<style>{`@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}`}</style>
|
|
993
|
+
{/* Search bar */}
|
|
994
|
+
<div style={{ padding: 6, backgroundColor: 'var(--ds-background-100)' }}>
|
|
995
|
+
<label
|
|
996
|
+
style={{
|
|
997
|
+
display: 'flex',
|
|
998
|
+
alignItems: 'center',
|
|
999
|
+
justifyContent: 'center',
|
|
1000
|
+
borderRadius: 6,
|
|
1001
|
+
boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
|
|
1002
|
+
background: 'var(--ds-background-100)',
|
|
1003
|
+
height: 40,
|
|
1004
|
+
}}
|
|
1005
|
+
>
|
|
1006
|
+
<div
|
|
1007
|
+
style={{
|
|
1008
|
+
width: 40,
|
|
1009
|
+
height: 40,
|
|
1010
|
+
display: 'flex',
|
|
1011
|
+
alignItems: 'center',
|
|
1012
|
+
justifyContent: 'center',
|
|
1013
|
+
color: 'var(--ds-gray-800)',
|
|
1014
|
+
flexShrink: 0,
|
|
1015
|
+
}}
|
|
1016
|
+
>
|
|
1017
|
+
<svg
|
|
1018
|
+
width={16}
|
|
1019
|
+
height={16}
|
|
1020
|
+
viewBox="0 0 16 16"
|
|
1021
|
+
fill="none"
|
|
1022
|
+
aria-hidden="true"
|
|
1023
|
+
focusable="false"
|
|
1024
|
+
>
|
|
1025
|
+
<circle
|
|
1026
|
+
cx="7"
|
|
1027
|
+
cy="7"
|
|
1028
|
+
r="4.5"
|
|
1029
|
+
stroke="currentColor"
|
|
1030
|
+
strokeWidth="1.5"
|
|
1031
|
+
/>
|
|
1032
|
+
<path
|
|
1033
|
+
d="M11.5 11.5L14 14"
|
|
1034
|
+
stroke="currentColor"
|
|
1035
|
+
strokeWidth="1.5"
|
|
1036
|
+
strokeLinecap="round"
|
|
1037
|
+
/>
|
|
1038
|
+
</svg>
|
|
1039
|
+
</div>
|
|
1040
|
+
<input
|
|
1041
|
+
type="search"
|
|
1042
|
+
placeholder="Search by event ID or correlation ID…"
|
|
1043
|
+
value={searchQuery}
|
|
1044
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
1045
|
+
style={{
|
|
1046
|
+
marginLeft: -16,
|
|
1047
|
+
paddingInline: 12,
|
|
1048
|
+
fontFamily: 'inherit',
|
|
1049
|
+
fontSize: 14,
|
|
1050
|
+
background: 'transparent',
|
|
1051
|
+
border: 'none',
|
|
1052
|
+
outline: 'none',
|
|
1053
|
+
height: 40,
|
|
1054
|
+
width: '100%',
|
|
1055
|
+
}}
|
|
419
1056
|
/>
|
|
420
|
-
|
|
1057
|
+
</label>
|
|
421
1058
|
</div>
|
|
422
1059
|
|
|
423
|
-
{/*
|
|
1060
|
+
{/* Header */}
|
|
424
1061
|
<div
|
|
425
|
-
className="
|
|
1062
|
+
className="flex items-center gap-0 text-sm font-medium h-10 border-b flex-shrink-0"
|
|
426
1063
|
style={{
|
|
427
|
-
borderColor: 'var(--ds-gray-
|
|
428
|
-
color: 'var(--ds-gray-
|
|
1064
|
+
borderColor: 'var(--ds-gray-alpha-200)',
|
|
1065
|
+
color: 'var(--ds-gray-900)',
|
|
1066
|
+
backgroundColor: 'var(--ds-background-100)',
|
|
429
1067
|
}}
|
|
430
1068
|
>
|
|
431
|
-
|
|
1069
|
+
<div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
|
|
1070
|
+
<div className="w-5 flex-shrink-0" />
|
|
1071
|
+
<div className="flex-1 min-w-0 px-4">Time</div>
|
|
1072
|
+
<div className="flex-1 min-w-0 px-4">Event Type</div>
|
|
1073
|
+
<div className="flex-1 min-w-0 px-4">Name</div>
|
|
1074
|
+
<div className="flex-1 min-w-0 px-4">Correlation ID</div>
|
|
1075
|
+
<div className="flex-1 min-w-0 px-4">Event ID</div>
|
|
432
1076
|
</div>
|
|
1077
|
+
|
|
1078
|
+
{/* Virtualized event rows */}
|
|
1079
|
+
<Virtuoso
|
|
1080
|
+
ref={virtuosoRef}
|
|
1081
|
+
totalCount={sortedEvents.length}
|
|
1082
|
+
overscan={20}
|
|
1083
|
+
defaultItemHeight={40}
|
|
1084
|
+
endReached={() => {
|
|
1085
|
+
if (!hasMoreEvents || isLoadingMoreEvents) {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
void onLoadMoreEvents?.();
|
|
1089
|
+
}}
|
|
1090
|
+
itemContent={(index: number) => {
|
|
1091
|
+
return (
|
|
1092
|
+
<EventRow
|
|
1093
|
+
event={sortedEvents[index]}
|
|
1094
|
+
index={index}
|
|
1095
|
+
isFirst={index === 0}
|
|
1096
|
+
isLast={index === sortedEvents.length - 1}
|
|
1097
|
+
activeGroupKey={activeGroupKey}
|
|
1098
|
+
selectedGroupKey={selectedGroupKey}
|
|
1099
|
+
selectedGroupRange={selectedGroupRange}
|
|
1100
|
+
correlationNameMap={correlationNameMap}
|
|
1101
|
+
workflowName={workflowName}
|
|
1102
|
+
durationMap={durationMap}
|
|
1103
|
+
onSelectGroup={onSelectGroup}
|
|
1104
|
+
onHoverGroup={onHoverGroup}
|
|
1105
|
+
onLoadEventData={onLoadEventData}
|
|
1106
|
+
/>
|
|
1107
|
+
);
|
|
1108
|
+
}}
|
|
1109
|
+
components={{
|
|
1110
|
+
Footer: () => (
|
|
1111
|
+
<>
|
|
1112
|
+
{hasMoreEvents && (
|
|
1113
|
+
<div className="px-3 pt-3 flex justify-center">
|
|
1114
|
+
<button
|
|
1115
|
+
type="button"
|
|
1116
|
+
onClick={() => void onLoadMoreEvents?.()}
|
|
1117
|
+
disabled={isLoadingMoreEvents}
|
|
1118
|
+
className="h-8 px-3 text-xs rounded-md border transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
|
1119
|
+
style={{
|
|
1120
|
+
borderColor: 'var(--ds-gray-alpha-400)',
|
|
1121
|
+
color: 'var(--ds-gray-900)',
|
|
1122
|
+
backgroundColor: 'var(--ds-background-100)',
|
|
1123
|
+
}}
|
|
1124
|
+
>
|
|
1125
|
+
{isLoadingMoreEvents
|
|
1126
|
+
? 'Loading more events...'
|
|
1127
|
+
: 'Load more'}
|
|
1128
|
+
</button>
|
|
1129
|
+
</div>
|
|
1130
|
+
)}
|
|
1131
|
+
<div
|
|
1132
|
+
className="mt-4 pt-3 border-t text-xs px-3"
|
|
1133
|
+
style={{
|
|
1134
|
+
borderColor: 'var(--ds-gray-alpha-200)',
|
|
1135
|
+
color: 'var(--ds-gray-900)',
|
|
1136
|
+
}}
|
|
1137
|
+
>
|
|
1138
|
+
{sortedEvents.length} event
|
|
1139
|
+
{sortedEvents.length !== 1 ? 's' : ''} total
|
|
1140
|
+
</div>
|
|
1141
|
+
</>
|
|
1142
|
+
),
|
|
1143
|
+
}}
|
|
1144
|
+
style={{ flex: 1, minHeight: 0 }}
|
|
1145
|
+
/>
|
|
433
1146
|
</div>
|
|
434
1147
|
);
|
|
435
1148
|
}
|