@workflow/web-shared 5.0.0-beta.0 → 5.0.0-beta.10

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.
Files changed (275) hide show
  1. package/README.md +1 -1
  2. package/dist/components/error-boundary.js +1 -1
  3. package/dist/components/event-list-view.d.ts +18 -1
  4. package/dist/components/event-list-view.d.ts.map +1 -1
  5. package/dist/components/event-list-view.js +242 -142
  6. package/dist/components/hook-actions.js +1 -1
  7. package/dist/components/index.d.ts +4 -1
  8. package/dist/components/index.d.ts.map +1 -1
  9. package/dist/components/index.js +5 -2
  10. package/dist/components/new-trace-viewer/components/copy-button.d.ts +9 -0
  11. package/dist/components/new-trace-viewer/components/copy-button.d.ts.map +1 -0
  12. package/dist/components/new-trace-viewer/components/copy-button.js +27 -0
  13. package/dist/components/new-trace-viewer/components/event-list.d.ts +10 -0
  14. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -0
  15. package/dist/components/new-trace-viewer/components/event-list.js +39 -0
  16. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
  17. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
  18. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
  19. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
  20. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
  21. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
  22. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
  23. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
  24. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
  25. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
  26. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
  27. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
  28. package/dist/components/new-trace-viewer/components/split-pane.d.ts +14 -0
  29. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -0
  30. package/dist/components/new-trace-viewer/components/split-pane.js +99 -0
  31. package/dist/components/new-trace-viewer/components/timeline.d.ts +34 -0
  32. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -0
  33. package/dist/components/new-trace-viewer/components/timeline.js +223 -0
  34. package/dist/components/new-trace-viewer/context.d.ts +15 -0
  35. package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
  36. package/dist/components/new-trace-viewer/context.js +47 -0
  37. package/dist/components/new-trace-viewer/detail-panel.d.ts +10 -0
  38. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -0
  39. package/dist/components/new-trace-viewer/detail-panel.js +13 -0
  40. package/dist/components/new-trace-viewer/icons.d.ts +6 -0
  41. package/dist/components/new-trace-viewer/icons.d.ts.map +1 -0
  42. package/dist/components/new-trace-viewer/icons.js +15 -0
  43. package/dist/components/new-trace-viewer/search.d.ts +19 -0
  44. package/dist/components/new-trace-viewer/search.d.ts.map +1 -0
  45. package/dist/components/new-trace-viewer/search.js +134 -0
  46. package/dist/components/new-trace-viewer/trace-viewer.d.ts +8 -0
  47. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -0
  48. package/dist/components/new-trace-viewer/trace-viewer.js +387 -0
  49. package/dist/components/new-trace-viewer/types.d.ts +1 -0
  50. package/dist/components/new-trace-viewer/types.d.ts.map +1 -0
  51. package/dist/components/new-trace-viewer/types.js +2 -0
  52. package/dist/components/new-trace-viewer/util/constants.d.ts +1 -0
  53. package/dist/components/new-trace-viewer/util/constants.d.ts.map +1 -0
  54. package/dist/components/new-trace-viewer/util/constants.js +2 -0
  55. package/dist/components/new-trace-viewer/utils.d.ts +46 -0
  56. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
  57. package/dist/components/new-trace-viewer/utils.js +330 -0
  58. package/dist/components/run-trace-view.d.ts +2 -1
  59. package/dist/components/run-trace-view.d.ts.map +1 -1
  60. package/dist/components/run-trace-view.js +3 -3
  61. package/dist/components/sidebar/attribute-panel.d.ts +7 -1
  62. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  63. package/dist/components/sidebar/attribute-panel.js +131 -70
  64. package/dist/components/sidebar/conversation-view.js +1 -1
  65. package/dist/components/sidebar/copyable-data-block.d.ts +1 -0
  66. package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
  67. package/dist/components/sidebar/copyable-data-block.js +16 -15
  68. package/dist/components/sidebar/detail-card.d.ts +5 -6
  69. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  70. package/dist/components/sidebar/detail-card.js +32 -16
  71. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  72. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  73. package/dist/components/sidebar/entity-detail-panel.js +20 -37
  74. package/dist/components/sidebar/events-list.d.ts +1 -1
  75. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  76. package/dist/components/sidebar/events-list.js +46 -32
  77. package/dist/components/sidebar/resolve-hook-modal.js +1 -1
  78. package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
  79. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
  80. package/dist/components/sidebar/sidebar-data-context.js +19 -0
  81. package/dist/components/stream-viewer.d.ts +1 -1
  82. package/dist/components/stream-viewer.d.ts.map +1 -1
  83. package/dist/components/stream-viewer.js +3 -27
  84. package/dist/components/trace-viewer/components/map.js +1 -1
  85. package/dist/components/trace-viewer/components/markers.js +1 -1
  86. package/dist/components/trace-viewer/components/node.js +1 -1
  87. package/dist/components/trace-viewer/components/search-input.js +1 -1
  88. package/dist/components/trace-viewer/components/search.js +1 -1
  89. package/dist/components/trace-viewer/components/span-content.js +1 -1
  90. package/dist/components/trace-viewer/components/span-detail-panel.js +1 -1
  91. package/dist/components/trace-viewer/components/span-segments.js +1 -1
  92. package/dist/components/trace-viewer/components/span-strategies.js +1 -1
  93. package/dist/components/trace-viewer/components/ui.js +1 -1
  94. package/dist/components/trace-viewer/components/zoom-button.js +1 -1
  95. package/dist/components/trace-viewer/components/zoom-icons.js +1 -1
  96. package/dist/components/trace-viewer/context.js +1 -1
  97. package/dist/components/trace-viewer/index.js +1 -1
  98. package/dist/components/trace-viewer/trace-viewer.js +1 -1
  99. package/dist/components/trace-viewer/types.js +1 -1
  100. package/dist/components/trace-viewer/util/constants.js +1 -1
  101. package/dist/components/trace-viewer/util/scrollbar-width.js +1 -1
  102. package/dist/components/trace-viewer/util/timing.d.ts +1 -1
  103. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  104. package/dist/components/trace-viewer/util/timing.js +6 -9
  105. package/dist/components/trace-viewer/util/tree.js +1 -1
  106. package/dist/components/trace-viewer/util/use-immediate-style.js +1 -1
  107. package/dist/components/trace-viewer/util/use-streaming-spans.js +1 -1
  108. package/dist/components/trace-viewer/util/use-trackpad-zoom.js +1 -1
  109. package/dist/components/trace-viewer/worker.js +1 -1
  110. package/dist/components/trace-viewer-new.d.ts +10 -0
  111. package/dist/components/trace-viewer-new.d.ts.map +1 -0
  112. package/dist/components/trace-viewer-new.js +23 -0
  113. package/dist/components/ui/alert.js +1 -1
  114. package/dist/components/ui/button.d.ts +10 -0
  115. package/dist/components/ui/button.d.ts.map +1 -0
  116. package/dist/components/ui/button.js +46 -0
  117. package/dist/components/ui/card.js +1 -1
  118. package/dist/components/ui/data-inspector.d.ts +39 -1
  119. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  120. package/dist/components/ui/data-inspector.js +207 -22
  121. package/dist/components/ui/decrypt-button.js +1 -1
  122. package/dist/components/ui/error-card.js +1 -1
  123. package/dist/components/ui/error-stack-block.d.ts.map +1 -1
  124. package/dist/components/ui/error-stack-block.js +27 -19
  125. package/dist/components/ui/icon-button.d.ts +13 -0
  126. package/dist/components/ui/icon-button.d.ts.map +1 -0
  127. package/dist/components/ui/icon-button.js +29 -0
  128. package/dist/components/ui/inspector-theme.d.ts +9 -10
  129. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  130. package/dist/components/ui/inspector-theme.js +39 -72
  131. package/dist/components/ui/load-more-button.js +1 -1
  132. package/dist/components/ui/menu-dropdown.js +1 -1
  133. package/dist/components/ui/skeleton.js +1 -1
  134. package/dist/components/ui/spinner.js +1 -1
  135. package/dist/components/ui/timestamp-tooltip.js +1 -1
  136. package/dist/components/workflow-trace-view.d.ts +3 -1
  137. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  138. package/dist/components/workflow-trace-view.js +10 -5
  139. package/dist/components/workflow-traces/event-colors.js +1 -1
  140. package/dist/components/workflow-traces/trace-colors.js +1 -1
  141. package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
  142. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  143. package/dist/components/workflow-traces/trace-span-construction.js +19 -6
  144. package/dist/components/workflow-traces/trace-time-utils.js +1 -1
  145. package/dist/hooks/use-dark-mode.js +1 -1
  146. package/dist/hooks/use-reduced-motion.d.ts +10 -0
  147. package/dist/hooks/use-reduced-motion.d.ts.map +1 -0
  148. package/dist/hooks/use-reduced-motion.js +29 -0
  149. package/dist/index.d.ts +3 -1
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +3 -2
  152. package/dist/lib/event-analysis.js +1 -1
  153. package/dist/lib/event-materialization.js +1 -1
  154. package/dist/lib/exact-event-search-id.d.ts +25 -0
  155. package/dist/lib/exact-event-search-id.d.ts.map +1 -0
  156. package/dist/lib/exact-event-search-id.js +39 -0
  157. package/dist/lib/hydration.d.ts +15 -2
  158. package/dist/lib/hydration.d.ts.map +1 -1
  159. package/dist/lib/hydration.js +168 -16
  160. package/dist/lib/stream-display.d.ts +18 -0
  161. package/dist/lib/stream-display.d.ts.map +1 -0
  162. package/dist/lib/stream-display.js +30 -0
  163. package/dist/lib/toast.js +1 -1
  164. package/dist/lib/trace-builder.d.ts.map +1 -1
  165. package/dist/lib/trace-builder.js +3 -2
  166. package/dist/lib/utils.d.ts +9 -9
  167. package/dist/lib/utils.d.ts.map +1 -1
  168. package/dist/lib/utils.js +27 -32
  169. package/dist/styles.css +475 -1
  170. package/package.json +6 -4
  171. package/src/components/event-list-view.tsx +447 -282
  172. package/src/components/index.ts +12 -1
  173. package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
  174. package/src/components/new-trace-viewer/components/event-list.tsx +127 -0
  175. package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
  176. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
  177. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
  178. package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
  179. package/src/components/new-trace-viewer/components/split-pane.tsx +185 -0
  180. package/src/components/new-trace-viewer/components/timeline.tsx +552 -0
  181. package/src/components/new-trace-viewer/context.tsx +92 -0
  182. package/src/components/new-trace-viewer/detail-panel.tsx +55 -0
  183. package/src/components/new-trace-viewer/icons.tsx +79 -0
  184. package/src/components/new-trace-viewer/search.ts +178 -0
  185. package/src/components/new-trace-viewer/trace-viewer.tsx +694 -0
  186. package/src/components/new-trace-viewer/types.ts +0 -0
  187. package/src/components/new-trace-viewer/util/constants.ts +0 -0
  188. package/src/components/new-trace-viewer/utils.ts +482 -0
  189. package/src/components/run-trace-view.tsx +3 -0
  190. package/src/components/sidebar/attribute-panel.tsx +240 -196
  191. package/src/components/sidebar/copyable-data-block.tsx +49 -27
  192. package/src/components/sidebar/detail-card.tsx +119 -59
  193. package/src/components/sidebar/entity-detail-panel.tsx +92 -148
  194. package/src/components/sidebar/events-list.tsx +93 -108
  195. package/src/components/sidebar/sidebar-data-context.tsx +62 -0
  196. package/src/components/stream-viewer.tsx +18 -40
  197. package/src/components/trace-viewer/util/timing.ts +5 -9
  198. package/src/components/trace-viewer-new.tsx +49 -0
  199. package/src/components/ui/button.tsx +68 -0
  200. package/src/components/ui/data-inspector.tsx +427 -31
  201. package/src/components/ui/error-stack-block.tsx +39 -30
  202. package/src/components/ui/icon-button.tsx +57 -0
  203. package/src/components/ui/inspector-theme.ts +39 -81
  204. package/src/components/workflow-trace-view.tsx +12 -2
  205. package/src/components/workflow-traces/trace-span-construction.ts +24 -6
  206. package/src/hooks/use-reduced-motion.ts +34 -0
  207. package/src/index.ts +9 -0
  208. package/src/lib/exact-event-search-id.ts +67 -0
  209. package/src/lib/hydration.ts +182 -14
  210. package/src/lib/stream-display.ts +48 -0
  211. package/src/lib/trace-builder.ts +2 -1
  212. package/src/lib/utils.ts +27 -32
  213. package/src/styles.css +475 -1
  214. package/dist/components/error-boundary.js.map +0 -1
  215. package/dist/components/event-list-view.js.map +0 -1
  216. package/dist/components/hook-actions.js.map +0 -1
  217. package/dist/components/index.js.map +0 -1
  218. package/dist/components/run-trace-view.js.map +0 -1
  219. package/dist/components/sidebar/attribute-panel.js.map +0 -1
  220. package/dist/components/sidebar/conversation-view.js.map +0 -1
  221. package/dist/components/sidebar/copyable-data-block.js.map +0 -1
  222. package/dist/components/sidebar/detail-card.js.map +0 -1
  223. package/dist/components/sidebar/entity-detail-panel.js.map +0 -1
  224. package/dist/components/sidebar/events-list.js.map +0 -1
  225. package/dist/components/sidebar/resolve-hook-modal.js.map +0 -1
  226. package/dist/components/stream-viewer.js.map +0 -1
  227. package/dist/components/trace-viewer/components/map.js.map +0 -1
  228. package/dist/components/trace-viewer/components/markers.js.map +0 -1
  229. package/dist/components/trace-viewer/components/node.js.map +0 -1
  230. package/dist/components/trace-viewer/components/search-input.js.map +0 -1
  231. package/dist/components/trace-viewer/components/search.js.map +0 -1
  232. package/dist/components/trace-viewer/components/span-content.js.map +0 -1
  233. package/dist/components/trace-viewer/components/span-detail-panel.js.map +0 -1
  234. package/dist/components/trace-viewer/components/span-segments.js.map +0 -1
  235. package/dist/components/trace-viewer/components/span-strategies.js.map +0 -1
  236. package/dist/components/trace-viewer/components/ui.js.map +0 -1
  237. package/dist/components/trace-viewer/components/zoom-button.js.map +0 -1
  238. package/dist/components/trace-viewer/components/zoom-icons.js.map +0 -1
  239. package/dist/components/trace-viewer/context.js.map +0 -1
  240. package/dist/components/trace-viewer/index.js.map +0 -1
  241. package/dist/components/trace-viewer/trace-viewer.js.map +0 -1
  242. package/dist/components/trace-viewer/types.js.map +0 -1
  243. package/dist/components/trace-viewer/util/constants.js.map +0 -1
  244. package/dist/components/trace-viewer/util/scrollbar-width.js.map +0 -1
  245. package/dist/components/trace-viewer/util/timing.js.map +0 -1
  246. package/dist/components/trace-viewer/util/tree.js.map +0 -1
  247. package/dist/components/trace-viewer/util/use-immediate-style.js.map +0 -1
  248. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +0 -1
  249. package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +0 -1
  250. package/dist/components/trace-viewer/worker.js.map +0 -1
  251. package/dist/components/ui/alert.js.map +0 -1
  252. package/dist/components/ui/card.js.map +0 -1
  253. package/dist/components/ui/data-inspector.js.map +0 -1
  254. package/dist/components/ui/decrypt-button.js.map +0 -1
  255. package/dist/components/ui/error-card.js.map +0 -1
  256. package/dist/components/ui/error-stack-block.js.map +0 -1
  257. package/dist/components/ui/inspector-theme.js.map +0 -1
  258. package/dist/components/ui/load-more-button.js.map +0 -1
  259. package/dist/components/ui/menu-dropdown.js.map +0 -1
  260. package/dist/components/ui/skeleton.js.map +0 -1
  261. package/dist/components/ui/spinner.js.map +0 -1
  262. package/dist/components/ui/timestamp-tooltip.js.map +0 -1
  263. package/dist/components/workflow-trace-view.js.map +0 -1
  264. package/dist/components/workflow-traces/event-colors.js.map +0 -1
  265. package/dist/components/workflow-traces/trace-colors.js.map +0 -1
  266. package/dist/components/workflow-traces/trace-span-construction.js.map +0 -1
  267. package/dist/components/workflow-traces/trace-time-utils.js.map +0 -1
  268. package/dist/hooks/use-dark-mode.js.map +0 -1
  269. package/dist/index.js.map +0 -1
  270. package/dist/lib/event-analysis.js.map +0 -1
  271. package/dist/lib/event-materialization.js.map +0 -1
  272. package/dist/lib/hydration.js.map +0 -1
  273. package/dist/lib/toast.js.map +0 -1
  274. package/dist/lib/trace-builder.js.map +0 -1
  275. package/dist/lib/utils.js.map +0 -1
