@workflow/web-shared 4.1.5 → 4.1.7
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/event-list-view.d.ts +16 -1
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +171 -77
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/workflow-trace-view.js +1 -1
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/exact-event-search-id.d.ts +25 -0
- package/dist/lib/exact-event-search-id.d.ts.map +1 -0
- package/dist/lib/exact-event-search-id.js +39 -0
- package/dist/lib/exact-event-search-id.js.map +1 -0
- package/package.json +4 -4
- package/src/components/event-list-view.tsx +246 -95
- package/src/components/workflow-trace-view.tsx +1 -1
- package/src/components/workflow-traces/trace-span-construction.ts +1 -1
- package/src/index.ts +7 -0
- package/src/lib/exact-event-search-id.ts +67 -0
|
@@ -3,12 +3,23 @@
|
|
|
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 {
|
|
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';
|
|
22
|
+
import { useToast } from '../lib/toast';
|
|
12
23
|
import { DataInspector, DecryptClickContext } from './ui/data-inspector';
|
|
13
24
|
import {
|
|
14
25
|
ErrorStackBlock,
|
|
@@ -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
|
|
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.
|
|
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
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
@@ -705,6 +732,12 @@ interface EventsListProps {
|
|
|
705
732
|
isDecrypting?: boolean;
|
|
706
733
|
/** Run-level hint: the run contains encrypted data (from probe). */
|
|
707
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>;
|
|
708
741
|
}
|
|
709
742
|
|
|
710
743
|
function EventRow({
|
|
@@ -727,6 +760,7 @@ function EventRow({
|
|
|
727
760
|
onCacheEventData,
|
|
728
761
|
encryptionKey,
|
|
729
762
|
onEncryptedDataDetected,
|
|
763
|
+
suppressGroupDimming = false,
|
|
730
764
|
}: {
|
|
731
765
|
event: Event;
|
|
732
766
|
index: number;
|
|
@@ -747,6 +781,8 @@ function EventRow({
|
|
|
747
781
|
onCacheEventData: (eventId: string, data: unknown) => void;
|
|
748
782
|
encryptionKey?: Uint8Array;
|
|
749
783
|
onEncryptedDataDetected?: () => void;
|
|
784
|
+
/** Exact-ID search results should not dim unrelated rows. */
|
|
785
|
+
suppressGroupDimming?: boolean;
|
|
750
786
|
}) {
|
|
751
787
|
const [isLoading, setIsLoading] = useState(false);
|
|
752
788
|
const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
|
|
@@ -788,7 +824,7 @@ function EventRow({
|
|
|
788
824
|
|
|
789
825
|
const hasActive = activeGroupKey !== undefined;
|
|
790
826
|
const isRelated = rowGroupKey !== undefined && rowGroupKey === activeGroupKey;
|
|
791
|
-
const isDimmed = hasActive && !isRelated;
|
|
827
|
+
const isDimmed = hasActive && !isRelated && !suppressGroupDimming;
|
|
792
828
|
const isPulsing = hasActive && isRelated;
|
|
793
829
|
|
|
794
830
|
// Gutter state derived from selectedGroupRange
|
|
@@ -1139,7 +1175,9 @@ export function EventListView({
|
|
|
1139
1175
|
onDecrypt,
|
|
1140
1176
|
isDecrypting = false,
|
|
1141
1177
|
hasEncryptedData: hasEncryptedDataProp = false,
|
|
1178
|
+
onExactIdSearch,
|
|
1142
1179
|
}: EventsListProps) {
|
|
1180
|
+
const toast = useToast();
|
|
1143
1181
|
const [internalSortOrder, setInternalSortOrder] = useState<'asc' | 'desc'>(
|
|
1144
1182
|
'asc'
|
|
1145
1183
|
);
|
|
@@ -1155,25 +1193,42 @@ export function EventListView({
|
|
|
1155
1193
|
[onSortOrderChange]
|
|
1156
1194
|
);
|
|
1157
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
|
+
|
|
1158
1211
|
const sortedEvents = useMemo(() => {
|
|
1159
|
-
|
|
1212
|
+
const sourceEvents = isExactSearchActive ? searchResults : (events ?? []);
|
|
1213
|
+
if (sourceEvents.length === 0) return [];
|
|
1160
1214
|
const dir = effectiveSortOrder === 'desc' ? -1 : 1;
|
|
1161
|
-
return [...
|
|
1215
|
+
return [...sourceEvents].sort(
|
|
1162
1216
|
(a, b) =>
|
|
1163
1217
|
dir *
|
|
1164
1218
|
(new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
|
1165
1219
|
);
|
|
1166
|
-
}, [events, effectiveSortOrder]);
|
|
1220
|
+
}, [events, effectiveSortOrder, isExactSearchActive, searchResults]);
|
|
1167
1221
|
|
|
1168
1222
|
// Detect encrypted fields across all loaded events (inline eventData).
|
|
1169
1223
|
const hasEncryptedInlineData = useMemo(() => {
|
|
1170
|
-
|
|
1171
|
-
|
|
1224
|
+
const sourceEvents = isExactSearchActive ? searchResults : events;
|
|
1225
|
+
if (!sourceEvents) return false;
|
|
1226
|
+
for (const event of sourceEvents) {
|
|
1172
1227
|
const ed = (event as Record<string, unknown>).eventData;
|
|
1173
1228
|
if (hasEncryptedValues(ed)) return true;
|
|
1174
1229
|
}
|
|
1175
1230
|
return false;
|
|
1176
|
-
}, [events]);
|
|
1231
|
+
}, [events, isExactSearchActive, searchResults]);
|
|
1177
1232
|
|
|
1178
1233
|
// Tracks whether any expanded row's lazy-loaded data contained encrypted markers.
|
|
1179
1234
|
// Set to true by EventRow via onEncryptedDataDetected; never reset (sticky).
|
|
@@ -1187,8 +1242,12 @@ export function EventListView({
|
|
|
1187
1242
|
hasEncryptedDataProp || hasEncryptedInlineData || foundEncryptedInLazyData;
|
|
1188
1243
|
|
|
1189
1244
|
const { correlationNameMap, workflowName } = useMemo(
|
|
1190
|
-
() =>
|
|
1191
|
-
|
|
1245
|
+
() =>
|
|
1246
|
+
buildNameMaps(
|
|
1247
|
+
isExactSearchActive ? searchResults : (events ?? null),
|
|
1248
|
+
run ?? null
|
|
1249
|
+
),
|
|
1250
|
+
[events, isExactSearchActive, run, searchResults]
|
|
1192
1251
|
);
|
|
1193
1252
|
|
|
1194
1253
|
const durationMap = useMemo(
|
|
@@ -1278,68 +1337,139 @@ export function EventListView({
|
|
|
1278
1337
|
return first >= 0 ? { first, last } : null;
|
|
1279
1338
|
}, [activeGroupKey, sortedEvents]);
|
|
1280
1339
|
|
|
1281
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
1282
|
-
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
|
1283
|
-
|
|
1284
|
-
const searchIndex = useMemo(() => {
|
|
1285
|
-
const entries: {
|
|
1286
|
-
fields: string[];
|
|
1287
|
-
groupKey?: string;
|
|
1288
|
-
eventId: string;
|
|
1289
|
-
index: number;
|
|
1290
|
-
}[] = [];
|
|
1291
|
-
for (let i = 0; i < sortedEvents.length; i++) {
|
|
1292
|
-
const ev = sortedEvents[i];
|
|
1293
|
-
const isRun = isRunLevel(ev.eventType);
|
|
1294
|
-
const name = isRun
|
|
1295
|
-
? (workflowName ?? '')
|
|
1296
|
-
: ev.correlationId
|
|
1297
|
-
? (correlationNameMap.get(ev.correlationId) ?? '')
|
|
1298
|
-
: '';
|
|
1299
|
-
entries.push({
|
|
1300
|
-
fields: [
|
|
1301
|
-
ev.eventId,
|
|
1302
|
-
ev.correlationId ?? '',
|
|
1303
|
-
ev.eventType,
|
|
1304
|
-
formatEventType(ev.eventType),
|
|
1305
|
-
name,
|
|
1306
|
-
].map((f) => f.toLowerCase()),
|
|
1307
|
-
groupKey: ev.correlationId ?? (isRun ? '__run__' : undefined),
|
|
1308
|
-
eventId: ev.eventId,
|
|
1309
|
-
index: i,
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
return entries;
|
|
1313
|
-
}, [sortedEvents, correlationNameMap, workflowName]);
|
|
1314
|
-
|
|
1315
1340
|
useEffect(() => {
|
|
1316
|
-
const
|
|
1317
|
-
if (!
|
|
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);
|
|
1318
1349
|
setSelectedGroupKey(undefined);
|
|
1319
1350
|
return;
|
|
1320
1351
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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);
|
|
1330
1442
|
}
|
|
1331
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;
|
|
1332
1457
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
+
);
|
|
1343
1473
|
|
|
1344
1474
|
// Track whether we've ever had events to distinguish initial load from refetch
|
|
1345
1475
|
const hasHadEventsRef = useRef(false);
|
|
@@ -1385,17 +1515,6 @@ export function EventListView({
|
|
|
1385
1515
|
);
|
|
1386
1516
|
}
|
|
1387
1517
|
|
|
1388
|
-
if (!isLoading && (!events || events.length === 0)) {
|
|
1389
|
-
return (
|
|
1390
|
-
<div
|
|
1391
|
-
className="flex items-center justify-center h-full text-sm"
|
|
1392
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
1393
|
-
>
|
|
1394
|
-
No events found
|
|
1395
|
-
</div>
|
|
1396
|
-
);
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
1518
|
return (
|
|
1400
1519
|
<DecryptClickContext.Provider
|
|
1401
1520
|
value={onDecrypt ? { onDecrypt, isDecrypting } : undefined}
|
|
@@ -1460,9 +1579,16 @@ export function EventListView({
|
|
|
1460
1579
|
</div>
|
|
1461
1580
|
<input
|
|
1462
1581
|
type="search"
|
|
1463
|
-
placeholder="Search by
|
|
1582
|
+
placeholder="Search by step ID, wait ID, hook ID, or event ID…"
|
|
1464
1583
|
value={searchQuery}
|
|
1465
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
|
+
}
|
|
1466
1592
|
style={{
|
|
1467
1593
|
marginLeft: -16,
|
|
1468
1594
|
paddingInline: 12,
|
|
@@ -1473,6 +1599,8 @@ export function EventListView({
|
|
|
1473
1599
|
outline: 'none',
|
|
1474
1600
|
height: 40,
|
|
1475
1601
|
width: '100%',
|
|
1602
|
+
opacity: onExactIdSearch ? 1 : 0.5,
|
|
1603
|
+
cursor: onExactIdSearch ? 'text' : 'not-allowed',
|
|
1476
1604
|
}}
|
|
1477
1605
|
/>
|
|
1478
1606
|
</label>
|
|
@@ -1519,8 +1647,21 @@ export function EventListView({
|
|
|
1519
1647
|
</div>
|
|
1520
1648
|
|
|
1521
1649
|
{/* Virtualized event rows or refetching skeleton */}
|
|
1522
|
-
{isRefetching ? (
|
|
1650
|
+
{isRefetching || searchLoading ? (
|
|
1523
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'}
|
|
1664
|
+
</div>
|
|
1524
1665
|
) : (
|
|
1525
1666
|
<Virtuoso
|
|
1526
1667
|
ref={virtuosoRef}
|
|
@@ -1528,7 +1669,11 @@ export function EventListView({
|
|
|
1528
1669
|
overscan={20}
|
|
1529
1670
|
defaultItemHeight={40}
|
|
1530
1671
|
endReached={() => {
|
|
1531
|
-
if (
|
|
1672
|
+
if (
|
|
1673
|
+
isExactSearchActive ||
|
|
1674
|
+
!hasMoreEvents ||
|
|
1675
|
+
isLoadingMoreEvents
|
|
1676
|
+
) {
|
|
1532
1677
|
return;
|
|
1533
1678
|
}
|
|
1534
1679
|
void onLoadMoreEvents?.();
|
|
@@ -1558,6 +1703,7 @@ export function EventListView({
|
|
|
1558
1703
|
onCacheEventData={cacheEventData}
|
|
1559
1704
|
encryptionKey={encryptionKey}
|
|
1560
1705
|
onEncryptedDataDetected={handleEncryptedDataDetected}
|
|
1706
|
+
suppressGroupDimming={isExactSearchActive}
|
|
1561
1707
|
/>
|
|
1562
1708
|
);
|
|
1563
1709
|
}}
|
|
@@ -1575,10 +1721,15 @@ export function EventListView({
|
|
|
1575
1721
|
}}
|
|
1576
1722
|
>
|
|
1577
1723
|
<span>
|
|
1578
|
-
{
|
|
1579
|
-
|
|
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`}
|
|
1580
1731
|
</span>
|
|
1581
|
-
{hasMoreEvents && (
|
|
1732
|
+
{!isExactSearchActive && hasMoreEvents && (
|
|
1582
1733
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
1583
1734
|
<div className="pointer-events-auto">
|
|
1584
1735
|
<LoadMoreButton
|
|
@@ -968,7 +968,7 @@ export const WorkflowTraceViewer = ({
|
|
|
968
968
|
}
|
|
969
969
|
|
|
970
970
|
return (
|
|
971
|
-
<div className="relative w-full h-full flex">
|
|
971
|
+
<div className="relative w-full h-full flex flex-row">
|
|
972
972
|
{/* Timeline (takes remaining space) */}
|
|
973
973
|
<div className="flex-1 min-w-0 relative">
|
|
974
974
|
<TraceViewerContextProvider
|
|
@@ -296,7 +296,7 @@ export function hookToSpan(hookEvents: Event[], maxEndTime: Date): Span | null {
|
|
|
296
296
|
|
|
297
297
|
return {
|
|
298
298
|
spanId: String(hook.hookId),
|
|
299
|
-
name: String(hook.hookId),
|
|
299
|
+
name: hook.token ?? String(hook.hookId),
|
|
300
300
|
kind: 1, // INTERNAL span kind
|
|
301
301
|
resource: 'hook',
|
|
302
302
|
library: WORKFLOW_LIBRARY,
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,13 @@ export {
|
|
|
14
14
|
waitEventsToWaitEntity,
|
|
15
15
|
} from './components/workflow-traces/trace-span-construction';
|
|
16
16
|
export type { EventAnalysis } from './lib/event-analysis';
|
|
17
|
+
export {
|
|
18
|
+
parseExactWorkflowSearchId,
|
|
19
|
+
looksLikeWorkflowIdSearchInput,
|
|
20
|
+
type ExactWorkflowSearchId,
|
|
21
|
+
type ExactWorkflowSearchIdKind,
|
|
22
|
+
type ExactIdSearchResult,
|
|
23
|
+
} from './lib/exact-event-search-id';
|
|
17
24
|
export {
|
|
18
25
|
analyzeEvents,
|
|
19
26
|
hasPendingHooksFromEvents,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Event } from '@workflow/world';
|
|
2
|
+
|
|
3
|
+
const WORKFLOW_ULID_BODY = '[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}';
|
|
4
|
+
|
|
5
|
+
const STEP_ID_PATTERN = new RegExp(`^step_(${WORKFLOW_ULID_BODY})$`, 'i');
|
|
6
|
+
const WAIT_ID_PATTERN = new RegExp(`^wait_(${WORKFLOW_ULID_BODY})$`, 'i');
|
|
7
|
+
const HOOK_ID_PATTERN = new RegExp(`^hook_(${WORKFLOW_ULID_BODY})$`, 'i');
|
|
8
|
+
const EVENT_ID_PATTERN = new RegExp(`^evnt_(${WORKFLOW_ULID_BODY})$`, 'i');
|
|
9
|
+
|
|
10
|
+
const WORKFLOW_ID_PREFIX_PATTERN = /^(step_|wait_|hook_|evnt_|wrun_)/i;
|
|
11
|
+
|
|
12
|
+
export type ExactWorkflowSearchIdKind = 'step' | 'wait' | 'hook' | 'event';
|
|
13
|
+
|
|
14
|
+
export type ExactWorkflowSearchId = {
|
|
15
|
+
kind: ExactWorkflowSearchIdKind;
|
|
16
|
+
id: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ExactIdSearchResult =
|
|
20
|
+
| { status: 'ok'; events: Event[]; truncated?: boolean }
|
|
21
|
+
| { status: 'not_found' }
|
|
22
|
+
| { status: 'error'; message: string };
|
|
23
|
+
|
|
24
|
+
function matchPrefixedId(
|
|
25
|
+
pattern: RegExp,
|
|
26
|
+
prefix: 'step' | 'wait' | 'hook' | 'evnt',
|
|
27
|
+
kind: ExactWorkflowSearchIdKind,
|
|
28
|
+
query: string
|
|
29
|
+
): ExactWorkflowSearchId | null {
|
|
30
|
+
const match = query.match(pattern);
|
|
31
|
+
if (!match) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return { kind, id: `${prefix}_${match[1].toUpperCase()}` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns a parsed workflow ID when `query` is a full step, wait, hook, or event ID.
|
|
39
|
+
* Partial IDs and run IDs (`wrun_`) are ignored. ULID bodies are matched case-insensitively
|
|
40
|
+
* and normalized to uppercase in the returned ID.
|
|
41
|
+
*/
|
|
42
|
+
export function parseExactWorkflowSearchId(
|
|
43
|
+
query: string
|
|
44
|
+
): ExactWorkflowSearchId | null {
|
|
45
|
+
const trimmed = query.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
matchPrefixedId(STEP_ID_PATTERN, 'step', 'step', trimmed) ??
|
|
52
|
+
matchPrefixedId(WAIT_ID_PATTERN, 'wait', 'wait', trimmed) ??
|
|
53
|
+
matchPrefixedId(HOOK_ID_PATTERN, 'hook', 'hook', trimmed) ??
|
|
54
|
+
matchPrefixedId(EVENT_ID_PATTERN, 'evnt', 'event', trimmed)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** True when input looks like the user is attempting an ID search (including partial). */
|
|
59
|
+
export function looksLikeWorkflowIdSearchInput(query: string): boolean {
|
|
60
|
+
const trimmed = query.trim();
|
|
61
|
+
if (!WORKFLOW_ID_PREFIX_PATTERN.test(trimmed)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// Distinguish IDs (contain digits) from event-type strings like step_started.
|
|
65
|
+
// Assumes workflow event types do not include digits in their names.
|
|
66
|
+
return /\d/.test(trimmed);
|
|
67
|
+
}
|