Harden topology canvas edge delete, discover cancel, and map UX.

Persist Fabric edge deletes, stop LLDP jobs from the canvas, surface truncation/placeholders/stale edges, and fix LLDP job deep-links.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-05 21:23:00 +08:00
parent 5e567408e7
commit 617d4245b0
14 changed files with 468 additions and 238 deletions

View file

@ -4,6 +4,8 @@ from __future__ import annotations
from .topology_fabric_links import (
_apply_missing_and_purge,
_mark_replaced_port_peers,
delete_fabric_edge,
delete_fabric_edges,
merge_duplicate_fabric_nodes,
upsert_fabric_edge,
)
@ -37,6 +39,8 @@ __all__ = [
"_match_hit_to_fabric_node",
"_node_out",
"_nodes_by_ids",
"delete_fabric_edge",
"delete_fabric_edges",
"ensure_fabric_node_for_managed",
"ensure_fabric_node_for_ume",
"ensure_lldp_discovered_managed_ne",

View file

@ -511,4 +511,39 @@ def merge_duplicate_fabric_nodes(db: Session) -> dict[str, int]:
return {"merged": merged, "placeholders_removed": placeholders_removed}
def delete_fabric_edges(db: Session, edge_ids: list[str]) -> dict[str, int]:
"""Hard-delete fabric edges and any per-view edge styles."""
ids = [str(x).strip() for x in (edge_ids or []) if str(x).strip()]
# De-dupe while preserving order.
seen: set[str] = set()
ordered: list[str] = []
for i in ids:
if i in seen:
continue
seen.add(i)
ordered.append(i)
if not ordered:
raise HTTPException(status_code=400, detail="edge_ids_required")
rows = db.query(TopoFabricEdge).filter(TopoFabricEdge.id.in_(ordered)).all()
found_ids = [str(r.id) for r in rows]
if not found_ids:
raise HTTPException(status_code=404, detail="fabric_edge_not_found")
db.query(TopoViewEdgeStyle).filter(
TopoViewEdgeStyle.fabric_edge_id.in_(found_ids)
).delete(synchronize_session=False)
db.query(TopoFabricEdge).filter(TopoFabricEdge.id.in_(found_ids)).delete(
synchronize_session=False
)
db.commit()
try:
refresh_fabric_stats(db)
except Exception: # noqa: BLE001
pass
return {"deleted": len(found_ids)}
def delete_fabric_edge(db: Session, edge_id: str) -> dict[str, int]:
return delete_fabric_edges(db, [edge_id])

View file

@ -26,6 +26,7 @@ from .topology_schemas import (
ClassifyRuleCreate,
ClassifyRuleUpdate,
FabricDiscoverRequest,
FabricEdgesDeleteRequest,
FabricManualEdgeIn,
FabricNodesBulkTagRequest,
FabricNodesDeleteRequest,
@ -45,6 +46,8 @@ from .topology_schemas import (
)
from .topology_discover import get_discover_job, start_discover_job
from .topology_fabric import (
delete_fabric_edge,
delete_fabric_edges,
get_fabric_neighborhood,
get_fabric_summary,
list_fabric_edges,
@ -175,6 +178,21 @@ def api_fabric_manual_edge(
}
@router.delete("/fabric/edges/{edge_id}")
def api_fabric_delete_edge(edge_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
out = delete_fabric_edge(db, edge_id)
return {"ok": True, **out}
@router.post("/fabric/edges/delete")
def api_fabric_delete_edges(
body: FabricEdgesDeleteRequest,
db: Session = Depends(get_db),
) -> dict[str, Any]:
out = delete_fabric_edges(db, body.edge_ids)
return {"ok": True, **out}
@router.post("/fabric/discover")
def api_fabric_discover(
body: FabricDiscoverRequest | None = None,

View file

@ -259,6 +259,7 @@ class ViewNodeOut(BaseModel):
vendor: str = ""
device_type: str = ""
connect_status: str = ""
managed_source: str = "" # manual | ume_sync | lldp | topology | webcrt | …
class ViewEdgeOut(BaseModel):
@ -390,6 +391,14 @@ class FabricManualEdgeIn(BaseModel):
b_port: str = ""
class FabricEdgesDeleteRequest(BaseModel):
edge_ids: list[str] = Field(default_factory=list, min_length=1)
class FabricEdgesDeleteOut(BaseModel):
deleted: int = 0
# ---------------------------------------------------------------------------
# Classify rules + slices + search
# ---------------------------------------------------------------------------

View file

@ -21,6 +21,8 @@ from .topology_fabric import (
_mark_replaced_port_peers,
_match_hit_to_fabric_node,
_node_out,
delete_fabric_edge,
delete_fabric_edges,
ensure_fabric_node_for_managed,
ensure_fabric_node_for_ume,
ensure_lldp_discovered_managed_ne,
@ -68,6 +70,8 @@ __all__ = [
"create_folder",
"create_topology_placeholder_on_view",
"create_view",
"delete_fabric_edge",
"delete_fabric_edges",
"delete_folder",
"delete_view",
"ensure_fabric_node_for_managed",

View file

@ -99,6 +99,14 @@ def _connect_status_for_node(db: Session, n: TopoFabricNode) -> str:
return ""
def _managed_source_for_node(db: Session, n: TopoFabricNode) -> str:
if n.managed_ne_id:
ne = db.get(ManagedNE, n.managed_ne_id)
if ne is not None:
return str(ne.source or "").strip()
return ""
def get_view_graph(db: Session, view_id: str) -> TopologyViewGraphOut:
view = _get_view_or_404(db, view_id)
vnodes = db.query(TopoViewNode).filter(TopoViewNode.view_id == view.id).all()
@ -135,6 +143,7 @@ def get_view_graph(db: Session, view_id: str) -> TopologyViewGraphOut:
vendor=(fn.vendor if fn else "") or "",
device_type=(fn.device_type if fn else "") or "",
connect_status=_connect_status_for_node(db, fn) if fn else "",
managed_source=_managed_source_for_node(db, fn) if fn else "",
)
)
edges_out: list[ViewEdgeOut] = []