@workflow/web-shared 4.1.6 → 4.1.8
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 +4 -1
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +146 -65
- package/dist/components/event-list-view.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 +3 -3
- package/src/components/event-list-view.tsx +217 -82
- package/src/index.ts +7 -0
- package/src/lib/exact-event-search-id.ts +67 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow/web-shared",
|
|
3
3
|
"description": "Shared components for Workflow Observability UI",
|
|
4
|
-
"version": "4.1.
|
|
4
|
+
"version": "4.1.8",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"streamdown": "2.3.0",
|
|
53
53
|
"tailwind-merge": "3.5.0",
|
|
54
54
|
"tailwindcss": "4",
|
|
55
|
-
"@workflow/core": "4.
|
|
55
|
+
"@workflow/core": "4.3.0",
|
|
56
56
|
"@workflow/utils": "4.1.2",
|
|
57
|
-
"@workflow/world": "4.1.
|
|
57
|
+
"@workflow/world": "4.1.4"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@biomejs/biome": "^2.4.4",
|
|
@@ -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,
|
|
@@ -721,6 +732,12 @@ interface EventsListProps {
|
|
|
721
732
|
isDecrypting?: boolean;
|
|
722
733
|
/** Run-level hint: the run contains encrypted data (from probe). */
|
|
723
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>;
|
|
724
741
|
}
|
|
725
742
|
|
|
726
743
|
function EventRow({
|
|
@@ -743,6 +760,7 @@ function EventRow({
|
|
|
743
760
|
onCacheEventData,
|
|
744
761
|
encryptionKey,
|
|
745
762
|
onEncryptedDataDetected,
|
|
763
|
+
suppressGroupDimming = false,
|
|
746
764
|
}: {
|
|
747
765
|
event: Event;
|
|
748
766
|
index: number;
|
|
@@ -763,6 +781,8 @@ function EventRow({
|
|
|
763
781
|
onCacheEventData: (eventId: string, data: unknown) => void;
|
|
764
782
|
encryptionKey?: Uint8Array;
|
|
765
783
|
onEncryptedDataDetected?: () => void;
|
|
784
|
+
/** Exact-ID search results should not dim unrelated rows. */
|
|
785
|
+
suppressGroupDimming?: boolean;
|
|
766
786
|
}) {
|
|
767
787
|
const [isLoading, setIsLoading] = useState(false);
|
|
768
788
|
const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
|
|
@@ -804,7 +824,7 @@ function EventRow({
|
|
|
804
824
|
|
|
805
825
|
const hasActive = activeGroupKey !== undefined;
|
|
806
826
|
const isRelated = rowGroupKey !== undefined && rowGroupKey === activeGroupKey;
|
|
807
|
-
const isDimmed = hasActive && !isRelated;
|
|
827
|
+
const isDimmed = hasActive && !isRelated && !suppressGroupDimming;
|
|
808
828
|
const isPulsing = hasActive && isRelated;
|
|
809
829
|
|
|
810
830
|
// Gutter state derived from selectedGroupRange
|
|
@@ -1155,7 +1175,9 @@ export function EventListView({
|
|
|
1155
1175
|
onDecrypt,
|
|
1156
1176
|
isDecrypting = false,
|
|
1157
1177
|
hasEncryptedData: hasEncryptedDataProp = false,
|
|
1178
|
+
onExactIdSearch,
|
|
1158
1179
|
}: EventsListProps) {
|
|
1180
|
+
const toast = useToast();
|
|
1159
1181
|
const [internalSortOrder, setInternalSortOrder] = useState<'asc' | 'desc'>(
|
|
1160
1182
|
'asc'
|
|
1161
1183
|
);
|
|
@@ -1171,25 +1193,42 @@ export function EventListView({
|
|
|
1171
1193
|
[onSortOrderChange]
|
|
1172
1194
|
);
|
|
1173
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
|
+
|
|
1174
1211
|
const sortedEvents = useMemo(() => {
|
|
1175
|
-
|
|
1212
|
+
const sourceEvents = isExactSearchActive ? searchResults : (events ?? []);
|
|
1213
|
+
if (sourceEvents.length === 0) return [];
|
|
1176
1214
|
const dir = effectiveSortOrder === 'desc' ? -1 : 1;
|
|
1177
|
-
return [...
|
|
1215
|
+
return [...sourceEvents].sort(
|
|
1178
1216
|
(a, b) =>
|
|
1179
1217
|
dir *
|
|
1180
1218
|
(new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
|
1181
1219
|
);
|
|
1182
|
-
}, [events, effectiveSortOrder]);
|
|
1220
|
+
}, [events, effectiveSortOrder, isExactSearchActive, searchResults]);
|
|
1183
1221
|
|
|
1184
1222
|
// Detect encrypted fields across all loaded events (inline eventData).
|
|
1185
1223
|
const hasEncryptedInlineData = useMemo(() => {
|
|
1186
|
-
|
|
1187
|
-
|
|
1224
|
+
const sourceEvents = isExactSearchActive ? searchResults : events;
|
|
1225
|
+
if (!sourceEvents) return false;
|
|
1226
|
+
for (const event of sourceEvents) {
|
|
1188
1227
|
const ed = (event as Record<string, unknown>).eventData;
|
|
1189
1228
|
if (hasEncryptedValues(ed)) return true;
|
|
1190
1229
|
}
|
|
1191
1230
|
return false;
|
|
1192
|
-
}, [events]);
|
|
1231
|
+
}, [events, isExactSearchActive, searchResults]);
|
|
1193
1232
|
|
|
1194
1233
|
// Tracks whether any expanded row's lazy-loaded data contained encrypted markers.
|
|
1195
1234
|
// Set to true by EventRow via onEncryptedDataDetected; never reset (sticky).
|
|
@@ -1203,8 +1242,12 @@ export function EventListView({
|
|
|
1203
1242
|
hasEncryptedDataProp || hasEncryptedInlineData || foundEncryptedInLazyData;
|
|
1204
1243
|
|
|
1205
1244
|
const { correlationNameMap, workflowName } = useMemo(
|
|
1206
|
-
() =>
|
|
1207
|
-
|
|
1245
|
+
() =>
|
|
1246
|
+
buildNameMaps(
|
|
1247
|
+
isExactSearchActive ? searchResults : (events ?? null),
|
|
1248
|
+
run ?? null
|
|
1249
|
+
),
|
|
1250
|
+
[events, isExactSearchActive, run, searchResults]
|
|
1208
1251
|
);
|
|
1209
1252
|
|
|
1210
1253
|
const durationMap = useMemo(
|
|
@@ -1294,68 +1337,139 @@ export function EventListView({
|
|
|
1294
1337
|
return first >= 0 ? { first, last } : null;
|
|
1295
1338
|
}, [activeGroupKey, sortedEvents]);
|
|
1296
1339
|
|
|
1297
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
1298
|
-
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
|
1299
|
-
|
|
1300
|
-
const searchIndex = useMemo(() => {
|
|
1301
|
-
const entries: {
|
|
1302
|
-
fields: string[];
|
|
1303
|
-
groupKey?: string;
|
|
1304
|
-
eventId: string;
|
|
1305
|
-
index: number;
|
|
1306
|
-
}[] = [];
|
|
1307
|
-
for (let i = 0; i < sortedEvents.length; i++) {
|
|
1308
|
-
const ev = sortedEvents[i];
|
|
1309
|
-
const isRun = isRunLevel(ev.eventType);
|
|
1310
|
-
const name = isRun
|
|
1311
|
-
? (workflowName ?? '')
|
|
1312
|
-
: ev.correlationId
|
|
1313
|
-
? (correlationNameMap.get(ev.correlationId) ?? '')
|
|
1314
|
-
: '';
|
|
1315
|
-
entries.push({
|
|
1316
|
-
fields: [
|
|
1317
|
-
ev.eventId,
|
|
1318
|
-
ev.correlationId ?? '',
|
|
1319
|
-
ev.eventType,
|
|
1320
|
-
formatEventType(ev.eventType),
|
|
1321
|
-
name,
|
|
1322
|
-
].map((f) => f.toLowerCase()),
|
|
1323
|
-
groupKey: ev.correlationId ?? (isRun ? '__run__' : undefined),
|
|
1324
|
-
eventId: ev.eventId,
|
|
1325
|
-
index: i,
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
return entries;
|
|
1329
|
-
}, [sortedEvents, correlationNameMap, workflowName]);
|
|
1330
|
-
|
|
1331
1340
|
useEffect(() => {
|
|
1332
|
-
const
|
|
1333
|
-
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);
|
|
1334
1349
|
setSelectedGroupKey(undefined);
|
|
1335
1350
|
return;
|
|
1336
1351
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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);
|
|
1346
1442
|
}
|
|
1347
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;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const trimmed = searchQuery.trim();
|
|
1460
|
+
if (
|
|
1461
|
+
!trimmed ||
|
|
1462
|
+
parseExactWorkflowSearchId(trimmed) ||
|
|
1463
|
+
!onExactIdSearch ||
|
|
1464
|
+
!looksLikeWorkflowIdSearchInput(trimmed)
|
|
1465
|
+
) {
|
|
1466
|
+
return;
|
|
1348
1467
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
align: 'center',
|
|
1355
|
-
behavior: 'smooth',
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
}, [searchQuery, searchIndex]);
|
|
1468
|
+
|
|
1469
|
+
toast.info('Enter a full step ID, wait ID, hook ID, or event ID');
|
|
1470
|
+
},
|
|
1471
|
+
[searchQuery, onExactIdSearch, toast]
|
|
1472
|
+
);
|
|
1359
1473
|
|
|
1360
1474
|
// Track whether we've ever had events to distinguish initial load from refetch
|
|
1361
1475
|
const hasHadEventsRef = useRef(false);
|
|
@@ -1401,17 +1515,6 @@ export function EventListView({
|
|
|
1401
1515
|
);
|
|
1402
1516
|
}
|
|
1403
1517
|
|
|
1404
|
-
if (!isLoading && (!events || events.length === 0)) {
|
|
1405
|
-
return (
|
|
1406
|
-
<div
|
|
1407
|
-
className="flex items-center justify-center h-full text-sm"
|
|
1408
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
1409
|
-
>
|
|
1410
|
-
No events found
|
|
1411
|
-
</div>
|
|
1412
|
-
);
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
1518
|
return (
|
|
1416
1519
|
<DecryptClickContext.Provider
|
|
1417
1520
|
value={onDecrypt ? { onDecrypt, isDecrypting } : undefined}
|
|
@@ -1476,9 +1579,16 @@ export function EventListView({
|
|
|
1476
1579
|
</div>
|
|
1477
1580
|
<input
|
|
1478
1581
|
type="search"
|
|
1479
|
-
placeholder="Search by
|
|
1582
|
+
placeholder="Search by step ID, wait ID, hook ID, or event ID…"
|
|
1480
1583
|
value={searchQuery}
|
|
1481
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
|
+
}
|
|
1482
1592
|
style={{
|
|
1483
1593
|
marginLeft: -16,
|
|
1484
1594
|
paddingInline: 12,
|
|
@@ -1489,6 +1599,8 @@ export function EventListView({
|
|
|
1489
1599
|
outline: 'none',
|
|
1490
1600
|
height: 40,
|
|
1491
1601
|
width: '100%',
|
|
1602
|
+
opacity: onExactIdSearch ? 1 : 0.5,
|
|
1603
|
+
cursor: onExactIdSearch ? 'text' : 'not-allowed',
|
|
1492
1604
|
}}
|
|
1493
1605
|
/>
|
|
1494
1606
|
</label>
|
|
@@ -1535,8 +1647,21 @@ export function EventListView({
|
|
|
1535
1647
|
</div>
|
|
1536
1648
|
|
|
1537
1649
|
{/* Virtualized event rows or refetching skeleton */}
|
|
1538
|
-
{isRefetching ? (
|
|
1650
|
+
{isRefetching || searchLoading ? (
|
|
1539
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>
|
|
1540
1665
|
) : (
|
|
1541
1666
|
<Virtuoso
|
|
1542
1667
|
ref={virtuosoRef}
|
|
@@ -1544,7 +1669,11 @@ export function EventListView({
|
|
|
1544
1669
|
overscan={20}
|
|
1545
1670
|
defaultItemHeight={40}
|
|
1546
1671
|
endReached={() => {
|
|
1547
|
-
if (
|
|
1672
|
+
if (
|
|
1673
|
+
isExactSearchActive ||
|
|
1674
|
+
!hasMoreEvents ||
|
|
1675
|
+
isLoadingMoreEvents
|
|
1676
|
+
) {
|
|
1548
1677
|
return;
|
|
1549
1678
|
}
|
|
1550
1679
|
void onLoadMoreEvents?.();
|
|
@@ -1574,6 +1703,7 @@ export function EventListView({
|
|
|
1574
1703
|
onCacheEventData={cacheEventData}
|
|
1575
1704
|
encryptionKey={encryptionKey}
|
|
1576
1705
|
onEncryptedDataDetected={handleEncryptedDataDetected}
|
|
1706
|
+
suppressGroupDimming={isExactSearchActive}
|
|
1577
1707
|
/>
|
|
1578
1708
|
);
|
|
1579
1709
|
}}
|
|
@@ -1591,10 +1721,15 @@ export function EventListView({
|
|
|
1591
1721
|
}}
|
|
1592
1722
|
>
|
|
1593
1723
|
<span>
|
|
1594
|
-
{
|
|
1595
|
-
|
|
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`}
|
|
1596
1731
|
</span>
|
|
1597
|
-
{hasMoreEvents && (
|
|
1732
|
+
{!isExactSearchActive && hasMoreEvents && (
|
|
1598
1733
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
1599
1734
|
<div className="pointer-events-auto">
|
|
1600
1735
|
<LoadMoreButton
|
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
|
+
}
|