mirror of
https://github.com/hansjone/netx.git
synced 2026-10-15 03:40:51 +08:00
Optimize topology canvas per the 2026-10-12 plan.
Save path: one atomic POST /views/{id}/commit (edges, ports, styles, node
add/remove, positions) replaces the multi-request save. Edge styles and
manual-edge port edits now go through dirty/undo/save instead of writing
immediately; non-manual edge ports are read-only.
Data safety: UI saves no longer clear MCP node locks or freeze the device
name into the placement label (label_override + omit-means-keep semantics,
also applied to the MCP positions tool). Adding NEs/neighbors with unsaved
edits now saves first instead of silently dropping them. Positions no longer
bypass membership caps, except persisting synthesized child-region icons.
Fixes found in acceptance: nodes added in the same commit kept (40,40)
under autoflush=False; edits on edges deleted in the same save 404'd the
whole save; box-selection drags and arrow-key moves were not marked dirty;
large canvases far from the origin opened blank and locate did nothing
because fitView waits for measurement that onlyRenderVisibleElements never
does (now fitBounds from known geometry).
Also: batch N+1 lookups in graph/positions, deterministic truncation,
outside-peer scan excludes internal edges, split TopologyPage into hooks,
fix flaky UME/LLDP port test, add scripts/topology_clear_frozen_labels.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
60a0ea6021
commit
f301544b99
40 changed files with 5587 additions and 1916 deletions
116
web/tests/topologySaveDiff.test.mjs
Normal file
116
web/tests/topologySaveDiff.test.mjs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { buildViewCommitBody, isRegionNodeId } from "../src/features/topology/lib/saveDiff.ts";
|
||||
|
||||
test("local add/remove nodes; region nodes stay out of add/remove lists", () => {
|
||||
const body = buildViewCommitBody({
|
||||
nodes: [
|
||||
{ id: "fab-a" },
|
||||
{ id: "fab-new" },
|
||||
{ id: "region:x", data: { kind: "region" } },
|
||||
],
|
||||
edges: [],
|
||||
serverNodeIds: ["fab-a", "fab-gone", "region:y"],
|
||||
positions: [
|
||||
{ fabric_node_id: "fab-a", x: 1, y: 2 },
|
||||
{ fabric_node_id: "fab-new", x: 3, y: 4 },
|
||||
{ fabric_node_id: "region:x", x: 5, y: 6 },
|
||||
],
|
||||
pendingCreates: [],
|
||||
pendingDeletes: [],
|
||||
});
|
||||
assert.deepEqual(body.node_adds, ["fab-new"]);
|
||||
assert.deepEqual(body.node_removes, ["fab-gone"]);
|
||||
assert.ok(!body.node_adds.some(isRegionNodeId));
|
||||
assert.ok(!body.node_removes.some(isRegionNodeId));
|
||||
assert.equal(body.positions.length, 3);
|
||||
});
|
||||
|
||||
test("only changed label overrides appear in positions payload from caller", () => {
|
||||
const body = buildViewCommitBody({
|
||||
nodes: [{ id: "fab-a" }],
|
||||
edges: [],
|
||||
serverNodeIds: ["fab-a"],
|
||||
positions: [{ fabric_node_id: "fab-a", x: 1, y: 2, label: "CUSTOM" }],
|
||||
pendingCreates: [],
|
||||
pendingDeletes: [],
|
||||
});
|
||||
assert.equal(body.positions[0].label, "CUSTOM");
|
||||
});
|
||||
|
||||
test("deleted then re-created edge is not in edge_deletes", () => {
|
||||
const body = buildViewCommitBody({
|
||||
nodes: [{ id: "a" }, { id: "b" }],
|
||||
edges: [
|
||||
{
|
||||
id: "local:1",
|
||||
source: "a",
|
||||
target: "b",
|
||||
data: { source_port: "gi0/1", target_port: "gi0/2" },
|
||||
},
|
||||
],
|
||||
serverNodeIds: ["a", "b"],
|
||||
positions: [],
|
||||
pendingCreates: ["local:1"],
|
||||
pendingDeletes: ["edge-server-1", "local:1"],
|
||||
});
|
||||
assert.deepEqual(body.edge_deletes, ["edge-server-1"]);
|
||||
assert.equal(body.edge_creates.length, 1);
|
||||
assert.equal(body.edge_creates[0].client_id, "local:1");
|
||||
assert.equal(body.edge_creates[0].a_port, "gi0/1");
|
||||
});
|
||||
|
||||
test("undo after delete removes edge from edge_deletes when not pending", () => {
|
||||
const body = buildViewCommitBody({
|
||||
nodes: [{ id: "a" }, { id: "b" }],
|
||||
edges: [{ id: "edge-server-1", source: "a", target: "b" }],
|
||||
serverNodeIds: ["a", "b"],
|
||||
positions: [],
|
||||
pendingCreates: [],
|
||||
pendingDeletes: [],
|
||||
});
|
||||
assert.deepEqual(body.edge_deletes, []);
|
||||
assert.deepEqual(body.edge_creates, []);
|
||||
});
|
||||
|
||||
test("styled or port-edited edge that is then deleted only appears in edge_deletes", () => {
|
||||
const style = { stroke_color: "#ff0000", stroke_width: 2, line_style: "dashed" };
|
||||
const body = buildViewCommitBody({
|
||||
nodes: [{ id: "a" }, { id: "b" }],
|
||||
edges: [],
|
||||
serverNodeIds: ["a", "b"],
|
||||
positions: [],
|
||||
pendingCreates: [],
|
||||
pendingDeletes: ["edge-1"],
|
||||
pendingPortEdits: [["edge-1", { a_port: "gi0/1", b_port: "gi0/2" }]],
|
||||
pendingStyles: [["edge-1", style]],
|
||||
});
|
||||
assert.deepEqual(body.edge_deletes, ["edge-1"]);
|
||||
assert.deepEqual(body.edge_port_edits, []);
|
||||
assert.deepEqual(body.edge_styles, []);
|
||||
});
|
||||
|
||||
test("style on a new local edge is sent under its client id; dropped once the edge is gone", () => {
|
||||
const style = { stroke_color: "#00ff00", stroke_width: 3, line_style: "" };
|
||||
const edges = [{ id: "local:9", source: "a", target: "b", data: {} }];
|
||||
const kept = buildViewCommitBody({
|
||||
nodes: [{ id: "a" }, { id: "b" }],
|
||||
edges,
|
||||
serverNodeIds: ["a", "b"],
|
||||
positions: [],
|
||||
pendingCreates: ["local:9"],
|
||||
pendingDeletes: [],
|
||||
pendingStyles: [["local:9", style]],
|
||||
});
|
||||
assert.deepEqual(kept.edge_styles.map((s) => s.fabric_edge_id), ["local:9"]);
|
||||
const dropped = buildViewCommitBody({
|
||||
nodes: [{ id: "a" }, { id: "b" }],
|
||||
edges: [],
|
||||
serverNodeIds: ["a", "b"],
|
||||
positions: [],
|
||||
pendingCreates: [],
|
||||
pendingDeletes: [],
|
||||
pendingStyles: [["local:9", style]],
|
||||
});
|
||||
assert.deepEqual(dropped.edge_styles, []);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue