@emeryld/rrroutes-export 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/defaultViewerTemplate.d.ts +1 -1
- package/dist/exportFinalizedLeaves.bundle.d.ts +21 -0
- package/dist/index.cjs +135 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +132 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/tools/finalized-leaves-viewer.html +473 -217
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>
|
|
6
|
+
<title>RRRoutes Export Viewer</title>
|
|
7
7
|
<style>
|
|
8
8
|
:root {
|
|
9
9
|
--bg: #212121;
|
|
@@ -472,7 +472,7 @@
|
|
|
472
472
|
</head>
|
|
473
473
|
<body>
|
|
474
474
|
<div class="wrap">
|
|
475
|
-
<h1>
|
|
475
|
+
<h1>RRRoutes Export Viewer</h1>
|
|
476
476
|
<div class="card controls">
|
|
477
477
|
<div class="primary-row">
|
|
478
478
|
<label class="control-block">
|
|
@@ -529,12 +529,29 @@
|
|
|
529
529
|
</div>
|
|
530
530
|
</details>
|
|
531
531
|
|
|
532
|
+
<div class="scope-row">
|
|
533
|
+
<div class="scope-group">
|
|
534
|
+
<div class="scope-group-title">Views</div>
|
|
535
|
+
<div class="scope-group-chips">
|
|
536
|
+
<button id="tabLeaves" class="field-chip" type="button" aria-pressed="true">
|
|
537
|
+
Leaves
|
|
538
|
+
</button>
|
|
539
|
+
<button id="tabChangelog" class="field-chip" type="button" aria-pressed="false">
|
|
540
|
+
Changelog
|
|
541
|
+
</button>
|
|
542
|
+
<button id="tabChangelogSource" class="field-chip" type="button" aria-pressed="false">
|
|
543
|
+
Changelog / By Source
|
|
544
|
+
</button>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
|
|
532
549
|
<div class="active-filters-row">
|
|
533
550
|
<div class="active-filters-title">Active filters</div>
|
|
534
551
|
<div id="activeFilterChips" class="chips filters"></div>
|
|
535
552
|
</div>
|
|
536
553
|
|
|
537
|
-
<div id="status" class="meta">Load a
|
|
554
|
+
<div id="status" class="meta">Load a leaves export, changelog export, or unified bundle JSON to begin.</div>
|
|
538
555
|
</div>
|
|
539
556
|
|
|
540
557
|
<div id="results"></div>
|
|
@@ -612,6 +629,84 @@
|
|
|
612
629
|
},
|
|
613
630
|
]
|
|
614
631
|
|
|
632
|
+
const CHANGELOG_TIMELINE_FIELDS = [
|
|
633
|
+
{
|
|
634
|
+
id: 'eventKind',
|
|
635
|
+
label: 'kind',
|
|
636
|
+
get: (item) => [item.eventKind],
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
id: 'eventType',
|
|
640
|
+
label: 'event',
|
|
641
|
+
get: (item) => [item.event.type],
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
id: 'routeKey',
|
|
645
|
+
label: 'route',
|
|
646
|
+
get: (item) => [item.routeKey],
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
id: 'sourceObject',
|
|
650
|
+
label: 'source object',
|
|
651
|
+
get: (item) => [item.sourceObjectId, item.displayName],
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
id: 'commit',
|
|
655
|
+
label: 'commit',
|
|
656
|
+
get: (item) => [
|
|
657
|
+
item.event.commitSha,
|
|
658
|
+
item.commit?.authorDate,
|
|
659
|
+
item.commit?.authorName,
|
|
660
|
+
item.commit?.subject,
|
|
661
|
+
],
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
id: 'schemaDiff',
|
|
665
|
+
label: 'schema diff',
|
|
666
|
+
get: (item) =>
|
|
667
|
+
Array.isArray(item.event.schemaDiff)
|
|
668
|
+
? item.event.schemaDiff.map((delta) => JSON.stringify(delta))
|
|
669
|
+
: [],
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
id: 'cfgDiff',
|
|
673
|
+
label: 'cfg diff',
|
|
674
|
+
get: (item) =>
|
|
675
|
+
Array.isArray(item.event.cfgDiff)
|
|
676
|
+
? item.event.cfgDiff.map((delta) => JSON.stringify(delta))
|
|
677
|
+
: [],
|
|
678
|
+
},
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
const CHANGELOG_SOURCE_FIELDS = [
|
|
682
|
+
{
|
|
683
|
+
id: 'sourceObject',
|
|
684
|
+
label: 'source object',
|
|
685
|
+
get: (item) => [item.sourceId, item.displayName],
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
id: 'eventType',
|
|
689
|
+
label: 'event type',
|
|
690
|
+
get: (item) => item.events.map((event) => event.type),
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
id: 'impactedRoutes',
|
|
694
|
+
label: 'impacted routes',
|
|
695
|
+
get: (item) =>
|
|
696
|
+
item.events.flatMap((event) => (Array.isArray(event.impactedRoutes) ? event.impactedRoutes : [])),
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
id: 'schemaDiff',
|
|
700
|
+
label: 'schema diff',
|
|
701
|
+
get: (item) =>
|
|
702
|
+
item.events.flatMap((event) =>
|
|
703
|
+
Array.isArray(event.schemaDiff)
|
|
704
|
+
? event.schemaDiff.map((delta) => JSON.stringify(delta))
|
|
705
|
+
: [],
|
|
706
|
+
),
|
|
707
|
+
},
|
|
708
|
+
]
|
|
709
|
+
|
|
615
710
|
const SCHEMA_SECTIONS = ['params', 'query', 'body', 'output']
|
|
616
711
|
const SCHEMA_FIELD_IDS = new Set(SCHEMA_SECTIONS)
|
|
617
712
|
const SOURCE_FIELD_IDS = new Set(['sourceDefinition', 'sourceSchemas'])
|
|
@@ -645,11 +740,39 @@
|
|
|
645
740
|
},
|
|
646
741
|
]
|
|
647
742
|
|
|
743
|
+
const VIEW_IDS = {
|
|
744
|
+
leaves: 'leaves',
|
|
745
|
+
changelog: 'changelog',
|
|
746
|
+
changelogSource: 'changelogSource',
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const TAB_CONFIG = {
|
|
750
|
+
[VIEW_IDS.leaves]: {
|
|
751
|
+
fieldDefs: SEARCH_FIELDS,
|
|
752
|
+
defaultSelected: new Set(SEARCH_FIELDS.map((field) => field.id)),
|
|
753
|
+
},
|
|
754
|
+
[VIEW_IDS.changelog]: {
|
|
755
|
+
fieldDefs: CHANGELOG_TIMELINE_FIELDS,
|
|
756
|
+
defaultSelected: new Set(CHANGELOG_TIMELINE_FIELDS.map((field) => field.id)),
|
|
757
|
+
},
|
|
758
|
+
[VIEW_IDS.changelogSource]: {
|
|
759
|
+
fieldDefs: CHANGELOG_SOURCE_FIELDS,
|
|
760
|
+
defaultSelected: new Set(CHANGELOG_SOURCE_FIELDS.map((field) => field.id)),
|
|
761
|
+
},
|
|
762
|
+
}
|
|
763
|
+
|
|
648
764
|
const state = {
|
|
649
765
|
payload: null,
|
|
766
|
+
leavesPayload: null,
|
|
767
|
+
changelogPayload: null,
|
|
650
768
|
leaves: [],
|
|
651
|
-
|
|
652
|
-
|
|
769
|
+
activeTab: VIEW_IDS.leaves,
|
|
770
|
+
availableTabs: new Set([VIEW_IDS.leaves]),
|
|
771
|
+
selectedFieldIdsByTab: {
|
|
772
|
+
[VIEW_IDS.leaves]: new Set(TAB_CONFIG[VIEW_IDS.leaves].defaultSelected),
|
|
773
|
+
[VIEW_IDS.changelog]: new Set(TAB_CONFIG[VIEW_IDS.changelog].defaultSelected),
|
|
774
|
+
[VIEW_IDS.changelogSource]: new Set(TAB_CONFIG[VIEW_IDS.changelogSource].defaultSelected),
|
|
775
|
+
},
|
|
653
776
|
}
|
|
654
777
|
|
|
655
778
|
const fileInput = document.getElementById('fileInput')
|
|
@@ -669,6 +792,9 @@
|
|
|
669
792
|
const statusEl = document.getElementById('status')
|
|
670
793
|
const resultsEl = document.getElementById('results')
|
|
671
794
|
const controlsEl = document.querySelector('.controls')
|
|
795
|
+
const tabLeavesBtn = document.getElementById('tabLeaves')
|
|
796
|
+
const tabChangelogBtn = document.getElementById('tabChangelog')
|
|
797
|
+
const tabChangelogSourceBtn = document.getElementById('tabChangelogSource')
|
|
672
798
|
|
|
673
799
|
const URL_PARAM_KEY = 'filters'
|
|
674
800
|
let isHydratingFromUrl = false
|
|
@@ -1304,6 +1430,29 @@
|
|
|
1304
1430
|
content.appendChild(separatedSchemas)
|
|
1305
1431
|
}
|
|
1306
1432
|
|
|
1433
|
+
const routeHistory = state.changelogPayload?.byRoute?.[leaf.key]
|
|
1434
|
+
if (Array.isArray(routeHistory) && routeHistory.length > 0) {
|
|
1435
|
+
const commitBySha = new Map(
|
|
1436
|
+
(state.changelogPayload?.commits || []).map((commit) => [commit.sha, commit]),
|
|
1437
|
+
)
|
|
1438
|
+
const routeChangelogSection = el('div', 'section')
|
|
1439
|
+
const routeChangelogDetails = el('details')
|
|
1440
|
+
const routeChangelogSummary = el(
|
|
1441
|
+
'summary',
|
|
1442
|
+
'',
|
|
1443
|
+
`Route Changelog (${routeHistory.length} event${routeHistory.length === 1 ? '' : 's'})`,
|
|
1444
|
+
)
|
|
1445
|
+
routeChangelogDetails.appendChild(routeChangelogSummary)
|
|
1446
|
+
|
|
1447
|
+
const routeChangelogBody = el('div', 'leaf-content')
|
|
1448
|
+
routeHistory.forEach((event) => {
|
|
1449
|
+
routeChangelogBody.appendChild(renderEventCard(event, commitBySha))
|
|
1450
|
+
})
|
|
1451
|
+
routeChangelogDetails.appendChild(routeChangelogBody)
|
|
1452
|
+
routeChangelogSection.appendChild(routeChangelogDetails)
|
|
1453
|
+
content.appendChild(routeChangelogSection)
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1307
1456
|
details.appendChild(content)
|
|
1308
1457
|
return details
|
|
1309
1458
|
}
|
|
@@ -1316,188 +1465,193 @@
|
|
|
1316
1465
|
)
|
|
1317
1466
|
}
|
|
1318
1467
|
|
|
1319
|
-
function
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
controlsEl.style.display = 'none'
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
const commitBySha = new Map(
|
|
1329
|
-
(payload.commits || []).map((commit) => [commit.sha, commit]),
|
|
1330
|
-
)
|
|
1331
|
-
const routeEntries = Object.entries(payload.byRoute || {}).sort(
|
|
1332
|
-
([a], [b]) => a.localeCompare(b),
|
|
1333
|
-
)
|
|
1334
|
-
const sourceEntries = Object.entries(payload.bySourceObject || {}).sort(
|
|
1335
|
-
([a], [b]) => a.localeCompare(b),
|
|
1468
|
+
function isBundlePayload(value) {
|
|
1469
|
+
return (
|
|
1470
|
+
value &&
|
|
1471
|
+
value.kind === 'finalized-leaves-bundle' &&
|
|
1472
|
+
value.leavesPayload &&
|
|
1473
|
+
value.changelogPayload
|
|
1336
1474
|
)
|
|
1475
|
+
}
|
|
1337
1476
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1477
|
+
function currentFieldDefs() {
|
|
1478
|
+
return TAB_CONFIG[state.activeTab]?.fieldDefs || []
|
|
1479
|
+
}
|
|
1340
1480
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
tabs.appendChild(bySourceBtn)
|
|
1348
|
-
resultsEl.appendChild(tabs)
|
|
1349
|
-
|
|
1350
|
-
const view = el('div')
|
|
1351
|
-
resultsEl.appendChild(view)
|
|
1352
|
-
|
|
1353
|
-
const formatCommitDateTime = (value) => {
|
|
1354
|
-
if (!value) return null
|
|
1355
|
-
const parsed = new Date(value)
|
|
1356
|
-
if (Number.isNaN(parsed.getTime())) return String(value)
|
|
1357
|
-
return parsed.toLocaleString()
|
|
1358
|
-
}
|
|
1481
|
+
function formatCommitDateTime(value) {
|
|
1482
|
+
if (!value) return null
|
|
1483
|
+
const parsed = new Date(value)
|
|
1484
|
+
if (Number.isNaN(parsed.getTime())) return String(value)
|
|
1485
|
+
return parsed.toLocaleString()
|
|
1486
|
+
}
|
|
1359
1487
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
}
|
|
1370
|
-
return parts.join(', ')
|
|
1488
|
+
function formatRouteSchemaEntry(entry) {
|
|
1489
|
+
if (!entry || typeof entry !== 'object') return 'n/a'
|
|
1490
|
+
const parts = [
|
|
1491
|
+
`type=${entry.type}`,
|
|
1492
|
+
`nullable=${Boolean(entry.nullable)}`,
|
|
1493
|
+
`optional=${Boolean(entry.optional)}`,
|
|
1494
|
+
]
|
|
1495
|
+
if ('literal' in entry) {
|
|
1496
|
+
parts.push(`literal=${JSON.stringify(entry.literal)}`)
|
|
1371
1497
|
}
|
|
1498
|
+
return parts.join(', ')
|
|
1499
|
+
}
|
|
1372
1500
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
if ('
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
return `added ${location}: ${JSON.stringify(delta.after || [])}`
|
|
1381
|
-
}
|
|
1382
|
-
if (delta.op === 'removed') {
|
|
1383
|
-
return `removed ${location}: ${JSON.stringify(delta.before || [])}`
|
|
1384
|
-
}
|
|
1385
|
-
return `changed ${location}: ${JSON.stringify(delta.before || [])} -> ${JSON.stringify(
|
|
1386
|
-
delta.after || [],
|
|
1387
|
-
)}`
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// Route schema diff shape: { path, op, before?, after? }
|
|
1391
|
-
if ('path' in delta) {
|
|
1392
|
-
if (delta.op === 'added') {
|
|
1393
|
-
return `added ${delta.path}: ${formatRouteSchemaEntry(delta.after)}`
|
|
1394
|
-
}
|
|
1395
|
-
if (delta.op === 'removed') {
|
|
1396
|
-
return `removed ${delta.path}: ${formatRouteSchemaEntry(delta.before)}`
|
|
1397
|
-
}
|
|
1398
|
-
return `changed ${delta.path}: ${formatRouteSchemaEntry(
|
|
1399
|
-
delta.before,
|
|
1400
|
-
)} -> ${formatRouteSchemaEntry(delta.after)}`
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
return null
|
|
1501
|
+
function formatSchemaDiffLine(delta) {
|
|
1502
|
+
if (!delta || typeof delta !== 'object') return null
|
|
1503
|
+
if ('section' in delta) {
|
|
1504
|
+
const location = `${delta.section}.${delta.path}`
|
|
1505
|
+
if (delta.op === 'added') return `added ${location}: ${JSON.stringify(delta.after || [])}`
|
|
1506
|
+
if (delta.op === 'removed') return `removed ${location}: ${JSON.stringify(delta.before || [])}`
|
|
1507
|
+
return `changed ${location}: ${JSON.stringify(delta.before || [])} -> ${JSON.stringify(delta.after || [])}`
|
|
1404
1508
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
if (
|
|
1408
|
-
return
|
|
1509
|
+
if ('path' in delta) {
|
|
1510
|
+
if (delta.op === 'added') return `added ${delta.path}: ${formatRouteSchemaEntry(delta.after)}`
|
|
1511
|
+
if (delta.op === 'removed') return `removed ${delta.path}: ${formatRouteSchemaEntry(delta.before)}`
|
|
1512
|
+
return `changed ${delta.path}: ${formatRouteSchemaEntry(delta.before)} -> ${formatRouteSchemaEntry(delta.after)}`
|
|
1409
1513
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
)
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1514
|
+
return null
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function formatCfgDiffLine(diff) {
|
|
1518
|
+
if (!diff || typeof diff !== 'object') return null
|
|
1519
|
+
return `${diff.field}: ${JSON.stringify(diff.before)} -> ${JSON.stringify(diff.after)}`
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function renderEventCard(event, commitBySha) {
|
|
1523
|
+
const card = el('div', 'leaf-content')
|
|
1524
|
+
const commit = commitBySha.get(event.commitSha)
|
|
1525
|
+
const commitDateTime = formatCommitDateTime(commit?.authorDate)
|
|
1526
|
+
const commitMeta = [
|
|
1527
|
+
event.commitSha.slice(0, 12),
|
|
1528
|
+
commitDateTime,
|
|
1529
|
+
commit?.authorName || null,
|
|
1530
|
+
commit?.subject || null,
|
|
1531
|
+
]
|
|
1532
|
+
.filter(Boolean)
|
|
1533
|
+
.join(' - ')
|
|
1534
|
+
card.appendChild(el('div', 'meta', commitMeta))
|
|
1535
|
+
card.appendChild(el('div', 'mono', event.type))
|
|
1536
|
+
if (Array.isArray(event.schemaDiff) && event.schemaDiff.length > 0) {
|
|
1537
|
+
card.appendChild(el('div', 'meta', `schema changes: ${event.schemaDiff.length}`))
|
|
1538
|
+
const schemaChanges = el('div')
|
|
1539
|
+
event.schemaDiff.forEach((delta) => {
|
|
1540
|
+
const line = formatSchemaDiffLine(delta)
|
|
1541
|
+
if (!line) return
|
|
1542
|
+
schemaChanges.appendChild(el('div', 'meta', line))
|
|
1543
|
+
})
|
|
1544
|
+
card.appendChild(schemaChanges)
|
|
1545
|
+
}
|
|
1546
|
+
if (Array.isArray(event.cfgDiff) && event.cfgDiff.length > 0) {
|
|
1547
|
+
card.appendChild(el('div', 'meta', `cfg changes: ${event.cfgDiff.length}`))
|
|
1548
|
+
const cfgChanges = el('div')
|
|
1549
|
+
event.cfgDiff.forEach((diff) => {
|
|
1550
|
+
const line = formatCfgDiffLine(diff)
|
|
1551
|
+
if (!line) return
|
|
1552
|
+
cfgChanges.appendChild(el('div', 'meta', line))
|
|
1553
|
+
})
|
|
1554
|
+
card.appendChild(cfgChanges)
|
|
1555
|
+
}
|
|
1556
|
+
if (Array.isArray(event.impactedRoutes) && event.impactedRoutes.length > 0) {
|
|
1557
|
+
card.appendChild(el('div', 'meta', `impacted routes: ${event.impactedRoutes.length}`))
|
|
1558
|
+
}
|
|
1559
|
+
return card
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function buildChronologicalEvents(payload) {
|
|
1563
|
+
if (!payload) return []
|
|
1564
|
+
const commitBySha = new Map((payload.commits || []).map((commit) => [commit.sha, commit]))
|
|
1565
|
+
const commitOrder = new Map((payload.commits || []).map((commit, index) => [commit.sha, index]))
|
|
1566
|
+
const rows = []
|
|
1567
|
+
let sequence = 0
|
|
1568
|
+
|
|
1569
|
+
Object.entries(payload.byRoute || {}).forEach(([routeKey, events]) => {
|
|
1570
|
+
;(events || []).forEach((event) => {
|
|
1571
|
+
rows.push({
|
|
1572
|
+
eventKind: 'route',
|
|
1573
|
+
routeKey,
|
|
1574
|
+
event,
|
|
1575
|
+
commit: commitBySha.get(event.commitSha),
|
|
1576
|
+
commitIndex: commitOrder.get(event.commitSha) ?? Number.MAX_SAFE_INTEGER,
|
|
1577
|
+
eventIndex: sequence++,
|
|
1438
1578
|
})
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1579
|
+
})
|
|
1580
|
+
})
|
|
1581
|
+
|
|
1582
|
+
Object.entries(payload.bySourceObject || {}).forEach(([sourceObjectId, events]) => {
|
|
1583
|
+
;(events || []).forEach((event) => {
|
|
1584
|
+
rows.push({
|
|
1585
|
+
eventKind: 'source',
|
|
1586
|
+
sourceObjectId,
|
|
1587
|
+
displayName: event.displayName || sourceObjectId,
|
|
1588
|
+
event,
|
|
1589
|
+
commit: commitBySha.get(event.commitSha),
|
|
1590
|
+
commitIndex: commitOrder.get(event.commitSha) ?? Number.MAX_SAFE_INTEGER,
|
|
1591
|
+
eventIndex: sequence++,
|
|
1448
1592
|
})
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (Array.isArray(event.impactedRoutes) && event.impactedRoutes.length > 0) {
|
|
1452
|
-
card.appendChild(
|
|
1453
|
-
el('div', 'meta', `impacted routes: ${event.impactedRoutes.length}`),
|
|
1454
|
-
)
|
|
1455
|
-
}
|
|
1456
|
-
return card
|
|
1457
|
-
}
|
|
1593
|
+
})
|
|
1594
|
+
})
|
|
1458
1595
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
return
|
|
1464
|
-
}
|
|
1596
|
+
rows.sort((a, b) => {
|
|
1597
|
+
if (a.commitIndex !== b.commitIndex) return a.commitIndex - b.commitIndex
|
|
1598
|
+
return a.eventIndex - b.eventIndex
|
|
1599
|
+
})
|
|
1465
1600
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1601
|
+
return rows
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function changelogItemsByTab(tabId) {
|
|
1605
|
+
const payload = state.changelogPayload
|
|
1606
|
+
if (!payload) return []
|
|
1607
|
+
if (tabId === VIEW_IDS.changelog) {
|
|
1608
|
+
return buildChronologicalEvents(payload)
|
|
1609
|
+
}
|
|
1610
|
+
if (tabId === VIEW_IDS.changelogSource) {
|
|
1611
|
+
return Object.entries(payload.bySourceObject || {})
|
|
1612
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
1613
|
+
.map(([sourceId, events]) => ({
|
|
1614
|
+
sourceId,
|
|
1615
|
+
events,
|
|
1616
|
+
displayName: events[0]?.displayName || sourceId,
|
|
1617
|
+
}))
|
|
1472
1618
|
}
|
|
1619
|
+
return []
|
|
1620
|
+
}
|
|
1473
1621
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1622
|
+
function matchesGenericItem(item, fieldDefs, engine, selectedIds) {
|
|
1623
|
+
if (!engine.active) return true
|
|
1624
|
+
return fieldDefs.some((field) => {
|
|
1625
|
+
if (selectedIds.length > 0 && !selectedIds.includes(field.id)) return false
|
|
1626
|
+
const tokens = toTokens(field.get(item))
|
|
1627
|
+
return tokens.some((token) => engine.test(token))
|
|
1628
|
+
})
|
|
1629
|
+
}
|
|
1482
1630
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
events.forEach((event) => details.appendChild(renderEventDetails(event)))
|
|
1488
|
-
view.appendChild(details)
|
|
1489
|
-
})
|
|
1631
|
+
function renderCollection(items, renderItem, emptyMessage) {
|
|
1632
|
+
if (items.length === 0) {
|
|
1633
|
+
resultsEl.appendChild(el('div', 'empty', emptyMessage))
|
|
1634
|
+
return
|
|
1490
1635
|
}
|
|
1636
|
+
items.forEach((item) => {
|
|
1637
|
+
resultsEl.appendChild(renderItem(item))
|
|
1638
|
+
})
|
|
1639
|
+
}
|
|
1491
1640
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1641
|
+
function renderTabButtons() {
|
|
1642
|
+
setPressed(tabLeavesBtn, state.activeTab === VIEW_IDS.leaves)
|
|
1643
|
+
setPressed(tabChangelogBtn, state.activeTab === VIEW_IDS.changelog)
|
|
1644
|
+
setPressed(tabChangelogSourceBtn, state.activeTab === VIEW_IDS.changelogSource)
|
|
1645
|
+
tabLeavesBtn.style.display = state.availableTabs.has(VIEW_IDS.leaves) ? '' : 'none'
|
|
1646
|
+
tabChangelogBtn.style.display = state.availableTabs.has(VIEW_IDS.changelog) ? '' : 'none'
|
|
1647
|
+
tabChangelogSourceBtn.style.display = state.availableTabs.has(VIEW_IDS.changelogSource) ? '' : 'none'
|
|
1648
|
+
const isLeavesTab = state.activeTab === VIEW_IDS.leaves
|
|
1649
|
+
coreFieldsBtn.style.display = isLeavesTab ? '' : 'none'
|
|
1650
|
+
schemasOnlyFieldsBtn.style.display = isLeavesTab ? '' : 'none'
|
|
1651
|
+
sourceOnlyFieldsBtn.style.display = isLeavesTab ? '' : 'none'
|
|
1495
1652
|
}
|
|
1496
1653
|
|
|
1497
1654
|
function renderResults() {
|
|
1498
|
-
if (state.mode === 'changelog') {
|
|
1499
|
-
return
|
|
1500
|
-
}
|
|
1501
1655
|
const engine = createSearchEngine(searchInput.value.trim(), {
|
|
1502
1656
|
caseSensitive: isPressed(caseSensitiveToggle),
|
|
1503
1657
|
regex: isPressed(regexSearchToggle),
|
|
@@ -1515,51 +1669,88 @@
|
|
|
1515
1669
|
}
|
|
1516
1670
|
|
|
1517
1671
|
const selectedIds = selectedFieldIds()
|
|
1518
|
-
const filtered = state.leaves.filter((leaf) => matchesLeaf(leaf, engine, selectedIds))
|
|
1519
|
-
|
|
1520
|
-
statusEl.textContent = `${filtered.length} / ${state.leaves.length} routes matched.`
|
|
1521
1672
|
resultsEl.innerHTML = ''
|
|
1673
|
+
renderTabButtons()
|
|
1522
1674
|
renderActiveFilterChips({ selectedIds, hasRegexError: false })
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1675
|
+
if (state.activeTab === VIEW_IDS.leaves) {
|
|
1676
|
+
const filtered = state.leaves.filter((leaf) => matchesLeaf(leaf, engine, selectedIds))
|
|
1677
|
+
statusEl.textContent = `${filtered.length} / ${state.leaves.length} routes matched.`
|
|
1678
|
+
renderCollection(filtered, (leaf) => renderLeaf(leaf, engine, selectedIds), 'No matches.')
|
|
1679
|
+
} else {
|
|
1680
|
+
const fieldDefs = currentFieldDefs()
|
|
1681
|
+
const items = changelogItemsByTab(state.activeTab)
|
|
1682
|
+
const filtered = items.filter((item) => matchesGenericItem(item, fieldDefs, engine, selectedIds))
|
|
1683
|
+
statusEl.textContent =
|
|
1684
|
+
state.activeTab === VIEW_IDS.changelog
|
|
1685
|
+
? `${filtered.length} / ${items.length} changelog events matched.`
|
|
1686
|
+
: `${filtered.length} / ${items.length} source changelog groups matched.`
|
|
1687
|
+
const commitBySha = new Map(
|
|
1688
|
+
(state.changelogPayload?.commits || []).map((commit) => [commit.sha, commit]),
|
|
1689
|
+
)
|
|
1690
|
+
renderCollection(
|
|
1691
|
+
filtered,
|
|
1692
|
+
(item) => {
|
|
1693
|
+
const details = el('details', 'leaf')
|
|
1694
|
+
const title =
|
|
1695
|
+
state.activeTab === VIEW_IDS.changelog
|
|
1696
|
+
? `${item.eventKind === 'route' ? `Route: ${item.routeKey}` : `Source: ${item.displayName || item.sourceObjectId}`} (${item.event.type})`
|
|
1697
|
+
: `${item.displayName} (${item.events.length})`
|
|
1698
|
+
details.appendChild(el('summary', '', title))
|
|
1699
|
+
if (state.activeTab === VIEW_IDS.changelog) {
|
|
1700
|
+
details.appendChild(renderEventCard(item.event, commitBySha))
|
|
1701
|
+
} else {
|
|
1702
|
+
item.events.forEach((event) => details.appendChild(renderEventCard(event, commitBySha)))
|
|
1703
|
+
}
|
|
1704
|
+
return details
|
|
1705
|
+
},
|
|
1706
|
+
'No matches.',
|
|
1707
|
+
)
|
|
1528
1708
|
}
|
|
1529
|
-
|
|
1530
|
-
filtered.forEach((leaf) => {
|
|
1531
|
-
resultsEl.appendChild(renderLeaf(leaf, engine, selectedIds))
|
|
1532
|
-
})
|
|
1533
1709
|
syncFilterStateToUrl()
|
|
1534
1710
|
}
|
|
1535
1711
|
|
|
1536
1712
|
function selectedFieldIds() {
|
|
1537
|
-
|
|
1713
|
+
const selected = state.selectedFieldIdsByTab[state.activeTab] || new Set()
|
|
1714
|
+
return currentFieldDefs().filter((field) => selected.has(field.id)).map(
|
|
1538
1715
|
(field) => field.id,
|
|
1539
1716
|
)
|
|
1540
1717
|
}
|
|
1541
1718
|
|
|
1542
1719
|
function renderFieldChips() {
|
|
1720
|
+
const fieldDefs = currentFieldDefs()
|
|
1721
|
+
const selected = state.selectedFieldIdsByTab[state.activeTab] || new Set()
|
|
1543
1722
|
fieldChipGroups.innerHTML = ''
|
|
1544
|
-
|
|
1723
|
+
const groups =
|
|
1724
|
+
state.activeTab === VIEW_IDS.leaves
|
|
1725
|
+
? FIELD_GROUPS
|
|
1726
|
+
: [
|
|
1727
|
+
{
|
|
1728
|
+
id: 'tabFields',
|
|
1729
|
+
label: state.activeTab === VIEW_IDS.changelog ? 'Changelog fields' : 'Source fields',
|
|
1730
|
+
fieldIds: fieldDefs.map((field) => field.id),
|
|
1731
|
+
},
|
|
1732
|
+
]
|
|
1733
|
+
|
|
1734
|
+
groups.forEach((group) => {
|
|
1545
1735
|
const groupWrap = el('div', 'scope-group')
|
|
1546
1736
|
groupWrap.appendChild(el('div', 'scope-group-title', group.label))
|
|
1547
1737
|
const chipsWrap = el('div', 'scope-group-chips')
|
|
1548
1738
|
|
|
1549
1739
|
group.fieldIds.forEach((fieldId) => {
|
|
1550
|
-
const field =
|
|
1740
|
+
const field = fieldDefs.find((item) => item.id === fieldId)
|
|
1551
1741
|
if (!field) return
|
|
1552
1742
|
const chip = document.createElement('button')
|
|
1553
1743
|
chip.type = 'button'
|
|
1554
1744
|
chip.className = 'field-chip'
|
|
1555
1745
|
chip.textContent = field.label
|
|
1556
|
-
setPressed(chip,
|
|
1746
|
+
setPressed(chip, selected.has(field.id))
|
|
1557
1747
|
chip.addEventListener('click', () => {
|
|
1558
|
-
if (
|
|
1559
|
-
|
|
1748
|
+
if (selected.has(field.id)) {
|
|
1749
|
+
selected.delete(field.id)
|
|
1560
1750
|
} else {
|
|
1561
|
-
|
|
1751
|
+
selected.add(field.id)
|
|
1562
1752
|
}
|
|
1753
|
+
state.selectedFieldIdsByTab[state.activeTab] = selected
|
|
1563
1754
|
renderFieldChips()
|
|
1564
1755
|
renderResults()
|
|
1565
1756
|
})
|
|
@@ -1573,8 +1764,8 @@
|
|
|
1573
1764
|
|
|
1574
1765
|
function setFieldSelection(allowedIds, options = {}) {
|
|
1575
1766
|
const { rerender = true } = options
|
|
1576
|
-
state.
|
|
1577
|
-
|
|
1767
|
+
state.selectedFieldIdsByTab[state.activeTab] = new Set(
|
|
1768
|
+
currentFieldDefs().map((field) => field.id).filter((id) => allowedIds.has(id)),
|
|
1578
1769
|
)
|
|
1579
1770
|
renderFieldChips()
|
|
1580
1771
|
if (rerender) renderResults()
|
|
@@ -1586,7 +1777,7 @@
|
|
|
1586
1777
|
setPressed(regexSearchToggle, false)
|
|
1587
1778
|
typeMatchModeInput.value = 'contains'
|
|
1588
1779
|
setPressed(schemaRowsMatchOnlyToggle, false)
|
|
1589
|
-
setFieldSelection(new Set(
|
|
1780
|
+
setFieldSelection(new Set(currentFieldDefs().map((field) => field.id)), { rerender: false })
|
|
1590
1781
|
renderResults()
|
|
1591
1782
|
}
|
|
1592
1783
|
|
|
@@ -1642,7 +1833,7 @@
|
|
|
1642
1833
|
}
|
|
1643
1834
|
if (hasRegexError) chipModels.push({ text: 'regex error' })
|
|
1644
1835
|
|
|
1645
|
-
const allIds =
|
|
1836
|
+
const allIds = currentFieldDefs().map((field) => field.id)
|
|
1646
1837
|
if (selectedIds.length !== allIds.length) {
|
|
1647
1838
|
chipModels.push({
|
|
1648
1839
|
text: `fields: ${selectedIds.join(', ') || 'none'}`,
|
|
@@ -1669,12 +1860,15 @@
|
|
|
1669
1860
|
|
|
1670
1861
|
function collectFilterState() {
|
|
1671
1862
|
return {
|
|
1863
|
+
activeTab: state.activeTab,
|
|
1672
1864
|
search: searchInput.value,
|
|
1673
1865
|
caseSensitive: isPressed(caseSensitiveToggle),
|
|
1674
1866
|
regex: isPressed(regexSearchToggle),
|
|
1675
1867
|
typeMatchMode: typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
|
|
1676
1868
|
schemaRowsMatchOnly: isPressed(schemaRowsMatchOnlyToggle),
|
|
1677
|
-
|
|
1869
|
+
selectedFieldsByTab: Object.fromEntries(
|
|
1870
|
+
Object.entries(state.selectedFieldIdsByTab).map(([key, value]) => [key, Array.from(value)]),
|
|
1871
|
+
),
|
|
1678
1872
|
}
|
|
1679
1873
|
}
|
|
1680
1874
|
|
|
@@ -1699,6 +1893,16 @@
|
|
|
1699
1893
|
try {
|
|
1700
1894
|
const parsed = JSON.parse(decodeURIComponent(raw))
|
|
1701
1895
|
isHydratingFromUrl = true
|
|
1896
|
+
if (
|
|
1897
|
+
parsed.activeTab === VIEW_IDS.leaves ||
|
|
1898
|
+
parsed.activeTab === VIEW_IDS.changelog ||
|
|
1899
|
+
parsed.activeTab === VIEW_IDS.changelogSource
|
|
1900
|
+
) {
|
|
1901
|
+
state.activeTab = parsed.activeTab
|
|
1902
|
+
}
|
|
1903
|
+
if (parsed.activeTab === 'changelogRoute') {
|
|
1904
|
+
state.activeTab = VIEW_IDS.changelog
|
|
1905
|
+
}
|
|
1702
1906
|
if (typeof parsed.search === 'string') searchInput.value = parsed.search
|
|
1703
1907
|
setPressed(caseSensitiveToggle, Boolean(parsed.caseSensitive))
|
|
1704
1908
|
setPressed(regexSearchToggle, Boolean(parsed.regex))
|
|
@@ -1707,8 +1911,12 @@
|
|
|
1707
1911
|
}
|
|
1708
1912
|
setPressed(schemaRowsMatchOnlyToggle, Boolean(parsed.schemaRowsMatchOnly))
|
|
1709
1913
|
|
|
1710
|
-
if (
|
|
1711
|
-
|
|
1914
|
+
if (parsed.selectedFieldsByTab && typeof parsed.selectedFieldsByTab === 'object') {
|
|
1915
|
+
Object.entries(parsed.selectedFieldsByTab).forEach(([tabId, ids]) => {
|
|
1916
|
+
if (!Array.isArray(ids)) return
|
|
1917
|
+
if (!state.selectedFieldIdsByTab[tabId]) return
|
|
1918
|
+
state.selectedFieldIdsByTab[tabId] = new Set(ids)
|
|
1919
|
+
})
|
|
1712
1920
|
}
|
|
1713
1921
|
} catch (error) {
|
|
1714
1922
|
// Ignore malformed hash state.
|
|
@@ -1720,40 +1928,84 @@
|
|
|
1720
1928
|
async function handleFile(file) {
|
|
1721
1929
|
const text = await file.text()
|
|
1722
1930
|
const parsed = JSON.parse(text)
|
|
1723
|
-
if (
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1931
|
+
if (
|
|
1932
|
+
!isBundlePayload(parsed) &&
|
|
1933
|
+
!isChangelogPayload(parsed) &&
|
|
1934
|
+
(!parsed || !Array.isArray(parsed.leaves))
|
|
1935
|
+
) {
|
|
1728
1936
|
throw new Error(
|
|
1729
|
-
'Invalid export file: expected
|
|
1937
|
+
'Invalid export file: expected leaves payload, changelog payload, or unified bundle.',
|
|
1730
1938
|
)
|
|
1731
1939
|
}
|
|
1940
|
+
applyLoadedPayload(parsed)
|
|
1941
|
+
renderResults()
|
|
1942
|
+
}
|
|
1732
1943
|
|
|
1733
|
-
|
|
1734
|
-
if (
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
state.payload = parsed
|
|
1738
|
-
state.leaves = parsed.leaves
|
|
1944
|
+
function setActiveTab(nextTab) {
|
|
1945
|
+
if (!state.availableTabs.has(nextTab)) return
|
|
1946
|
+
state.activeTab = nextTab
|
|
1947
|
+
renderFieldChips()
|
|
1739
1948
|
renderResults()
|
|
1740
1949
|
}
|
|
1741
1950
|
|
|
1951
|
+
function applyLoadedPayload(payload) {
|
|
1952
|
+
state.payload = payload
|
|
1953
|
+
state.availableTabs = new Set()
|
|
1954
|
+
state.leavesPayload = null
|
|
1955
|
+
state.changelogPayload = null
|
|
1956
|
+
state.leaves = []
|
|
1957
|
+
|
|
1958
|
+
if (isBundlePayload(payload)) {
|
|
1959
|
+
state.leavesPayload = payload.leavesPayload
|
|
1960
|
+
state.changelogPayload = payload.changelogPayload
|
|
1961
|
+
} else if (isChangelogPayload(payload)) {
|
|
1962
|
+
state.changelogPayload = payload
|
|
1963
|
+
} else if (payload && Array.isArray(payload.leaves)) {
|
|
1964
|
+
state.leavesPayload = payload
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
if (state.leavesPayload) {
|
|
1968
|
+
state.availableTabs.add(VIEW_IDS.leaves)
|
|
1969
|
+
state.leaves = state.leavesPayload.leaves || []
|
|
1970
|
+
}
|
|
1971
|
+
if (state.changelogPayload) {
|
|
1972
|
+
state.availableTabs.add(VIEW_IDS.changelog)
|
|
1973
|
+
state.availableTabs.add(VIEW_IDS.changelogSource)
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (!state.availableTabs.has(state.activeTab)) {
|
|
1977
|
+
state.activeTab = state.availableTabs.has(VIEW_IDS.leaves)
|
|
1978
|
+
? VIEW_IDS.leaves
|
|
1979
|
+
: state.availableTabs.has(VIEW_IDS.changelog)
|
|
1980
|
+
? VIEW_IDS.changelog
|
|
1981
|
+
: VIEW_IDS.changelogSource
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
if (state.leavesPayload && state.changelogPayload) {
|
|
1985
|
+
statusEl.textContent =
|
|
1986
|
+
`Loaded unified bundle with ${state.leaves.length} routes and ${
|
|
1987
|
+
state.changelogPayload.commits?.length || 0
|
|
1988
|
+
} commits.`
|
|
1989
|
+
} else if (state.leavesPayload) {
|
|
1990
|
+
statusEl.textContent = `Loaded payload with ${state.leaves.length} routes.`
|
|
1991
|
+
} else if (state.changelogPayload) {
|
|
1992
|
+
statusEl.textContent =
|
|
1993
|
+
`Loaded changelog payload with ${state.changelogPayload.commits?.length || 0} commits.`
|
|
1994
|
+
} else {
|
|
1995
|
+
statusEl.textContent = 'No payload loaded.'
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1742
1999
|
function initializeFromBakedPayload() {
|
|
1743
2000
|
const baked = window.__FINALIZED_LEAVES_PAYLOAD
|
|
1744
|
-
if (
|
|
1745
|
-
|
|
2001
|
+
if (
|
|
2002
|
+
!isBundlePayload(baked) &&
|
|
2003
|
+
!isChangelogPayload(baked) &&
|
|
2004
|
+
(!baked || !Array.isArray(baked.leaves))
|
|
2005
|
+
) {
|
|
1746
2006
|
return
|
|
1747
2007
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
state.mode = 'snapshot'
|
|
1751
|
-
if (controlsEl) {
|
|
1752
|
-
controlsEl.style.display = ''
|
|
1753
|
-
}
|
|
1754
|
-
state.payload = baked
|
|
1755
|
-
state.leaves = baked.leaves
|
|
1756
|
-
statusEl.textContent = `Loaded baked payload with ${state.leaves.length} routes.`
|
|
2008
|
+
applyLoadedPayload(baked)
|
|
1757
2009
|
renderResults()
|
|
1758
2010
|
}
|
|
1759
2011
|
|
|
@@ -1784,8 +2036,12 @@
|
|
|
1784
2036
|
renderResults()
|
|
1785
2037
|
})
|
|
1786
2038
|
|
|
2039
|
+
tabLeavesBtn.addEventListener('click', () => setActiveTab(VIEW_IDS.leaves))
|
|
2040
|
+
tabChangelogBtn.addEventListener('click', () => setActiveTab(VIEW_IDS.changelog))
|
|
2041
|
+
tabChangelogSourceBtn.addEventListener('click', () => setActiveTab(VIEW_IDS.changelogSource))
|
|
2042
|
+
|
|
1787
2043
|
selectAllFieldsBtn.addEventListener('click', () =>
|
|
1788
|
-
setFieldSelection(new Set(
|
|
2044
|
+
setFieldSelection(new Set(currentFieldDefs().map((field) => field.id))),
|
|
1789
2045
|
)
|
|
1790
2046
|
clearAllFieldsBtn.addEventListener('click', () => setFieldSelection(new Set()))
|
|
1791
2047
|
coreFieldsBtn.addEventListener('click', () => setFieldSelection(CORE_FIELD_IDS))
|