@@ -3,13 +3,24 @@
3
3
  import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
4
4
  import type { Event, WorkflowRun } from '@workflow/world';
5
5
  import { Check, ChevronRight, Copy } from 'lucide-react';
6
- import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
6
+ import type {
7
+ KeyboardEvent as ReactKeyboardEvent,
8
+ MouseEvent as ReactMouseEvent,
9
+ ReactNode,
10
+ } from 'react';
7
11
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
12
  import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
9
13
  import { isEncryptedMarker } from '../lib/hydration';
14
+ import {
15
+ parseExactWorkflowSearchId,
16
+ looksLikeWorkflowIdSearchInput,
17
+ type ExactIdSearchResult,
18
+ type ExactWorkflowSearchIdKind,
19
+ } from '../lib/exact-event-search-id';
10
20
  import { DecryptButton } from './ui/decrypt-button';
11
21
  import { formatDuration } from '../lib/utils';
12
- import { DataInspector } from './ui/data-inspector';
22
+ import { useToast } from '../lib/toast';
23
+ import { DataInspector, DecryptClickContext } from './ui/data-inspector';
13
24
  import {
14
25
  ErrorStackBlock,
15
26
  isStructuredErrorWithStack,
@@ -142,7 +153,7 @@ function buildNameMaps(
142
153
  return { correlationNameMap, workflowName };
143
154
  }
144
155
 
145
- interface DurationInfo {
156
+ export interface DurationInfo {
146
157
  /** Time from created → started (ms) */
147
158
  queued?: number;
148
159
  /** Time from started → completed/failed/cancelled (ms) */
@@ -154,19 +165,30 @@ interface DurationInfo {
154
165
  * created ↔ started (queued) and started ↔ completed/failed/cancelled (ran).
155
166
  * Also computes run-level durations under the key '__run__'.
156
167
  */
157
- function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
168
+ export function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
169
+ // Process events in chronological order so the result doesn't depend on
170
+ // the caller's sort direction. Retried steps emit multiple `step_started`
171
+ // events for the same correlationId; the queued duration must be measured
172
+ // against the first one, not the last.
173
+ const chronological = [...events].sort(
174
+ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
175
+ );
176
+
158
177
  const createdTimes = new Map<string, number>();
178
+ const firstStartedTimes = new Map<string, number>();
159
179
  const startedTimes = new Map<string, number>();
160
180
  const durations = new Map<string, DurationInfo>();
161
181
 
162
- for (const event of events) {
182
+ for (const event of chronological) {
163
183
  const ts = new Date(event.createdAt).getTime();
164
184
  const key = event.correlationId ?? '__run__';
165
185
  const type: string = event.eventType;
166
186
 
167
187
  // Track created times (first event for each correlation)
168
188
  if (type === 'step_created' || type === 'run_created') {
169
- createdTimes.set(key, ts);
189
+ if (!createdTimes.has(key)) {
190
+ createdTimes.set(key, ts);
191
+ }
170
192
  }
171
193
 
172
194
  // Track started times & compute queued duration
@@ -176,16 +198,21 @@ function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
176
198
  type === 'workflow_started'
177
199
  ) {
178
200
  startedTimes.set(key, ts);
179
- // If no explicit created event was seen, use the started time as created
180
- if (!createdTimes.has(key)) {
181
- createdTimes.set(key, ts);
182
- }
183
- const createdAt = createdTimes.get(key);
184
- const info = durations.get(key) ?? {};
185
- if (createdAt !== undefined) {
186
- info.queued = ts - createdAt;
201
+ // The queued duration is anchored on the first start event only
202
+ // subsequent step_started events come from retries.
203
+ if (!firstStartedTimes.has(key)) {
204
+ firstStartedTimes.set(key, ts);
205
+ // If no explicit created event was seen, use the started time as created
206
+ if (!createdTimes.has(key)) {
207
+ createdTimes.set(key, ts);
208
+ }
209
+ const createdAt = createdTimes.get(key);
210
+ const info = durations.get(key) ?? {};
211
+ if (createdAt !== undefined) {
212
+ info.queued = ts - createdAt;
213
+ }
214
+ durations.set(key, info);
187
215
  }
188
- durations.set(key, info);
189
216
  }
190
217
 
191
218
  // Compute ran duration on terminal events
@@ -703,6 +730,14 @@ interface EventsListProps {
703
730
  onDecrypt?: () => void;
704
731
  /** Whether the encryption key is currently being fetched. */
705
732
  isDecrypting?: boolean;
733
+ /** Run-level hint: the run contains encrypted data (from probe). */
734
+ hasEncryptedData?: boolean;
735
+ /** Fetch events for an exact correlation or event ID. */
736
+ onExactIdSearch?: (
737
+ id: string,
738
+ kind: ExactWorkflowSearchIdKind,
739
+ signal?: AbortSignal
740
+ ) => Promise<ExactIdSearchResult>;
706
741
  }
707
742
 
708
743
  function EventRow({
@@ -725,6 +760,7 @@ function EventRow({
725
760
  onCacheEventData,
726
761
  encryptionKey,
727
762
  onEncryptedDataDetected,
763
+ suppressGroupDimming = false,
728
764
  }: {
729
765
  event: Event;
730
766
  index: number;
@@ -745,6 +781,8 @@ function EventRow({
745
781
  onCacheEventData: (eventId: string, data: unknown) => void;
746
782
  encryptionKey?: Uint8Array;
747
783
  onEncryptedDataDetected?: () => void;
784
+ /** Exact-ID search results should not dim unrelated rows. */
785
+ suppressGroupDimming?: boolean;
748
786
  }) {
749
787
  const [isLoading, setIsLoading] = useState(false);
750
788
  const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
@@ -786,7 +824,7 @@ function EventRow({
786
824
 
787
825
  const hasActive = activeGroupKey !== undefined;
788
826
  const isRelated = rowGroupKey !== undefined && rowGroupKey === activeGroupKey;
789
- const isDimmed = hasActive && !isRelated;
827
+ const isDimmed = hasActive && !isRelated && !suppressGroupDimming;
790
828
  const isPulsing = hasActive && isRelated;
791
829
 
792
830
  // Gutter state derived from selectedGroupRange
@@ -801,7 +839,15 @@ function EventRow({
801
839
  selectedGroupRange !== null && index === selectedGroupRange.last;
802
840
 
803
841
  const loadEventDetails = useCallback(async () => {
804
- if (loadedEventData !== null || hasExistingEventData) {
842
+ if (loadedEventData !== null) {
843
+ return;
844
+ }
845
+ if (cachedEventData !== null) {
846
+ setLoadedEventData(cachedEventData);
847
+ setHasAttemptedLoad(true);
848
+ return;
849
+ }
850
+ if (isLoading) {
805
851
  return;
806
852
  }
807
853
  setIsLoading(true);
@@ -830,24 +876,20 @@ function EventRow({
830
876
  }, [
831
877
  event,
832
878
  loadedEventData,
833
- hasExistingEventData,
879
+ isLoading,
834
880
  onLoadEventData,
835
881
  onCacheEventData,
836
882
  encryptionKey,
837
883
  onEncryptedDataDetected,
884
+ cachedEventData,
838
885
  ]);
839
886
 
840
887
  // Auto-load event data when remounting in expanded state without cached data
841
888
  useEffect(() => {
842
- if (
843
- isExpanded &&
844
- loadedEventData === null &&
845
- !hasExistingEventData &&
846
- !isLoading &&
847
- !hasAttemptedLoad
848
- ) {
849
- loadEventDetails();
889
+ if (!isExpanded || isLoading) {
890
+ return;
850
891
  }
892
+ void loadEventDetails();
851
893
  // eslint-disable-next-line react-hooks/exhaustive-deps
852
894
  }, []);
853
895
 
@@ -875,8 +917,8 @@ function EventRow({
875
917
  const handleRowClick = useCallback(() => {
876
918
  onSelectGroup(rowGroupKey === selectedGroupKey ? undefined : rowGroupKey);
877
919
  onToggleExpand(event.eventId);
878
- if (!isExpanded && loadedEventData === null && !hasExistingEventData) {
879
- loadEventDetails();
920
+ if (!isExpanded) {
921
+ void loadEventDetails();
880
922
  }
881
923
  }, [
882
924
  selectedGroupKey,
@@ -885,14 +927,16 @@ function EventRow({
885
927
  onToggleExpand,
886
928
  event.eventId,
887
929
  isExpanded,
888
- loadedEventData,
889
- hasExistingEventData,
890
930
  loadEventDetails,
891
931
  ]);
892
932
 
893
- const eventData = hasExistingEventData
894
- ? (event as Event & { eventData: unknown }).eventData
895
- : loadedEventData;
933
+ const mergedEventData =
934
+ loadedEventData ??
935
+ (hasExistingEventData
936
+ ? (event as Event & { eventData: unknown }).eventData
937
+ : null);
938
+
939
+ const displayPayload = isLoading ? loadedEventData : mergedEventData;
896
940
 
897
941
  const contentOpacity = isDimmed ? 0.3 : 1;
898
942
 
@@ -1076,8 +1120,8 @@ function EventRow({
1076
1120
  )}
1077
1121
 
1078
1122
  {/* Payload */}
1079
- {eventData != null ? (
1080
- <PayloadBlock data={eventData} eventType={event.eventType} />
1123
+ {displayPayload != null ? (
1124
+ <PayloadBlock data={displayPayload} eventType={event.eventType} />
1081
1125
  ) : loadError ? (
1082
1126
  <div
1083
1127
  className="rounded-md border p-3 text-xs"
@@ -1090,7 +1134,7 @@ function EventRow({
1090
1134
  {loadError}
1091
1135
  </div>
1092
1136
  ) : isLoading ||
1093
- (!hasExistingEventData &&
1137
+ (loadedEventData === null &&
1094
1138
  !hasAttemptedLoad &&
1095
1139
  event.correlationId) ? (
1096
1140
  <div className="flex flex-col gap-2 p-3">
@@ -1130,7 +1174,10 @@ export function EventListView({
1130
1174
  onSortOrderChange,
1131
1175
  onDecrypt,
1132
1176
  isDecrypting = false,
1177
+ hasEncryptedData: hasEncryptedDataProp = false,
1178
+ onExactIdSearch,
1133
1179
  }: EventsListProps) {
1180
+ const toast = useToast();
1134
1181
  const [internalSortOrder, setInternalSortOrder] = useState<'asc' | 'desc'>(
1135
1182
  'asc'
1136
1183
  );
@@ -1146,25 +1193,42 @@ export function EventListView({
1146
1193
  [onSortOrderChange]
1147
1194
  );
1148
1195
 
1196
+ const [searchQuery, setSearchQuery] = useState('');
1197
+ const [searchResults, setSearchResults] = useState<Event[] | null>(null);
1198
+ const [searchResultsTruncated, setSearchResultsTruncated] = useState(false);
1199
+ const [searchError, setSearchError] = useState<string | null>(null);
1200
+ const [searchLoading, setSearchLoading] = useState(false);
1201
+ const [searchNotFound, setSearchNotFound] = useState(false);
1202
+ const searchRequestRef = useRef(0);
1203
+ const virtuosoRef = useRef<VirtuosoHandle>(null);
1204
+
1205
+ const parsedSearchId = useMemo(
1206
+ () => parseExactWorkflowSearchId(searchQuery),
1207
+ [searchQuery]
1208
+ );
1209
+ const isExactSearchActive = searchResults !== null;
1210
+
1149
1211
  const sortedEvents = useMemo(() => {
1150
- if (!events || events.length === 0) return [];
1212
+ const sourceEvents = isExactSearchActive ? searchResults : (events ?? []);
1213
+ if (sourceEvents.length === 0) return [];
1151
1214
  const dir = effectiveSortOrder === 'desc' ? -1 : 1;
1152
- return [...events].sort(
1215
+ return [...sourceEvents].sort(
1153
1216
  (a, b) =>
1154
1217
  dir *
1155
1218
  (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
1156
1219
  );
1157
- }, [events, effectiveSortOrder]);
1220
+ }, [events, effectiveSortOrder, isExactSearchActive, searchResults]);
1158
1221
 
1159
1222
  // Detect encrypted fields across all loaded events (inline eventData).
1160
1223
  const hasEncryptedInlineData = useMemo(() => {
1161
- if (!events) return false;
1162
- for (const event of events) {
1224
+ const sourceEvents = isExactSearchActive ? searchResults : events;
1225
+ if (!sourceEvents) return false;
1226
+ for (const event of sourceEvents) {
1163
1227
  const ed = (event as Record<string, unknown>).eventData;
1164
1228
  if (hasEncryptedValues(ed)) return true;
1165
1229
  }
1166
1230
  return false;
1167
- }, [events]);
1231
+ }, [events, isExactSearchActive, searchResults]);
1168
1232
 
1169
1233
  // Tracks whether any expanded row's lazy-loaded data contained encrypted markers.
1170
1234
  // Set to true by EventRow via onEncryptedDataDetected; never reset (sticky).
@@ -1174,11 +1238,16 @@ export function EventListView({
1174
1238
  setFoundEncryptedInLazyData(true);
1175
1239
  }, []);
1176
1240
 
1177
- const hasEncryptedData = hasEncryptedInlineData || foundEncryptedInLazyData;
1241
+ const hasEncryptedData =
1242
+ hasEncryptedDataProp || hasEncryptedInlineData || foundEncryptedInLazyData;
1178
1243
 
1179
1244
  const { correlationNameMap, workflowName } = useMemo(
1180
- () => buildNameMaps(events ?? null, run ?? null),
1181
- [events, run]
1245
+ () =>
1246
+ buildNameMaps(
1247
+ isExactSearchActive ? searchResults : (events ?? null),
1248
+ run ?? null
1249
+ ),
1250
+ [events, isExactSearchActive, run, searchResults]
1182
1251
  );
1183
1252
 
1184
1253
  const durationMap = useMemo(
@@ -1268,68 +1337,139 @@ export function EventListView({
1268
1337
  return first >= 0 ? { first, last } : null;
1269
1338
  }, [activeGroupKey, sortedEvents]);
1270
1339
 
1271
- const [searchQuery, setSearchQuery] = useState('');
1272
- const virtuosoRef = useRef<VirtuosoHandle>(null);
1273
-
1274
- const searchIndex = useMemo(() => {
1275
- const entries: {
1276
- fields: string[];
1277
- groupKey?: string;
1278
- eventId: string;
1279
- index: number;
1280
- }[] = [];
1281
- for (let i = 0; i < sortedEvents.length; i++) {
1282
- const ev = sortedEvents[i];
1283
- const isRun = isRunLevel(ev.eventType);
1284
- const name = isRun
1285
- ? (workflowName ?? '')
1286
- : ev.correlationId
1287
- ? (correlationNameMap.get(ev.correlationId) ?? '')
1288
- : '';
1289
- entries.push({
1290
- fields: [
1291
- ev.eventId,
1292
- ev.correlationId ?? '',
1293
- ev.eventType,
1294
- formatEventType(ev.eventType),
1295
- name,
1296
- ].map((f) => f.toLowerCase()),
1297
- groupKey: ev.correlationId ?? (isRun ? '__run__' : undefined),
1298
- eventId: ev.eventId,
1299
- index: i,
1300
- });
1301
- }
1302
- return entries;
1303
- }, [sortedEvents, correlationNameMap, workflowName]);
1304
-
1305
1340
  useEffect(() => {
1306
- const q = searchQuery.trim().toLowerCase();
1307
- if (!q) {
1341
+ const trimmed = searchQuery.trim();
1342
+ if (!trimmed) {
1343
+ searchRequestRef.current += 1;
1344
+ setSearchResults(null);
1345
+ setSearchResultsTruncated(false);
1346
+ setSearchError(null);
1347
+ setSearchLoading(false);
1348
+ setSearchNotFound(false);
1308
1349
  setSelectedGroupKey(undefined);
1309
1350
  return;
1310
1351
  }
1311
- let bestMatch: (typeof searchIndex)[number] | null = null;
1312
- let bestScore = 0;
1313
- for (const entry of searchIndex) {
1314
- for (const field of entry.fields) {
1315
- if (field && field.includes(q)) {
1316
- const score = q.length / field.length;
1317
- if (score > bestScore) {
1318
- bestScore = score;
1319
- bestMatch = entry;
1352
+
1353
+ const parsed = parseExactWorkflowSearchId(trimmed);
1354
+ if (!parsed || !onExactIdSearch) {
1355
+ setSearchResults(null);
1356
+ setSearchLoading(false);
1357
+ setSearchNotFound(false);
1358
+ return;
1359
+ }
1360
+
1361
+ const requestId = ++searchRequestRef.current;
1362
+ setSearchLoading(true);
1363
+ setSearchNotFound(false);
1364
+ setSearchError(null);
1365
+
1366
+ const abortController = new AbortController();
1367
+
1368
+ const timer = setTimeout(() => {
1369
+ void (async () => {
1370
+ try {
1371
+ const results = await onExactIdSearch(
1372
+ parsed.id,
1373
+ parsed.kind,
1374
+ abortController.signal
1375
+ );
1376
+ if (
1377
+ abortController.signal.aborted ||
1378
+ searchRequestRef.current !== requestId
1379
+ ) {
1380
+ return;
1381
+ }
1382
+
1383
+ if (results.status === 'error') {
1384
+ setSearchResults([]);
1385
+ setSearchResultsTruncated(false);
1386
+ setSearchNotFound(false);
1387
+ setSearchError(results.message);
1388
+ setSelectedGroupKey(undefined);
1389
+ return;
1390
+ }
1391
+
1392
+ if (
1393
+ results.status === 'not_found' ||
1394
+ (results.status === 'ok' && results.events.length === 0)
1395
+ ) {
1396
+ setSearchResults([]);
1397
+ setSearchResultsTruncated(false);
1398
+ setSearchNotFound(true);
1399
+ setSearchError(null);
1400
+ setSelectedGroupKey(undefined);
1401
+ return;
1402
+ }
1403
+
1404
+ setSearchResults(results.events);
1405
+ setSearchResultsTruncated(Boolean(results.truncated));
1406
+ setSearchNotFound(false);
1407
+ setSearchError(null);
1408
+ setSelectedGroupKey(
1409
+ parsed.kind === 'event'
1410
+ ? (() => {
1411
+ const first = results.events[0];
1412
+ if (!first) return undefined;
1413
+ return isRunLevel(first.eventType)
1414
+ ? '__run__'
1415
+ : (first.correlationId ?? undefined);
1416
+ })()
1417
+ : parsed.id
1418
+ );
1419
+ virtuosoRef.current?.scrollToIndex({
1420
+ index: 0,
1421
+ align: 'start',
1422
+ behavior: 'smooth',
1423
+ });
1424
+ } catch {
1425
+ if (
1426
+ abortController.signal.aborted ||
1427
+ searchRequestRef.current !== requestId
1428
+ ) {
1429
+ return;
1430
+ }
1431
+ setSearchResults([]);
1432
+ setSearchResultsTruncated(false);
1433
+ setSearchNotFound(false);
1434
+ setSearchError('Failed to search events. Try again.');
1435
+ setSelectedGroupKey(undefined);
1436
+ } finally {
1437
+ if (
1438
+ searchRequestRef.current === requestId &&
1439
+ !abortController.signal.aborted
1440
+ ) {
1441
+ setSearchLoading(false);
1320
1442
  }
1321
1443
  }
1444
+ })();
1445
+ }, 300);
1446
+
1447
+ return () => {
1448
+ clearTimeout(timer);
1449
+ abortController.abort();
1450
+ };
1451
+ }, [searchQuery, onExactIdSearch]);
1452
+
1453
+ const handleSearchKeyDown = useCallback(
1454
+ (event: ReactKeyboardEvent<HTMLInputElement>) => {
1455
+ if (event.key !== 'Enter') {
1456
+ return;
1322
1457
  }
1323
- }
1324
- if (bestMatch) {
1325
- setSelectedGroupKey(bestMatch.groupKey);
1326
- virtuosoRef.current?.scrollToIndex({
1327
- index: bestMatch.index,
1328
- align: 'center',
1329
- behavior: 'smooth',
1330
- });
1331
- }
1332
- }, [searchQuery, searchIndex]);
1458
+
1459
+ const trimmed = searchQuery.trim();
1460
+ if (
1461
+ !trimmed ||
1462
+ parseExactWorkflowSearchId(trimmed) ||
1463
+ !onExactIdSearch ||
1464
+ !looksLikeWorkflowIdSearchInput(trimmed)
1465
+ ) {
1466
+ return;
1467
+ }
1468
+
1469
+ toast.info('Enter a full step ID, wait ID, hook ID, or event ID');
1470
+ },
1471
+ [searchQuery, onExactIdSearch, toast]
1472
+ );
1333
1473
 
1334
1474
  // Track whether we've ever had events to distinguish initial load from refetch
1335
1475
  const hasHadEventsRef = useRef(false);
@@ -1375,207 +1515,232 @@ export function EventListView({
1375
1515
  );
1376
1516
  }
1377
1517
 
1378
- if (!isLoading && (!events || events.length === 0)) {
1379
- return (
1380
- <div
1381
- className="flex items-center justify-center h-full text-sm"
1382
- style={{ color: 'var(--ds-gray-700)' }}
1383
- >
1384
- No events found
1385
- </div>
1386
- );
1387
- }
1388
-
1389
1518
  return (
1390
- <div className="h-full flex flex-col overflow-hidden">
1391
- <style>{`@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}`}</style>
1392
- {/* Search bar + sort */}
1393
- <div
1394
- style={{
1395
- padding: 6,
1396
- backgroundColor: 'var(--ds-background-100)',
1397
- display: 'flex',
1398
- gap: 6,
1399
- }}
1400
- >
1401
- <label
1519
+ <DecryptClickContext.Provider
1520
+ value={onDecrypt ? { onDecrypt, isDecrypting } : undefined}
1521
+ >
1522
+ <div className="h-full flex flex-col overflow-hidden">
1523
+ <style>{`@keyframes workflow-dot-pulse{0%{transform:scale(1);opacity:.7}70%,100%{transform:scale(2.2);opacity:0}}`}</style>
1524
+ {/* Search bar + sort */}
1525
+ <div
1402
1526
  style={{
1527
+ padding: 6,
1528
+ backgroundColor: 'var(--ds-background-100)',
1403
1529
  display: 'flex',
1404
- alignItems: 'center',
1405
- justifyContent: 'center',
1406
- borderRadius: 6,
1407
- boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
1408
- background: 'var(--ds-background-100)',
1409
- height: 40,
1410
- flex: 1,
1411
- minWidth: 0,
1530
+ gap: 6,
1412
1531
  }}
1413
1532
  >
1414
- <div
1533
+ <label
1415
1534
  style={{
1416
- width: 40,
1417
- height: 40,
1418
1535
  display: 'flex',
1419
1536
  alignItems: 'center',
1420
1537
  justifyContent: 'center',
1421
- color: 'var(--ds-gray-800)',
1422
- flexShrink: 0,
1538
+ borderRadius: 6,
1539
+ boxShadow: '0 0 0 1px var(--ds-gray-alpha-400)',
1540
+ background: 'var(--ds-background-100)',
1541
+ height: 40,
1542
+ flex: 1,
1543
+ minWidth: 0,
1423
1544
  }}
1424
1545
  >
1425
- <svg
1426
- width={16}
1427
- height={16}
1428
- viewBox="0 0 16 16"
1429
- fill="none"
1430
- aria-hidden="true"
1431
- focusable="false"
1546
+ <div
1547
+ style={{
1548
+ width: 40,
1549
+ height: 40,
1550
+ display: 'flex',
1551
+ alignItems: 'center',
1552
+ justifyContent: 'center',
1553
+ color: 'var(--ds-gray-800)',
1554
+ flexShrink: 0,
1555
+ }}
1432
1556
  >
1433
- <circle
1434
- cx="7"
1435
- cy="7"
1436
- r="4.5"
1437
- stroke="currentColor"
1438
- strokeWidth="1.5"
1439
- />
1440
- <path
1441
- d="M11.5 11.5L14 14"
1442
- stroke="currentColor"
1443
- strokeWidth="1.5"
1444
- strokeLinecap="round"
1445
- />
1446
- </svg>
1447
- </div>
1448
- <input
1449
- type="search"
1450
- placeholder="Search by name, event type, or ID…"
1451
- value={searchQuery}
1452
- onChange={(e) => setSearchQuery(e.target.value)}
1453
- style={{
1454
- marginLeft: -16,
1455
- paddingInline: 12,
1456
- fontFamily: 'inherit',
1457
- fontSize: 14,
1458
- background: 'transparent',
1459
- border: 'none',
1460
- outline: 'none',
1461
- height: 40,
1462
- width: '100%',
1463
- }}
1464
- />
1465
- </label>
1466
- <MenuDropdown
1467
- options={SORT_OPTIONS}
1468
- value={effectiveSortOrder}
1469
- onChange={handleSortOrderChange}
1470
- />
1471
- {(hasEncryptedData || encryptionKey) && onDecrypt && (
1472
- <DecryptButton
1473
- decrypted={!!encryptionKey}
1474
- loading={isDecrypting}
1475
- onClick={onDecrypt}
1557
+ <svg
1558
+ width={16}
1559
+ height={16}
1560
+ viewBox="0 0 16 16"
1561
+ fill="none"
1562
+ aria-hidden="true"
1563
+ focusable="false"
1564
+ >
1565
+ <circle
1566
+ cx="7"
1567
+ cy="7"
1568
+ r="4.5"
1569
+ stroke="currentColor"
1570
+ strokeWidth="1.5"
1571
+ />
1572
+ <path
1573
+ d="M11.5 11.5L14 14"
1574
+ stroke="currentColor"
1575
+ strokeWidth="1.5"
1576
+ strokeLinecap="round"
1577
+ />
1578
+ </svg>
1579
+ </div>
1580
+ <input
1581
+ type="search"
1582
+ placeholder="Search by step ID, wait ID, hook ID, or event ID…"
1583
+ value={searchQuery}
1584
+ onChange={(e) => setSearchQuery(e.target.value)}
1585
+ onKeyDown={handleSearchKeyDown}
1586
+ disabled={!onExactIdSearch}
1587
+ title={
1588
+ onExactIdSearch
1589
+ ? undefined
1590
+ : 'Exact ID search is unavailable in this view.'
1591
+ }
1592
+ style={{
1593
+ marginLeft: -16,
1594
+ paddingInline: 12,
1595
+ fontFamily: 'inherit',
1596
+ fontSize: 14,
1597
+ background: 'transparent',
1598
+ border: 'none',
1599
+ outline: 'none',
1600
+ height: 40,
1601
+ width: '100%',
1602
+ opacity: onExactIdSearch ? 1 : 0.5,
1603
+ cursor: onExactIdSearch ? 'text' : 'not-allowed',
1604
+ }}
1605
+ />
1606
+ </label>
1607
+ <MenuDropdown
1608
+ options={SORT_OPTIONS}
1609
+ value={effectiveSortOrder}
1610
+ onChange={handleSortOrderChange}
1476
1611
  />
1477
- )}
1478
- </div>
1479
-
1480
- {/* Header */}
1481
- <div
1482
- className="flex items-center gap-0 text-[13px] font-medium h-10 border-b flex-shrink-0"
1483
- style={{
1484
- borderColor: 'var(--ds-gray-alpha-200)',
1485
- color: 'var(--ds-gray-900)',
1486
- backgroundColor: 'var(--ds-background-100)',
1487
- }}
1488
- >
1489
- <div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
1490
- <div className="w-5 flex-shrink-0" />
1491
- <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1492
- Time
1493
- </div>
1494
- <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1495
- Event Type
1496
- </div>
1497
- <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1498
- Name
1499
- </div>
1500
- <div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
1501
- Correlation ID
1502
- </div>
1503
- <div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
1504
- Event ID
1612
+ {(hasEncryptedData || encryptionKey) && onDecrypt && (
1613
+ <DecryptButton
1614
+ decrypted={!!encryptionKey}
1615
+ loading={isDecrypting}
1616
+ onClick={onDecrypt}
1617
+ />
1618
+ )}
1505
1619
  </div>
1506
- </div>
1507
1620
 
1508
- {/* Virtualized event rows or refetching skeleton */}
1509
- {isRefetching ? (
1510
- <RowsSkeleton />
1511
- ) : (
1512
- <Virtuoso
1513
- ref={virtuosoRef}
1514
- totalCount={sortedEvents.length}
1515
- overscan={20}
1516
- defaultItemHeight={40}
1517
- endReached={() => {
1518
- if (!hasMoreEvents || isLoadingMoreEvents) {
1519
- return;
1520
- }
1521
- void onLoadMoreEvents?.();
1522
- }}
1523
- itemContent={(index: number) => {
1524
- const ev = sortedEvents[index];
1525
- return (
1526
- <EventRow
1527
- event={ev}
1528
- index={index}
1529
- isFirst={index === 0}
1530
- isLast={index === sortedEvents.length - 1}
1531
- isExpanded={expandedEventIds.has(ev.eventId)}
1532
- onToggleExpand={toggleEventExpanded}
1533
- activeGroupKey={activeGroupKey}
1534
- selectedGroupKey={selectedGroupKey}
1535
- selectedGroupRange={selectedGroupRange}
1536
- correlationNameMap={correlationNameMap}
1537
- workflowName={workflowName}
1538
- durationMap={durationMap}
1539
- onSelectGroup={onSelectGroup}
1540
- onHoverGroup={onHoverGroup}
1541
- onLoadEventData={onLoadEventData}
1542
- cachedEventData={
1543
- eventDataCacheRef.current.get(ev.eventId) ?? null
1544
- }
1545
- onCacheEventData={cacheEventData}
1546
- encryptionKey={encryptionKey}
1547
- onEncryptedDataDetected={handleEncryptedDataDetected}
1548
- />
1549
- );
1621
+ {/* Header */}
1622
+ <div
1623
+ className="flex items-center gap-0 text-[13px] font-medium h-10 border-b flex-shrink-0"
1624
+ style={{
1625
+ borderColor: 'var(--ds-gray-alpha-200)',
1626
+ color: 'var(--ds-gray-900)',
1627
+ backgroundColor: 'var(--ds-background-100)',
1550
1628
  }}
1551
- style={{ flex: 1, minHeight: 0 }}
1552
- />
1553
- )}
1629
+ >
1630
+ <div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
1631
+ <div className="w-5 flex-shrink-0" />
1632
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1633
+ Time
1634
+ </div>
1635
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1636
+ Event Type
1637
+ </div>
1638
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1639
+ Name
1640
+ </div>
1641
+ <div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
1642
+ Correlation ID
1643
+ </div>
1644
+ <div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
1645
+ Event ID
1646
+ </div>
1647
+ </div>
1554
1648
 
1555
- {/* Fixed footer count + load more */}
1556
- <div
1557
- className="relative flex-shrink-0 flex items-center h-10 border-t px-4 text-xs"
1558
- style={{
1559
- borderColor: 'var(--ds-gray-alpha-200)',
1560
- color: 'var(--ds-gray-900)',
1561
- backgroundColor: 'var(--ds-background-100)',
1562
- }}
1563
- >
1564
- <span>
1565
- {sortedEvents.length} event
1566
- {sortedEvents.length !== 1 ? 's' : ''} loaded
1567
- </span>
1568
- {hasMoreEvents && (
1569
- <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
1570
- <div className="pointer-events-auto">
1571
- <LoadMoreButton
1572
- loading={isLoadingMoreEvents}
1573
- onClick={() => void onLoadMoreEvents?.()}
1574
- />
1575
- </div>
1649
+ {/* Virtualized event rows or refetching skeleton */}
1650
+ {isRefetching || searchLoading ? (
1651
+ <RowsSkeleton />
1652
+ ) : sortedEvents.length === 0 ? (
1653
+ <div
1654
+ className="flex flex-1 items-center justify-center px-6 text-center text-sm"
1655
+ style={{ color: 'var(--ds-gray-700)' }}
1656
+ >
1657
+ {searchNotFound && searchQuery.trim()
1658
+ ? `No events found for ${searchQuery.trim()}`
1659
+ : searchError
1660
+ ? searchError
1661
+ : parsedSearchId && searchQuery.trim() && !onExactIdSearch
1662
+ ? 'Exact ID search is unavailable in this view.'
1663
+ : 'No events found'}
1576
1664
  </div>
1665
+ ) : (
1666
+ <Virtuoso
1667
+ ref={virtuosoRef}
1668
+ totalCount={sortedEvents.length}
1669
+ overscan={20}
1670
+ defaultItemHeight={40}
1671
+ endReached={() => {
1672
+ if (
1673
+ isExactSearchActive ||
1674
+ !hasMoreEvents ||
1675
+ isLoadingMoreEvents
1676
+ ) {
1677
+ return;
1678
+ }
1679
+ void onLoadMoreEvents?.();
1680
+ }}
1681
+ itemContent={(index: number) => {
1682
+ const ev = sortedEvents[index];
1683
+ return (
1684
+ <EventRow
1685
+ event={ev}
1686
+ index={index}
1687
+ isFirst={index === 0}
1688
+ isLast={index === sortedEvents.length - 1}
1689
+ isExpanded={expandedEventIds.has(ev.eventId)}
1690
+ onToggleExpand={toggleEventExpanded}
1691
+ activeGroupKey={activeGroupKey}
1692
+ selectedGroupKey={selectedGroupKey}
1693
+ selectedGroupRange={selectedGroupRange}
1694
+ correlationNameMap={correlationNameMap}
1695
+ workflowName={workflowName}
1696
+ durationMap={durationMap}
1697
+ onSelectGroup={onSelectGroup}
1698
+ onHoverGroup={onHoverGroup}
1699
+ onLoadEventData={onLoadEventData}
1700
+ cachedEventData={
1701
+ eventDataCacheRef.current.get(ev.eventId) ?? null
1702
+ }
1703
+ onCacheEventData={cacheEventData}
1704
+ encryptionKey={encryptionKey}
1705
+ onEncryptedDataDetected={handleEncryptedDataDetected}
1706
+ suppressGroupDimming={isExactSearchActive}
1707
+ />
1708
+ );
1709
+ }}
1710
+ style={{ flex: 1, minHeight: 0 }}
1711
+ />
1577
1712
  )}
1713
+
1714
+ {/* Fixed footer — count + load more */}
1715
+ <div
1716
+ className="relative flex-shrink-0 flex items-center h-10 border-t px-4 text-xs"
1717
+ style={{
1718
+ borderColor: 'var(--ds-gray-alpha-200)',
1719
+ color: 'var(--ds-gray-900)',
1720
+ backgroundColor: 'var(--ds-background-100)',
1721
+ }}
1722
+ >
1723
+ <span>
1724
+ {isExactSearchActive
1725
+ ? searchError
1726
+ ? searchError
1727
+ : searchNotFound
1728
+ ? `No events found for ${searchQuery.trim()}`
1729
+ : `${sortedEvents.length} event${sortedEvents.length !== 1 ? 's' : ''} for ${searchQuery.trim()}${searchResultsTruncated ? ' (results may be truncated)' : ''}`
1730
+ : `${sortedEvents.length} event${sortedEvents.length !== 1 ? 's' : ''} loaded`}
1731
+ </span>
1732
+ {!isExactSearchActive && hasMoreEvents && (
1733
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
1734
+ <div className="pointer-events-auto">
1735
+ <LoadMoreButton
1736
+ loading={isLoadingMoreEvents}
1737
+ onClick={() => void onLoadMoreEvents?.()}
1738
+ />
1739
+ </div>
1740
+ </div>
1741
+ )}
1742
+ </div>
1578
1743
  </div>
1579
- </div>
1744
+ </DecryptClickContext.Provider>
1580
1745
  );
1581
1746
  }