@intentius/behold 0.27.0 → 0.28.0
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/cli.js +132 -23
- package/package.json +4 -4
- package/web/app.js +17 -15
- package/web/edge-kind.js +31 -0
- package/web/edge-kind.test.js +31 -1
package/dist/cli.js
CHANGED
|
@@ -9133,6 +9133,11 @@ var init_workspace_kinds_schema = __esm({
|
|
|
9133
9133
|
},
|
|
9134
9134
|
fields: {
|
|
9135
9135
|
$ref: "#/$defs/fields"
|
|
9136
|
+
},
|
|
9137
|
+
reader: {
|
|
9138
|
+
type: "string",
|
|
9139
|
+
pattern: "^[a-z][a-z0-9-]{0,39}$",
|
|
9140
|
+
description: "A link reader chant ships, by name, that derives a member's links from the member's own files (#3932): dbt-project reads a dbt project's sources. The reader is chant's code and runs offline, parsing files as data; the package supplies only the name. A name this chant doesn't have keeps the kind without a reader, and the load reports it. A chant older than this field fails a kinds file that uses it."
|
|
9136
9141
|
}
|
|
9137
9142
|
},
|
|
9138
9143
|
patternProperties: {
|
|
@@ -15776,7 +15781,9 @@ function estateFace(irIn, boxesIn, opts = {}) {
|
|
|
15776
15781
|
const face = cardFaces(irIn, boxesIn, opts.prefixes ? { prefixes: opts.prefixes } : {});
|
|
15777
15782
|
const ir = face.ir;
|
|
15778
15783
|
if (Object.keys(face.overrides).length) faceOverrides.set(ir, face.overrides);
|
|
15779
|
-
const
|
|
15784
|
+
const toDisplay = (record) => record ? Object.fromEntries(Object.entries(record).map(([key, ids]) => [key, ids.map((id) => face.display.get(id) ?? id)])) : void 0;
|
|
15785
|
+
const boxes = toDisplay(boxesIn);
|
|
15786
|
+
const subgroups = boxes ? toDisplay(opts.subgroups) : void 0;
|
|
15780
15787
|
const realBand = moduleBandOf(irIn);
|
|
15781
15788
|
const toReal = new Map(ir.nodes.map((n, i) => [n.id, irIn.nodes[i].id]));
|
|
15782
15789
|
const bandOf = realBand ? (id) => realBand(toReal.get(id) ?? id) : void 0;
|
|
@@ -15786,13 +15793,14 @@ function estateFace(irIn, boxesIn, opts = {}) {
|
|
|
15786
15793
|
...opts.nodesep !== void 0 ? { nodesep: opts.nodesep } : {},
|
|
15787
15794
|
...opts.ranksep !== void 0 ? { ranksep: opts.ranksep } : {}
|
|
15788
15795
|
}) : { ir, grids: /* @__PURE__ */ new Map() };
|
|
15789
|
-
return { ir, overrides: face.overrides, boxes, display: face.display, bandOf, chains, restore: face.restore };
|
|
15796
|
+
return { ir, overrides: face.overrides, boxes, subgroups, display: face.display, bandOf, chains, restore: face.restore };
|
|
15790
15797
|
}
|
|
15791
15798
|
function packEstateBoxes(layout, face, opts = {}) {
|
|
15792
15799
|
const boxes = face.boxes;
|
|
15793
15800
|
if (!boxes) return;
|
|
15794
|
-
const boxGrids = packBoxComponents(layout, face.ir, boxes, face.bandOf);
|
|
15795
|
-
if (opts.members) packMemberBoxes(layout, face.ir, boxes);
|
|
15801
|
+
const boxGrids = packBoxComponents(layout, face.ir, boxes, face.bandOf, face.subgroups);
|
|
15802
|
+
if (opts.members) packMemberBoxes(layout, face.ir, { ...topLevelSubgroups(layout.groups, face.subgroups), ...boxes });
|
|
15803
|
+
if (face.subgroups) fitNestedBoxes(layout, face.ir, boxes, face.subgroups);
|
|
15796
15804
|
addBandBoxes(layout, new Map([...boxGrids, ...face.chains.grids]), footprints(face.ir, face.overrides));
|
|
15797
15805
|
applyBadges(layout.groups, opts.badges, opts.marks);
|
|
15798
15806
|
}
|
|
@@ -15826,17 +15834,27 @@ function renderGraph(irIn, opts = {}) {
|
|
|
15826
15834
|
const groupsIn = irIn.groups;
|
|
15827
15835
|
const pipelines = groupsIn.byPipeline && Object.keys(groupsIn.byPipeline).length ? groupsIn.byPipeline : void 0;
|
|
15828
15836
|
const boxKey = groupsIn.byWave ? "byWave" : opts.boxes ?? (pipelines ? "byPipeline" : void 0);
|
|
15829
|
-
const
|
|
15837
|
+
const nested = boxKey === "byStack" ? pipelines : void 0;
|
|
15838
|
+
const face = estateFace(irIn, boxKey ? groupsIn[boxKey] : void 0, nested ? { subgroups: nested } : {});
|
|
15830
15839
|
const ir = face.ir;
|
|
15831
15840
|
const boxes = face.boxes;
|
|
15832
15841
|
const chains = face.chains;
|
|
15833
|
-
const layout = layoutIr(chains.ir, {
|
|
15842
|
+
const layout = layoutIr(chains.ir, {
|
|
15843
|
+
fit: true,
|
|
15844
|
+
overrides: face.overrides,
|
|
15845
|
+
...boxes ? { groups: boxes } : {},
|
|
15846
|
+
...face.subgroups ? { subgroups: face.subgroups } : {}
|
|
15847
|
+
});
|
|
15834
15848
|
if (opts.radial && !boxes) radializeLayout(layout, groupKeyByNode(ir), footprints(ir, face.overrides));
|
|
15835
15849
|
else if (!boxes) packComponents(layout, ir);
|
|
15836
15850
|
packEstateBoxes(layout, face, { members: boxKey === "byStack", ...opts.groupBadges ? { badges: opts.groupBadges } : {} });
|
|
15837
|
-
if (boxKey === "byPipeline" && pipelines) {
|
|
15851
|
+
if ((boxKey === "byPipeline" || nested) && pipelines) {
|
|
15838
15852
|
const titles = pipelineTitles(irIn, pipelines);
|
|
15839
|
-
for (const box of layout.groups ?? [])
|
|
15853
|
+
for (const box of layout.groups ?? []) {
|
|
15854
|
+
if (box.id === void 0 || titles[box.id] === void 0) continue;
|
|
15855
|
+
const title = titles[box.id];
|
|
15856
|
+
box.title = title === box.id && box.parent !== void 0 && title.startsWith(`${box.parent}/`) ? title.slice(box.parent.length + 1) : title;
|
|
15857
|
+
}
|
|
15840
15858
|
}
|
|
15841
15859
|
const svg = renderSvg(ir, layout, {
|
|
15842
15860
|
fit: true,
|
|
@@ -15849,9 +15867,16 @@ function renderGraph(irIn, opts = {}) {
|
|
|
15849
15867
|
}
|
|
15850
15868
|
function pipelineTitles(ir, byPipeline) {
|
|
15851
15869
|
const nodes = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
15870
|
+
const workflowOf = (ids) => {
|
|
15871
|
+
const workflows = ids.flatMap((id) => {
|
|
15872
|
+
const n = nodes.get(id);
|
|
15873
|
+
return n && /::Workflow$/.test(n.kind) ? [n] : [];
|
|
15874
|
+
});
|
|
15875
|
+
return workflows.length === 1 ? workflows[0] : void 0;
|
|
15876
|
+
};
|
|
15852
15877
|
return Object.fromEntries(
|
|
15853
|
-
Object.
|
|
15854
|
-
const name = nodes.get(key)?.attrs?.["name"];
|
|
15878
|
+
Object.entries(byPipeline).map(([key, ids]) => {
|
|
15879
|
+
const name = (nodes.get(key) ?? workflowOf(ids))?.attrs?.["name"];
|
|
15855
15880
|
return [key, typeof name === "string" && name.trim() ? name : key];
|
|
15856
15881
|
})
|
|
15857
15882
|
);
|
|
@@ -15901,6 +15926,39 @@ function addBandBoxes(layout, grids, sizes) {
|
|
|
15901
15926
|
boxes.push(...added);
|
|
15902
15927
|
}
|
|
15903
15928
|
var BAND_PAD = 14;
|
|
15929
|
+
var NEST_PAD_X = 50;
|
|
15930
|
+
var NEST_PAD_Y = 36;
|
|
15931
|
+
var NEST_OUTER = 36;
|
|
15932
|
+
function topLevelSubgroups(boxes, subgroups) {
|
|
15933
|
+
if (!subgroups) return {};
|
|
15934
|
+
return Object.fromEntries(
|
|
15935
|
+
(boxes ?? []).flatMap((b) => b.parent === void 0 && b.id !== void 0 && subgroups[b.id] ? [[b.id, subgroups[b.id]]] : [])
|
|
15936
|
+
);
|
|
15937
|
+
}
|
|
15938
|
+
function fitNestedBoxes(layout, ir, boxes, subgroups, only2) {
|
|
15939
|
+
const at2 = new Map(layout.nodes.map((n) => [n.id, n]));
|
|
15940
|
+
const ownerOf2 = /* @__PURE__ */ new Map();
|
|
15941
|
+
for (const [key, ids] of Object.entries(boxes)) for (const id of ids) if (at2.has(id)) ownerOf2.set(id, key);
|
|
15942
|
+
const sizes = footprints(ir);
|
|
15943
|
+
for (const box of layout.groups ?? []) {
|
|
15944
|
+
if (box.parent === void 0 || box.id === void 0 || only2 !== void 0 && box.parent !== only2) continue;
|
|
15945
|
+
const cards = (subgroups[box.id] ?? []).flatMap((id) => {
|
|
15946
|
+
const p = at2.get(id);
|
|
15947
|
+
if (!p || ownerOf2.get(id) !== box.parent) return [];
|
|
15948
|
+
const s = sizes.get(id) ?? { w: NODE_W, h: NODE_H };
|
|
15949
|
+
return [{ x: p.x, y: p.y, w: s.w, h: s.h }];
|
|
15950
|
+
});
|
|
15951
|
+
if (!cards.length) continue;
|
|
15952
|
+
const x0 = Math.min(...cards.map((c) => c.x - c.w / 2)) - NEST_PAD_X;
|
|
15953
|
+
const x1 = Math.max(...cards.map((c) => c.x + c.w / 2)) + NEST_PAD_X;
|
|
15954
|
+
const y0 = Math.min(...cards.map((c) => c.y - c.h / 2)) - NEST_PAD_Y;
|
|
15955
|
+
const y1 = Math.max(...cards.map((c) => c.y + c.h / 2)) + NEST_PAD_Y;
|
|
15956
|
+
box.x = (x0 + x1) / 2;
|
|
15957
|
+
box.y = (y0 + y1) / 2;
|
|
15958
|
+
box.w = x1 - x0;
|
|
15959
|
+
box.h = y1 - y0;
|
|
15960
|
+
}
|
|
15961
|
+
}
|
|
15904
15962
|
function clampInside(r, box) {
|
|
15905
15963
|
const inset = 6;
|
|
15906
15964
|
const left = box.x - box.w / 2 + inset;
|
|
@@ -16216,7 +16274,8 @@ function packComponents(layout, ir) {
|
|
|
16216
16274
|
layout.height = Math.max(...nodes.map((n) => n.y + halfH(n))) + pad;
|
|
16217
16275
|
}
|
|
16218
16276
|
function packMemberBoxes(layout, ir, members) {
|
|
16219
|
-
const
|
|
16277
|
+
const allBoxes = layout.groups ?? [];
|
|
16278
|
+
const boxes = allBoxes.filter((b) => b.parent === void 0);
|
|
16220
16279
|
if (boxes.length < 4) return;
|
|
16221
16280
|
const nodes = layout.nodes;
|
|
16222
16281
|
const size = footprints(ir);
|
|
@@ -16332,7 +16391,7 @@ function gridComponent(one, sizes) {
|
|
|
16332
16391
|
const rows = Math.ceil(order.length / cols);
|
|
16333
16392
|
return { local, w: Math.min(cols, order.length) * (cell.w + sep12.x) - sep12.x, h: rows * (cell.h + sep12.y) - sep12.y };
|
|
16334
16393
|
}
|
|
16335
|
-
function packBoxComponents(layout, ir, boxes, bandOf) {
|
|
16394
|
+
function packBoxComponents(layout, ir, boxes, bandOf, subgroups) {
|
|
16336
16395
|
const grids = /* @__PURE__ */ new Map();
|
|
16337
16396
|
const groupBoxes = layout.groups ?? [];
|
|
16338
16397
|
const nodes = layout.nodes;
|
|
@@ -16363,6 +16422,10 @@ function packBoxComponents(layout, ir, boxes, bandOf) {
|
|
|
16363
16422
|
const b = idxOf.get(e.to);
|
|
16364
16423
|
if (a !== void 0 && b !== void 0) parent[find(a)] = find(b);
|
|
16365
16424
|
}
|
|
16425
|
+
for (const ids2 of Object.values(subgroups ?? {})) {
|
|
16426
|
+
const inside = ids2.flatMap((id) => idxOf.get(id) ?? []);
|
|
16427
|
+
for (const i of inside.slice(1)) parent[find(i)] = find(inside[0]);
|
|
16428
|
+
}
|
|
16366
16429
|
const comps = /* @__PURE__ */ new Map();
|
|
16367
16430
|
own.forEach((n, i) => (comps.get(find(i)) ?? comps.set(find(i), []).get(find(i))).push(n));
|
|
16368
16431
|
if (comps.size < 2) continue;
|
|
@@ -16375,12 +16438,19 @@ function packBoxComponents(layout, ir, boxes, bandOf) {
|
|
|
16375
16438
|
edges: ir.edges.filter((e) => inside.has(e.from) && inside.has(e.to)),
|
|
16376
16439
|
groups: {}
|
|
16377
16440
|
};
|
|
16378
|
-
|
|
16441
|
+
const nest = Object.fromEntries(
|
|
16442
|
+
Object.entries(subgroups ?? {}).flatMap(([k, ids2]) => {
|
|
16443
|
+
const mine = ids2.filter((id) => inside.has(id));
|
|
16444
|
+
return mine.length ? [[k, mine]] : [];
|
|
16445
|
+
})
|
|
16446
|
+
);
|
|
16447
|
+
const nests = Object.keys(nest).length > 0;
|
|
16448
|
+
let one = layoutIr(sub, { fit: true, ...nests ? { groups: nest } : {} });
|
|
16379
16449
|
if (one.width > one.height * BOX_ASPECT_MAX) {
|
|
16380
|
-
const sideways = layoutIr(sub, { fit: true, rankdir: "LR" });
|
|
16450
|
+
const sideways = layoutIr(sub, { fit: true, rankdir: "LR", ...nests ? { groups: nest } : {} });
|
|
16381
16451
|
if (offAspect(sideways) < offAspect(one)) one = sideways;
|
|
16382
16452
|
}
|
|
16383
|
-
const placed = offAspect(one) > GRID_COMPONENT ? gridComponent(one, size) : { local: new Map(one.nodes.map((n) => [n.id, { x: n.x, y: one.height - n.y }])), w: one.width, h: one.height };
|
|
16453
|
+
const placed = !nests && offAspect(one) > GRID_COMPONENT ? gridComponent(one, size) : { local: new Map(one.nodes.map((n) => [n.id, { x: n.x, y: one.height - n.y }])), w: one.width, h: one.height };
|
|
16384
16454
|
laid.set(root, { ...placed, ns });
|
|
16385
16455
|
}
|
|
16386
16456
|
const order = [void 0];
|
|
@@ -16396,8 +16466,9 @@ function packBoxComponents(layout, ir, boxes, bandOf) {
|
|
|
16396
16466
|
const all = [...laid.values()];
|
|
16397
16467
|
const area = all.reduce((s, c) => s + (c.w + COMPONENT_GAP) * (c.h + COMPONENT_GAP), 0);
|
|
16398
16468
|
const targetW = Math.max(Math.max(...all.map((c) => c.w)), Math.sqrt(area * ROW_ASPECT));
|
|
16399
|
-
const
|
|
16400
|
-
const
|
|
16469
|
+
const holdsNested = groupBoxes.some((b) => b.parent === key);
|
|
16470
|
+
const originX = box.x - box.w / 2 + (holdsNested ? NEST_OUTER : BAND_PAD);
|
|
16471
|
+
const originY = box.y - box.h / 2 + (holdsNested ? NEST_OUTER : TITLE);
|
|
16401
16472
|
let shelfX = 0;
|
|
16402
16473
|
let shelfY = 0;
|
|
16403
16474
|
let shelfH = 0;
|
|
@@ -16429,10 +16500,18 @@ function packBoxComponents(layout, ir, boxes, bandOf) {
|
|
|
16429
16500
|
}
|
|
16430
16501
|
bands.push({ key: band, ids: ids2 });
|
|
16431
16502
|
}
|
|
16432
|
-
|
|
16433
|
-
const
|
|
16434
|
-
|
|
16435
|
-
|
|
16503
|
+
if (subgroups) fitNestedBoxes(layout, ir, boxes, subgroups, key);
|
|
16504
|
+
const inner = groupBoxes.filter((b) => b.parent === key);
|
|
16505
|
+
let x0 = Math.min(...own.map((n) => n.x - halfW(n))) - BAND_PAD;
|
|
16506
|
+
let x1 = Math.max(...own.map((n) => n.x + halfW(n))) + BAND_PAD;
|
|
16507
|
+
let y0 = Math.min(...own.map((n) => n.y - halfH(n))) - TITLE;
|
|
16508
|
+
let y1 = Math.max(...own.map((n) => n.y + halfH(n))) + BAND_PAD;
|
|
16509
|
+
for (const b of inner) {
|
|
16510
|
+
x0 = Math.min(x0, b.x - b.w / 2 - NEST_OUTER);
|
|
16511
|
+
x1 = Math.max(x1, b.x + b.w / 2 + NEST_OUTER);
|
|
16512
|
+
y0 = Math.min(y0, b.y - b.h / 2 - NEST_OUTER);
|
|
16513
|
+
y1 = Math.max(y1, b.y + b.h / 2 + NEST_OUTER);
|
|
16514
|
+
}
|
|
16436
16515
|
box.x = (x0 + x1) / 2;
|
|
16437
16516
|
box.y = (y0 + y1) / 2;
|
|
16438
16517
|
box.w = x1 - x0;
|
|
@@ -19486,6 +19565,17 @@ async function ledgerIn(root, dir) {
|
|
|
19486
19565
|
return { members, prefix: memberLedgerPrefix(members) };
|
|
19487
19566
|
}
|
|
19488
19567
|
|
|
19568
|
+
// node_modules/@intentius/chant/src/lifecycle/no-record.ts
|
|
19569
|
+
var NO_RECORD_ENV = "CHANT_NO_RECORD";
|
|
19570
|
+
function ledgerRecordingOff(env = process.env) {
|
|
19571
|
+
const value = env[NO_RECORD_ENV]?.trim().toLowerCase();
|
|
19572
|
+
return value !== void 0 && value !== "" && value !== "0" && value !== "false";
|
|
19573
|
+
}
|
|
19574
|
+
var overlays = /* @__PURE__ */ new Map();
|
|
19575
|
+
function overlayTip(key) {
|
|
19576
|
+
return overlays.get(key)?.tip;
|
|
19577
|
+
}
|
|
19578
|
+
|
|
19489
19579
|
// node_modules/@intentius/chant/src/lifecycle/git.ts
|
|
19490
19580
|
var STATE_BRANCH = "chant/lifecycle";
|
|
19491
19581
|
async function ledgerDir(dir, opts) {
|
|
@@ -19502,7 +19592,7 @@ async function readBlobFromPath(environment, filename, opts) {
|
|
|
19502
19592
|
const rt = getRuntime();
|
|
19503
19593
|
const dir = await ledgerDir(environment, opts);
|
|
19504
19594
|
const result = await rt.spawn(
|
|
19505
|
-
["git", "show", `${
|
|
19595
|
+
["git", "show", `${await ledgerRevision(opts?.cwd)}:${dir}/${filename}`],
|
|
19506
19596
|
{ cwd: opts?.cwd }
|
|
19507
19597
|
);
|
|
19508
19598
|
if (result.exitCode !== 0) return null;
|
|
@@ -19527,7 +19617,7 @@ async function listLedgerTrees(opts) {
|
|
|
19527
19617
|
const rt = getRuntime();
|
|
19528
19618
|
const base = await ledgerDir("", opts);
|
|
19529
19619
|
const rootResult = await rt.spawn(
|
|
19530
|
-
["git", "ls-tree", "--full-tree", base === "" ?
|
|
19620
|
+
["git", "ls-tree", "--full-tree", base === "" ? tip : `${tip}:${base}`],
|
|
19531
19621
|
{ cwd: opts?.cwd }
|
|
19532
19622
|
);
|
|
19533
19623
|
if (rootResult.exitCode !== 0) return [];
|
|
@@ -19538,7 +19628,12 @@ async function listLedgerTrees(opts) {
|
|
|
19538
19628
|
}
|
|
19539
19629
|
return trees;
|
|
19540
19630
|
}
|
|
19631
|
+
var C_LOCALE_ENV = { LC_ALL: "C", LANGUAGE: "" };
|
|
19541
19632
|
async function getStateBranchTip(cwd) {
|
|
19633
|
+
if (ledgerRecordingOff()) {
|
|
19634
|
+
const tip = overlayTip(await overlayKey(cwd));
|
|
19635
|
+
if (tip !== void 0) return tip;
|
|
19636
|
+
}
|
|
19542
19637
|
const rt = getRuntime();
|
|
19543
19638
|
const result = await rt.spawn(
|
|
19544
19639
|
["git", "rev-parse", "--verify", `refs/heads/${STATE_BRANCH}`],
|
|
@@ -19547,6 +19642,20 @@ async function getStateBranchTip(cwd) {
|
|
|
19547
19642
|
if (result.exitCode !== 0) return null;
|
|
19548
19643
|
return result.stdout.trim();
|
|
19549
19644
|
}
|
|
19645
|
+
async function ledgerRevision(cwd) {
|
|
19646
|
+
if (!ledgerRecordingOff()) return STATE_BRANCH;
|
|
19647
|
+
return overlayTip(await overlayKey(cwd)) ?? STATE_BRANCH;
|
|
19648
|
+
}
|
|
19649
|
+
var overlayKeys = /* @__PURE__ */ new Map();
|
|
19650
|
+
async function overlayKey(cwd) {
|
|
19651
|
+
const from = cwd ?? process.cwd();
|
|
19652
|
+
const cached = overlayKeys.get(from);
|
|
19653
|
+
if (cached !== void 0) return cached;
|
|
19654
|
+
const result = await getRuntime().spawn(["git", "rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: from, env: C_LOCALE_ENV });
|
|
19655
|
+
const key = result.exitCode === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : from;
|
|
19656
|
+
overlayKeys.set(from, key);
|
|
19657
|
+
return key;
|
|
19658
|
+
}
|
|
19550
19659
|
|
|
19551
19660
|
// node_modules/@intentius/chant/src/content-digest.ts
|
|
19552
19661
|
import { createHash as createHash6 } from "node:crypto";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/behold",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "behold — a live control plane on chant. See your whole estate (every substrate in one graph), coloured by drift; act through delegated, gated Ops.",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@hono/node-server": "^2.0.5",
|
|
44
|
-
"@intentius/chant": "^0.124.
|
|
45
|
-
"@intentius/chant-k8s-client": "^0.124.
|
|
46
|
-
"@intentius/pinhole": "^0.
|
|
44
|
+
"@intentius/chant": "^0.124.24",
|
|
45
|
+
"@intentius/chant-k8s-client": "^0.124.24",
|
|
46
|
+
"@intentius/pinhole": "^0.5.0",
|
|
47
47
|
"ajv": "^8.20.0",
|
|
48
48
|
"hono": "^4.12.34"
|
|
49
49
|
},
|
package/web/app.js
CHANGED
|
@@ -21,7 +21,7 @@ import { initTerragucciEstate } from "./terragucci-estate.js";
|
|
|
21
21
|
import { auditLines as yrAuditLines, cornerText as yrCorner, datedWords as yrDated, environmentLines as yrEnvLines, joinMarks as yrJoin, legend as yrLegend, markLine as yrMarkLine } from "./yodel-report.js";
|
|
22
22
|
import { cardProgress as tgCardProgress, progressLines as tgProgressLines, progressSection as tgProgressSection, stampProgress as tgStampProgress } from "./terragucci-progress.js";
|
|
23
23
|
import { seconds, whyQuery, whyView } from "./why.js";
|
|
24
|
-
import { edgeLabel, edgeLegend, edgeTitle, isCiKind } from "./edge-kind.js";
|
|
24
|
+
import { edgeKey, edgeLabel, edgeLegend, edgeTitle, isCiKind, viaIndex } from "./edge-kind.js";
|
|
25
25
|
import { readCostLine } from "./read-cost.js";
|
|
26
26
|
import { mountHistoryPicker } from "./history-picker.js";
|
|
27
27
|
import { applyMemberFrame, pendingLine, stillPending } from "./pending.js";
|
|
@@ -4457,21 +4457,16 @@ function relabelEdgeChip(chip, kind, via) {
|
|
|
4457
4457
|
function wireEdgeHighlight(svgEl, ir) {
|
|
4458
4458
|
const edges = [...svgEl.querySelectorAll("g[data-edge-from]")];
|
|
4459
4459
|
if (!edges.length) return;
|
|
4460
|
-
// Reason per edge, keyed by from
|
|
4461
|
-
//
|
|
4462
|
-
//
|
|
4463
|
-
const viaOf =
|
|
4464
|
-
for (const e of ir.edges || []) {
|
|
4465
|
-
if (!e.viaAttr) continue;
|
|
4466
|
-
viaOf.set(`${e.from}|${e.to}`, e.viaAttr);
|
|
4467
|
-
viaOf.set(`${e.to}|${e.from}`, e.viaAttr);
|
|
4468
|
-
}
|
|
4460
|
+
// Reason per edge, keyed by from, to and kind (pinhole's edgeKey, #564), so
|
|
4461
|
+
// two kinds on one pair keep their own reason. pinhole stamps data-edge-via
|
|
4462
|
+
// itself; this fills it in where it did not.
|
|
4463
|
+
const viaOf = viaIndex(ir.edges || []);
|
|
4469
4464
|
for (const g of edges) {
|
|
4470
4465
|
const from = g.getAttribute("data-edge-from");
|
|
4471
4466
|
const to = g.getAttribute("data-edge-to");
|
|
4472
|
-
const via = g.getAttribute("data-edge-via") || viaOf.get(`${from}|${to}`);
|
|
4473
|
-
if (via) g.setAttribute("data-edge-via", via); // drives the dashed style
|
|
4474
4467
|
const kind = g.getAttribute("data-edge-kind") || "ref";
|
|
4468
|
+
const via = g.getAttribute("data-edge-via") || viaOf.get(edgeKey({ from, to, kind }));
|
|
4469
|
+
if (via) g.setAttribute("data-edge-via", via); // drives the dashed style
|
|
4475
4470
|
const cross = g.getAttribute("data-edge-cross") === "true";
|
|
4476
4471
|
if (!g.querySelector("path")) relabelEdgeChip(g, kind, via);
|
|
4477
4472
|
const title = document.createElementNS(SVGNS, "title");
|
|
@@ -5280,17 +5275,24 @@ function userRect(el, toUser) {
|
|
|
5280
5275
|
*/
|
|
5281
5276
|
function edgeLabelOf(g) {
|
|
5282
5277
|
if (!(g.getAttribute("data-edge-via") || "").trim()) return null;
|
|
5283
|
-
|
|
5284
|
-
|
|
5278
|
+
// The pair and the kind (pinhole#128, #564): two kinds on one pair draw two
|
|
5279
|
+
// lines with a chip each, and each line keeps its own chip.
|
|
5280
|
+
const key = edgeKeyOf(g);
|
|
5285
5281
|
for (const cand of g.parentNode ? g.parentNode.children : []) {
|
|
5286
5282
|
if (cand === g || cand.tagName.toLowerCase() !== "g") continue;
|
|
5287
|
-
if (cand.
|
|
5283
|
+
if (!cand.hasAttribute("data-edge-from") || edgeKeyOf(cand) !== key) continue;
|
|
5288
5284
|
if (cand.hasAttribute("data-node-id") || cand.querySelector("path")) continue;
|
|
5289
5285
|
return cand;
|
|
5290
5286
|
}
|
|
5291
5287
|
return null;
|
|
5292
5288
|
}
|
|
5293
5289
|
|
|
5290
|
+
/** A drawn edge group's identity, pinhole's `edgeKey` over its stamped
|
|
5291
|
+
* attributes (no `data-edge-kind` is a reference). */
|
|
5292
|
+
function edgeKeyOf(g) {
|
|
5293
|
+
return edgeKey({ from: g.getAttribute("data-edge-from"), to: g.getAttribute("data-edge-to"), kind: g.getAttribute("data-edge-kind") || "ref" });
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5294
5296
|
/** Index the freshly rendered SVG: node groups, containment boxes, edge paths. */
|
|
5295
5297
|
function indexLayout(svgEl) {
|
|
5296
5298
|
const toUser = userSpaceMapper(svgEl);
|
package/web/edge-kind.js
CHANGED
|
@@ -32,6 +32,37 @@ export function isCiKind(kind) {
|
|
|
32
32
|
return !!kind && kind !== "ref";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* An edge's identity, the same key pinhole 0.5.0's `edgeKey` gives
|
|
37
|
+
* (pinhole#128): `from>to` for a reference, `from>to>kind` otherwise. Two
|
|
38
|
+
* kinds on one pair (a GitLab job that both `needs` and `extends` another)
|
|
39
|
+
* are two edges, so anything the SPA looks up per edge goes through this
|
|
40
|
+
* (#564). Read off a drawn edge, pass `data-edge-kind`, which pinhole leaves
|
|
41
|
+
* off for "ref".
|
|
42
|
+
*/
|
|
43
|
+
export function edgeKey({ from, to, kind } = {}) {
|
|
44
|
+
return `${from}>${to}${isCiKind(kind) ? `>${kind}` : ""}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The `viaAttr` per edge key, for the drawn edges pinhole did not stamp
|
|
49
|
+
* `data-edge-via` on. The reverse direction is filled in as well, where
|
|
50
|
+
* nothing already holds it, since pinhole may paint an edge either way round
|
|
51
|
+
* relative to the IR.
|
|
52
|
+
*/
|
|
53
|
+
export function viaIndex(edges = []) {
|
|
54
|
+
const via = new Map();
|
|
55
|
+
for (const e of edges) {
|
|
56
|
+
if (!e.viaAttr) continue;
|
|
57
|
+
via.set(edgeKey(e), e.viaAttr);
|
|
58
|
+
}
|
|
59
|
+
for (const e of edges) {
|
|
60
|
+
const back = edgeKey({ from: e.to, to: e.from, kind: e.kind });
|
|
61
|
+
if (e.viaAttr && !via.has(back)) via.set(back, e.viaAttr);
|
|
62
|
+
}
|
|
63
|
+
return via;
|
|
64
|
+
}
|
|
65
|
+
|
|
35
66
|
/**
|
|
36
67
|
* The chip text, as chant's Mermaid and DOT writers label the edge
|
|
37
68
|
* (graph-mermaid.ts `edgeLabel`): a CI edge by its kind, plus the attribute
|
package/web/edge-kind.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { edgeLabel, edgeLegend, edgeTitle } from "./edge-kind.js";
|
|
2
|
+
import { edgeKey, edgeLabel, edgeLegend, edgeTitle, viaIndex } from "./edge-kind.js";
|
|
3
3
|
|
|
4
4
|
// The edges chant 0.124.20's GitHub lexicon writes (lexicons/github/src/
|
|
5
5
|
// graph-edges.ts) and one from its GitLab lexicon.
|
|
@@ -68,3 +68,33 @@ describe("edgeLegend (#553)", () => {
|
|
|
68
68
|
expect(edgeLegend([ref, { from: "a", to: "b", kind: "ref" }])).toEqual([]);
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
// #564: pinhole 0.5.0 keys an edge on from, to and kind (pinhole#128), so two
|
|
73
|
+
// kinds on one pair are two edges, each with its own chip and tooltip.
|
|
74
|
+
describe("edgeKey and viaIndex (#564)", () => {
|
|
75
|
+
it("gives the key pinhole's edgeKey gives: a reference keeps from>to", () => {
|
|
76
|
+
expect(edgeKey({ from: "api", to: "db", kind: "ref" })).toBe("api>db");
|
|
77
|
+
expect(edgeKey({ from: "api", to: "db" })).toBe("api>db");
|
|
78
|
+
expect(edgeKey({ from: "test", to: "build", kind: "needs" })).toBe("test>build>needs");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("keeps each kind's reason on a pair joined twice", () => {
|
|
82
|
+
const via = viaIndex([
|
|
83
|
+
{ from: "test", to: "build", kind: "needs", viaAttr: "stage" },
|
|
84
|
+
{ from: "test", to: "build", kind: "extends", viaAttr: "extends" },
|
|
85
|
+
]);
|
|
86
|
+
expect(via.get(edgeKey({ from: "test", to: "build", kind: "needs" }))).toBe("stage");
|
|
87
|
+
expect(via.get(edgeKey({ from: "test", to: "build", kind: "extends" }))).toBe("extends");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("fills in the reverse direction without overwriting an edge drawn that way", () => {
|
|
91
|
+
const via = viaIndex([
|
|
92
|
+
{ from: "a", to: "b", kind: "ref", viaAttr: "role" },
|
|
93
|
+
{ from: "b", to: "a", kind: "ref", viaAttr: "policy_arn" },
|
|
94
|
+
{ from: "c", to: "d", kind: "ref", viaAttr: "zone_id" },
|
|
95
|
+
]);
|
|
96
|
+
expect(via.get("a>b")).toBe("role");
|
|
97
|
+
expect(via.get("b>a")).toBe("policy_arn");
|
|
98
|
+
expect(via.get("d>c")).toBe("zone_id");
|
|
99
|
+
});
|
|
100
|
+
});
|