mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 17:20:46 +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
|
|
@ -1976,15 +1976,18 @@ def _update_topology_view_positions(args: dict[str, Any]) -> dict[str, Any]:
|
|||
fid = str(p.get("fabric_node_id") or "").strip()
|
||||
if not fid:
|
||||
continue
|
||||
cleaned.append(
|
||||
{
|
||||
"fabric_node_id": fid,
|
||||
"x": float(p.get("x") or 0),
|
||||
"y": float(p.get("y") or 0),
|
||||
"label": str(p.get("label") or ""),
|
||||
"locked": bool(p.get("locked") or False),
|
||||
}
|
||||
)
|
||||
row: dict[str, Any] = {
|
||||
"fabric_node_id": fid,
|
||||
"x": float(p.get("x") or 0),
|
||||
"y": float(p.get("y") or 0),
|
||||
}
|
||||
# Omitted label/locked = keep server value (a UI rename or an existing lock).
|
||||
# label "" clears the override; locked false explicitly unlocks.
|
||||
if p.get("label") is not None:
|
||||
row["label"] = str(p.get("label"))
|
||||
if p.get("locked") is not None:
|
||||
row["locked"] = bool(p.get("locked"))
|
||||
cleaned.append(row)
|
||||
if not cleaned:
|
||||
return {"ok": False, "error": "positions_required"}
|
||||
parts: list[dict[str, Any]] = []
|
||||
|
|
@ -3727,8 +3730,14 @@ HTTP_MCP_TOOLS: list[dict[str, Any]] = [
|
|||
"fabric_node_id": {"type": "string"},
|
||||
"x": {"type": "number"},
|
||||
"y": {"type": "number"},
|
||||
"label": {"type": "string"},
|
||||
"locked": {"type": "boolean"},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Omit to keep the current label; \"\" clears a rename (follow device name).",
|
||||
},
|
||||
"locked": {
|
||||
"type": "boolean",
|
||||
"description": "Omit to keep the lock. Locked nodes are skipped unless locked=false (unlock + move).",
|
||||
},
|
||||
},
|
||||
"required": ["fabric_node_id"],
|
||||
"additionalProperties": False,
|
||||
|
|
|
|||
|
|
@ -614,3 +614,24 @@ def test_analyze_topology_view_layout_single() -> None:
|
|||
assert "sparsity" in payload
|
||||
assert "verdict" in payload
|
||||
assert "total" in payload["verdict"]
|
||||
|
||||
|
||||
def test_update_positions_omits_label_and_locked_unless_given() -> None:
|
||||
from netx_topology_mcp.http_tools import _update_topology_view_positions
|
||||
|
||||
with patch("netx_topology_mcp.http_tools.http_json") as mock_http:
|
||||
mock_http.return_value = {"ok": True, "data": {"ok": True, "updated": 3}}
|
||||
_update_topology_view_positions(
|
||||
{
|
||||
"view_id": "v1",
|
||||
"positions": [
|
||||
{"fabric_node_id": "a", "x": 1, "y": 2},
|
||||
{"fabric_node_id": "b", "x": 3, "y": 4, "label": "", "locked": False},
|
||||
{"fabric_node_id": "c", "x": 5, "y": 6, "label": "Core-1", "locked": True},
|
||||
],
|
||||
}
|
||||
)
|
||||
sent = mock_http.call_args[1]["body"]["positions"]
|
||||
assert sent[0] == {"fabric_node_id": "a", "x": 1.0, "y": 2.0}
|
||||
assert sent[1]["label"] == "" and sent[1]["locked"] is False
|
||||
assert sent[2]["label"] == "Core-1" and sent[2]["locked"] is True
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue