feat(dashboard): elk-layout repair pipeline + applyElkLayout

This commit is contained in:
Lum1104
2026-05-03 16:00:49 +08:00
parent 46dc0485b9
commit 8f3a6bf2c4
2 changed files with 309 additions and 0 deletions
@@ -0,0 +1,72 @@
import { describe, it, expect } from "vitest";
import { repairElkInput, type ElkInput } from "../elk-layout";
describe("repairElkInput", () => {
it("ensures node dimensions when missing", () => {
const input: ElkInput = {
id: "root",
children: [{ id: "a" }, { id: "b", width: 100, height: 50 }] as ElkInput["children"],
edges: [],
};
const { input: out, issues } = repairElkInput(input);
expect(out.children![0].width).toBeGreaterThan(0);
expect(out.children![0].height).toBeGreaterThan(0);
expect(out.children![1]).toEqual({ id: "b", width: 100, height: 50 });
expect(issues.some((i) => i.level === "auto-corrected" && /dimensions/.test(i.message))).toBe(true);
});
it("dedupes duplicate child ids and reports auto-corrected", () => {
const input: ElkInput = {
id: "root",
children: [
{ id: "a", width: 1, height: 1 },
{ id: "a", width: 1, height: 1 },
],
edges: [],
};
const { input: out, issues } = repairElkInput(input);
expect(out.children).toHaveLength(1);
expect(issues.some((i) => i.level === "auto-corrected" && /duplicate/.test(i.message))).toBe(true);
});
it("drops orphan edges referencing nonexistent nodes", () => {
const input: ElkInput = {
id: "root",
children: [{ id: "a", width: 1, height: 1 }],
edges: [
{ id: "e1", sources: ["a"], targets: ["ghost"] },
],
};
const { input: out, issues } = repairElkInput(input);
expect(out.edges).toHaveLength(0);
expect(issues.some((i) => i.level === "dropped" && /edge/.test(i.message))).toBe(true);
});
it("drops children referencing nonexistent parents", () => {
const input: ElkInput = {
id: "root",
children: [
{
id: "p",
width: 100,
height: 100,
children: [{ id: "c1", width: 1, height: 1 }],
},
{ id: "orphan", width: 1, height: 1, parentId: "ghost" } as ElkInput["children"][0] & { parentId: string },
],
edges: [],
};
const { input: out, issues } = repairElkInput(input);
expect(out.children!.find((c) => c.id === "orphan")).toBeUndefined();
expect(issues.some((i) => i.level === "dropped" && /parent/.test(i.message))).toBe(true);
});
it("strict mode throws on any issue", () => {
const input: ElkInput = {
id: "root",
children: [{ id: "a" }] as ElkInput["children"],
edges: [],
};
expect(() => repairElkInput(input, { strict: true })).toThrow(/dimensions/);
});
});
@@ -0,0 +1,237 @@
import ELK from "elkjs/lib/elk.bundled.js";
import type { GraphIssue } from "@understand-anything/core/schema";
export interface ElkChild {
id: string;
width?: number;
height?: number;
children?: ElkChild[];
parentId?: string;
}
export interface ElkEdge {
id: string;
sources: string[];
targets: string[];
}
export interface ElkInput {
id: string;
children: ElkChild[];
edges: ElkEdge[];
layoutOptions?: Record<string, string>;
}
const DEFAULT_NODE_WIDTH = 280;
const DEFAULT_NODE_HEIGHT = 120;
interface RepairOptions {
strict?: boolean;
}
interface RepairResult {
input: ElkInput;
issues: GraphIssue[];
}
function makeIssue(
level: GraphIssue["level"],
category: string,
message: string,
): GraphIssue {
return { level, category, message };
}
function maybeThrow(strict: boolean | undefined, issue: GraphIssue): void {
if (strict) throw new Error(`[ELK repair] ${issue.level}: ${issue.message}`);
}
export function repairElkInput(
input: ElkInput,
opts: RepairOptions = {},
): RepairResult {
const issues: GraphIssue[] = [];
const strict = opts.strict;
// 1. ensureNodeDimensions
let dimsAdded = 0;
const fillDims = (children: ElkChild[]): ElkChild[] =>
children.map((c) => {
const next: ElkChild = { ...c };
if (next.width == null || next.height == null) {
next.width = next.width ?? DEFAULT_NODE_WIDTH;
next.height = next.height ?? DEFAULT_NODE_HEIGHT;
dimsAdded++;
}
if (next.children) next.children = fillDims(next.children);
return next;
});
const childrenA = fillDims(input.children);
if (dimsAdded > 0) {
const issue = makeIssue(
"auto-corrected",
"elk-missing-dimensions",
`Set default dimensions on ${dimsAdded} node(s) missing width/height.`,
);
issues.push(issue);
maybeThrow(strict, issue);
}
// 2. dedupeNodeIds (per parent)
let dupesRemoved = 0;
const dedupe = (children: ElkChild[]): ElkChild[] => {
const seen = new Set<string>();
const out: ElkChild[] = [];
for (const c of children) {
if (seen.has(c.id)) {
dupesRemoved++;
continue;
}
seen.add(c.id);
out.push({
...c,
children: c.children ? dedupe(c.children) : undefined,
});
}
return out;
};
const childrenB = dedupe(childrenA);
if (dupesRemoved > 0) {
const issue = makeIssue(
"auto-corrected",
"elk-duplicate-id",
`Removed ${dupesRemoved} duplicate child id(s).`,
);
issues.push(issue);
maybeThrow(strict, issue);
}
// 3. dropOrphanChildren — children whose parentId references nonexistent parent
const allIds = new Set<string>();
const walk = (children: ElkChild[]) => {
for (const c of children) {
allIds.add(c.id);
if (c.children) walk(c.children);
}
};
walk(childrenB);
let orphanChildren = 0;
const childrenC = childrenB.filter((c) => {
if (c.parentId && !allIds.has(c.parentId)) {
orphanChildren++;
return false;
}
return true;
});
if (orphanChildren > 0) {
const issue = makeIssue(
"dropped",
"elk-orphan-parent",
`Dropped ${orphanChildren} child(ren) with missing parent reference.`,
);
issues.push(issue);
maybeThrow(strict, issue);
}
// 4. dropOrphanEdges
let orphanEdges = 0;
const edges = input.edges.filter((e) => {
const ok = e.sources.every((s) => allIds.has(s)) &&
e.targets.every((t) => allIds.has(t));
if (!ok) {
orphanEdges++;
return false;
}
return true;
});
if (orphanEdges > 0) {
const issue = makeIssue(
"dropped",
"elk-orphan-edge",
`Dropped ${orphanEdges} edge(s) referencing nonexistent nodes.`,
);
issues.push(issue);
maybeThrow(strict, issue);
}
// 5. dropCircularContainment
const parentOf = new Map<string, string>();
const fillParents = (children: ElkChild[], parent?: string) => {
for (const c of children) {
if (parent) parentOf.set(c.id, parent);
if (c.children) fillParents(c.children, c.id);
}
};
fillParents(childrenC);
let cyclesRemoved = 0;
const isCyclic = (id: string): boolean => {
const seen = new Set<string>();
let cur = parentOf.get(id);
while (cur) {
if (cur === id || seen.has(cur)) return true;
seen.add(cur);
cur = parentOf.get(cur);
}
return false;
};
const stripCycles = (children: ElkChild[]): ElkChild[] =>
children
.filter((c) => {
if (isCyclic(c.id)) {
cyclesRemoved++;
return false;
}
return true;
})
.map((c) => ({
...c,
children: c.children ? stripCycles(c.children) : undefined,
}));
const childrenD = stripCycles(childrenC);
if (cyclesRemoved > 0) {
const issue = makeIssue(
"dropped",
"elk-containment-cycle",
`Dropped ${cyclesRemoved} node(s) in containment cycles.`,
);
issues.push(issue);
maybeThrow(strict, issue);
}
return {
input: { ...input, children: childrenD, edges },
issues,
};
}
const elk = new ELK();
export interface ElkLayoutOptions {
strict?: boolean;
}
export interface ElkLayoutResult {
positioned: ElkInput;
issues: GraphIssue[];
}
export async function applyElkLayout(
input: ElkInput,
opts: ElkLayoutOptions = {},
): Promise<ElkLayoutResult> {
const { input: repaired, issues } = repairElkInput(input, opts);
try {
const positioned = (await elk.layout(repaired as never)) as ElkInput;
return { positioned, issues };
} catch (err) {
const fatal: GraphIssue = {
level: "fatal",
category: "elk-layout-failed",
message:
`ELK layout failed: ${err instanceof Error ? err.message : String(err)}. ` +
`This looks like a dashboard rendering bug — please file an issue with the copied error.`,
};
if (opts.strict) throw err;
return { positioned: { ...repaired, children: [], edges: [] }, issues: [...issues, fatal] };
}
}