mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 18:30:46 +08:00
Fix topology truncate false positives and reduce graph/discover load.
Batch ManagedNE lookups, correct project-neighbors truncation/frozen signals, page discover polls, and delete both nodes and edges when multi-selected. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
03ef9eb39a
commit
2aafb72676
7 changed files with 186 additions and 25 deletions
|
|
@ -224,8 +224,13 @@ def api_fabric_reconcile_links(db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||||
|
|
||||||
|
|
||||||
@router.get("/fabric/discover/{job_id}")
|
@router.get("/fabric/discover/{job_id}")
|
||||||
def api_fabric_discover_job(job_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
|
def api_fabric_discover_job(
|
||||||
return get_discover_job(db, job_id).model_dump()
|
job_id: str,
|
||||||
|
page: int = Query(default=1, ge=1),
|
||||||
|
page_size: int = Query(default=20, ge=1, le=100),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return get_discover_job(db, job_id, page=page, page_size=page_size).model_dump()
|
||||||
|
|
||||||
|
|
||||||
# --- Tree / folders ---------------------------------------------------------
|
# --- Tree / folders ---------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ from .topology_common import (
|
||||||
ROOT_FOLDER_NAME,
|
ROOT_FOLDER_NAME,
|
||||||
VIEW_GRAPH_EDGE_HARD_CAP,
|
VIEW_GRAPH_EDGE_HARD_CAP,
|
||||||
VIEW_GRAPH_NODE_HARD_CAP,
|
VIEW_GRAPH_NODE_HARD_CAP,
|
||||||
|
_EDGE_STATUS_MISSING,
|
||||||
|
_EDGE_STATUS_MISSING_COMPAT,
|
||||||
_LEGACY_UNASSIGNED_NAME,
|
_LEGACY_UNASSIGNED_NAME,
|
||||||
_normalize_edge_status,
|
_normalize_edge_status,
|
||||||
_utcnow,
|
_utcnow,
|
||||||
|
|
@ -108,6 +110,39 @@ def _managed_source_for_node(db: Session, n: TopoFabricNode) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _batch_ne_lookups(
|
||||||
|
db: Session, fabric_nodes: dict[str, TopoFabricNode]
|
||||||
|
) -> tuple[dict[str, ManagedNE], dict[str, UmeInventoryNE]]:
|
||||||
|
mids = sorted(
|
||||||
|
{
|
||||||
|
str(fn.managed_ne_id or "").strip()
|
||||||
|
for fn in fabric_nodes.values()
|
||||||
|
if str(fn.managed_ne_id or "").strip()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
uids = sorted(
|
||||||
|
{
|
||||||
|
str(fn.ume_ne_id or "").strip()
|
||||||
|
for fn in fabric_nodes.values()
|
||||||
|
if str(fn.ume_ne_id or "").strip()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
managed = (
|
||||||
|
{str(n.id): n for n in db.query(ManagedNE).filter(ManagedNE.id.in_(mids)).all()}
|
||||||
|
if mids
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
ume = (
|
||||||
|
{
|
||||||
|
str(u.ne_id): u
|
||||||
|
for u in db.query(UmeInventoryNE).filter(UmeInventoryNE.ne_id.in_(uids)).all()
|
||||||
|
}
|
||||||
|
if uids
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
return managed, ume
|
||||||
|
|
||||||
|
|
||||||
def get_view_graph(db: Session, view_id: str) -> TopologyViewGraphOut:
|
def get_view_graph(db: Session, view_id: str) -> TopologyViewGraphOut:
|
||||||
view = _get_view_or_404(db, view_id)
|
view = _get_view_or_404(db, view_id)
|
||||||
vnodes = db.query(TopoViewNode).filter(TopoViewNode.view_id == view.id).all()
|
vnodes = db.query(TopoViewNode).filter(TopoViewNode.view_id == view.id).all()
|
||||||
|
|
@ -121,6 +156,7 @@ def get_view_graph(db: Session, view_id: str) -> TopologyViewGraphOut:
|
||||||
fabric_nodes = {
|
fabric_nodes = {
|
||||||
n.id: n for n in db.query(TopoFabricNode).filter(TopoFabricNode.id.in_(fids)).all()
|
n.id: n for n in db.query(TopoFabricNode).filter(TopoFabricNode.id.in_(fids)).all()
|
||||||
} if fids else {}
|
} if fids else {}
|
||||||
|
managed_by_id, ume_by_id = _batch_ne_lookups(db, fabric_nodes)
|
||||||
filt = dict(view.filter or {})
|
filt = dict(view.filter or {})
|
||||||
layer = str(filt.get("layer") or "physical").strip() or "physical"
|
layer = str(filt.get("layer") or "physical").strip() or "physical"
|
||||||
status = str(filt.get("status") or "").strip().lower()
|
status = str(filt.get("status") or "").strip().lower()
|
||||||
|
|
@ -130,6 +166,17 @@ def get_view_graph(db: Session, view_id: str) -> TopologyViewGraphOut:
|
||||||
label = (vn.label or "").strip()
|
label = (vn.label or "").strip()
|
||||||
if not label and fn is not None:
|
if not label and fn is not None:
|
||||||
label = (fn.name or fn.ip or vn.fabric_node_id)[:256]
|
label = (fn.name or fn.ip or vn.fabric_node_id)[:256]
|
||||||
|
connect_status = ""
|
||||||
|
managed_source = ""
|
||||||
|
if fn is not None:
|
||||||
|
mid = str(fn.managed_ne_id or "").strip()
|
||||||
|
uid = str(fn.ume_ne_id or "").strip()
|
||||||
|
if mid and mid in managed_by_id:
|
||||||
|
mne = managed_by_id[mid]
|
||||||
|
connect_status = mne.connect_status or ""
|
||||||
|
managed_source = str(mne.source or "").strip()
|
||||||
|
elif uid and uid in ume_by_id:
|
||||||
|
connect_status = ume_by_id[uid].connection_status or ""
|
||||||
nodes_out.append(
|
nodes_out.append(
|
||||||
ViewNodeOut(
|
ViewNodeOut(
|
||||||
fabric_node_id=vn.fabric_node_id,
|
fabric_node_id=vn.fabric_node_id,
|
||||||
|
|
@ -143,8 +190,8 @@ def get_view_graph(db: Session, view_id: str) -> TopologyViewGraphOut:
|
||||||
ip=(fn.ip if fn else "") or "",
|
ip=(fn.ip if fn else "") or "",
|
||||||
vendor=(fn.vendor if fn else "") or "",
|
vendor=(fn.vendor if fn else "") or "",
|
||||||
device_type=(fn.device_type if fn else "") or "",
|
device_type=(fn.device_type if fn else "") or "",
|
||||||
connect_status=_connect_status_for_node(db, fn) if fn else "",
|
connect_status=connect_status,
|
||||||
managed_source=_managed_source_for_node(db, fn) if fn else "",
|
managed_source=managed_source,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
edges_out: list[ViewEdgeOut] = []
|
edges_out: list[ViewEdgeOut] = []
|
||||||
|
|
@ -925,7 +972,10 @@ def project_fabric_neighbors_to_view(
|
||||||
view = _get_view_or_404(db, view_id)
|
view = _get_view_or_404(db, view_id)
|
||||||
mem = _membership_for_view(view)
|
mem = _membership_for_view(view)
|
||||||
if bool(mem.get("frozen")):
|
if bool(mem.get("frozen")):
|
||||||
return get_view_graph(db, view.id)
|
g = get_view_graph(db, view.id)
|
||||||
|
g.truncated = True
|
||||||
|
g.truncate_reason = "membership_frozen"
|
||||||
|
return g
|
||||||
|
|
||||||
max_nodes = int(mem.get("max_nodes") or 300)
|
max_nodes = int(mem.get("max_nodes") or 300)
|
||||||
hops = int(mem.get("expand_hops") or 1)
|
hops = int(mem.get("expand_hops") or 1)
|
||||||
|
|
@ -934,14 +984,34 @@ def project_fabric_neighbors_to_view(
|
||||||
req = body or ViewProjectNeighborsRequest()
|
req = body or ViewProjectNeighborsRequest()
|
||||||
|
|
||||||
vnodes = db.query(TopoViewNode).filter(TopoViewNode.view_id == view.id).all()
|
vnodes = db.query(TopoViewNode).filter(TopoViewNode.view_id == view.id).all()
|
||||||
|
fids_on_view = [vn.fabric_node_id for vn in vnodes]
|
||||||
|
fabric_on_view = (
|
||||||
|
{
|
||||||
|
n.id: n
|
||||||
|
for n in db.query(TopoFabricNode).filter(TopoFabricNode.id.in_(fids_on_view)).all()
|
||||||
|
}
|
||||||
|
if fids_on_view
|
||||||
|
else {}
|
||||||
|
)
|
||||||
# Drop placements pointing at missing fabric rows only (keep LLDP placeholders).
|
# Drop placements pointing at missing fabric rows only (keep LLDP placeholders).
|
||||||
orphan_vns = [vn for vn in vnodes if db.get(TopoFabricNode, vn.fabric_node_id) is None]
|
orphan_vns = [vn for vn in vnodes if vn.fabric_node_id not in fabric_on_view]
|
||||||
if orphan_vns:
|
if orphan_vns:
|
||||||
for vn in orphan_vns:
|
for vn in orphan_vns:
|
||||||
db.delete(vn)
|
db.delete(vn)
|
||||||
view.updated_at = _utcnow()
|
view.updated_at = _utcnow()
|
||||||
db.commit()
|
db.commit()
|
||||||
vnodes = db.query(TopoViewNode).filter(TopoViewNode.view_id == view.id).all()
|
vnodes = db.query(TopoViewNode).filter(TopoViewNode.view_id == view.id).all()
|
||||||
|
fids_on_view = [vn.fabric_node_id for vn in vnodes]
|
||||||
|
fabric_on_view = (
|
||||||
|
{
|
||||||
|
n.id: n
|
||||||
|
for n in db.query(TopoFabricNode)
|
||||||
|
.filter(TopoFabricNode.id.in_(fids_on_view))
|
||||||
|
.all()
|
||||||
|
}
|
||||||
|
if fids_on_view
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
existing = {vn.fabric_node_id for vn in vnodes}
|
existing = {vn.fabric_node_id for vn in vnodes}
|
||||||
if not existing:
|
if not existing:
|
||||||
|
|
@ -955,13 +1025,12 @@ def project_fabric_neighbors_to_view(
|
||||||
seed_ids: set[str] = {
|
seed_ids: set[str] = {
|
||||||
str(x).strip() for x in (req.seed_fabric_node_ids or []) if str(x).strip()
|
str(x).strip() for x in (req.seed_fabric_node_ids or []) if str(x).strip()
|
||||||
}
|
}
|
||||||
for mid in req.managed_ne_ids or []:
|
want_mids = {
|
||||||
mid_s = str(mid or "").strip()
|
str(mid or "").strip() for mid in (req.managed_ne_ids or []) if str(mid or "").strip()
|
||||||
if not mid_s:
|
}
|
||||||
continue
|
if want_mids:
|
||||||
for fid in existing:
|
for fid, fn in fabric_on_view.items():
|
||||||
fn = db.get(TopoFabricNode, fid)
|
if str(fn.managed_ne_id or "").strip() in want_mids:
|
||||||
if fn is not None and str(fn.managed_ne_id or "").strip() == mid_s:
|
|
||||||
seed_ids.add(fid)
|
seed_ids.add(fid)
|
||||||
if seed_ids:
|
if seed_ids:
|
||||||
seed_ids &= existing
|
seed_ids &= existing
|
||||||
|
|
@ -971,22 +1040,30 @@ def project_fabric_neighbors_to_view(
|
||||||
seed_ids = set(existing)
|
seed_ids = set(existing)
|
||||||
|
|
||||||
peer_ids = _neighbor_ids(db, seed_ids=seed_ids, layer=layer, hops=hops)
|
peer_ids = _neighbor_ids(db, seed_ids=seed_ids, layer=layer, hops=hops)
|
||||||
to_add: list[str] = []
|
peer_rows = (
|
||||||
|
{
|
||||||
|
n.id: n
|
||||||
|
for n in db.query(TopoFabricNode).filter(TopoFabricNode.id.in_(list(peer_ids))).all()
|
||||||
|
}
|
||||||
|
if peer_ids
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
eligible: list[str] = []
|
||||||
for peer in sorted(peer_ids):
|
for peer in sorted(peer_ids):
|
||||||
if peer in existing:
|
if peer in existing:
|
||||||
continue
|
continue
|
||||||
fn = db.get(TopoFabricNode, peer)
|
fn = peer_rows.get(peer)
|
||||||
if fn is None or not _is_inventory_node(fn):
|
if fn is None or not _is_inventory_node(fn):
|
||||||
continue
|
continue
|
||||||
if _fabric_match_score(db, fn) < 2:
|
if _fabric_match_score(db, fn) < 2:
|
||||||
continue
|
continue
|
||||||
if not _fabric_in_hard_scope(db, fn, mem):
|
if not _fabric_in_hard_scope(db, fn, mem):
|
||||||
continue
|
continue
|
||||||
to_add.append(peer)
|
eligible.append(peer)
|
||||||
if len(existing) + len(to_add) >= max_nodes:
|
|
||||||
break
|
|
||||||
|
|
||||||
truncated = len(peer_ids) > len(to_add)
|
room = max(0, max_nodes - len(existing))
|
||||||
|
to_add = eligible[:room]
|
||||||
|
truncated = len(eligible) > len(to_add)
|
||||||
if to_add:
|
if to_add:
|
||||||
_place_fabric_ids_on_view(
|
_place_fabric_ids_on_view(
|
||||||
db, view, to_add, existing=existing, near_fabric_ids=seed_ids
|
db, view, to_add, existing=existing, near_fabric_ids=seed_ids
|
||||||
|
|
|
||||||
|
|
@ -705,6 +705,14 @@ class FabricTopologyTests(unittest.TestCase):
|
||||||
self.assertIn(nodes[2].id, ids2)
|
self.assertIn(nodes[2].id, ids2)
|
||||||
self.assertNotIn(nodes[3].id, ids2)
|
self.assertNotIn(nodes[3].id, ids2)
|
||||||
|
|
||||||
|
# Peers already on canvas must not raise a false truncated banner.
|
||||||
|
g3 = svc.project_fabric_neighbors_to_view(
|
||||||
|
self.db,
|
||||||
|
view2.id,
|
||||||
|
ViewProjectNeighborsRequest(seed_fabric_node_ids=[nodes[0].id]),
|
||||||
|
)
|
||||||
|
self.assertFalse(g3.truncated)
|
||||||
|
|
||||||
pop = svc.populate_view(
|
pop = svc.populate_view(
|
||||||
self.db,
|
self.db,
|
||||||
view.id,
|
view.id,
|
||||||
|
|
|
||||||
|
|
@ -1413,9 +1413,13 @@ const en = {
|
||||||
edgeDeleted: "Deleted {{count}} link(s)",
|
edgeDeleted: "Deleted {{count}} link(s)",
|
||||||
discoverCancelled: "Discovery cancelled",
|
discoverCancelled: "Discovery cancelled",
|
||||||
truncatedMembership: "Membership cap reached; some neighbors were not placed. Use classify/slices or a new map.",
|
truncatedMembership: "Membership cap reached; some neighbors were not placed. Use classify/slices or a new map.",
|
||||||
|
truncatedFrozen: "This map membership is frozen; neighbors cannot be projected. Unfreeze in classify/slices or create a new map.",
|
||||||
truncatedNodes: "Too many view nodes; display truncated. Narrow membership or create another map.",
|
truncatedNodes: "Too many view nodes; display truncated. Narrow membership or create another map.",
|
||||||
truncatedEdges: "Too many edges; display truncated. Filter status or create another map.",
|
truncatedEdges: "Too many edges; display truncated. Filter status or create another map.",
|
||||||
truncatedGeneric: "Graph data truncated. Narrow scope or create another map.",
|
truncatedGeneric: "Graph data truncated. Narrow scope or create another map.",
|
||||||
|
deleteSelectionConfirm:
|
||||||
|
"Remove {{nodes}} NE(s) from this map and delete {{edges}} Fabric link(s)?",
|
||||||
|
selectionDeleted: "Removed {{nodes}} NE(s) and deleted {{edges}} link(s)",
|
||||||
newMap: "New",
|
newMap: "New",
|
||||||
newMapName: "New topology",
|
newMapName: "New topology",
|
||||||
rename: "Rename",
|
rename: "Rename",
|
||||||
|
|
|
||||||
|
|
@ -1404,9 +1404,12 @@ const zh = {
|
||||||
edgeDeleted: "已删除 {{count}} 条链路",
|
edgeDeleted: "已删除 {{count}} 条链路",
|
||||||
discoverCancelled: "发现已取消",
|
discoverCancelled: "发现已取消",
|
||||||
truncatedMembership: "视图已达成员上限,部分邻居未上图。可去分类/切片或新建视图。",
|
truncatedMembership: "视图已达成员上限,部分邻居未上图。可去分类/切片或新建视图。",
|
||||||
|
truncatedFrozen: "当前视图已冻结成员,无法投影邻居。请在分类/切片中解冻或新建视图。",
|
||||||
truncatedNodes: "画布节点过多已截断显示。建议新建视图或收紧成员范围。",
|
truncatedNodes: "画布节点过多已截断显示。建议新建视图或收紧成员范围。",
|
||||||
truncatedEdges: "画布链路过多已截断显示。建议筛选状态或新建视图。",
|
truncatedEdges: "画布链路过多已截断显示。建议筛选状态或新建视图。",
|
||||||
truncatedGeneric: "图数据已截断显示。建议收紧范围或新建视图。",
|
truncatedGeneric: "图数据已截断显示。建议收紧范围或新建视图。",
|
||||||
|
deleteSelectionConfirm: "删除选中的 {{nodes}} 个网元(移出画布)和 {{edges}} 条链路(从 Fabric 删除)?",
|
||||||
|
selectionDeleted: "已移除 {{nodes}} 个网元、删除 {{edges}} 条链路",
|
||||||
newMap: "新建",
|
newMap: "新建",
|
||||||
newMapName: "新拓扑图",
|
newMapName: "新拓扑图",
|
||||||
rename: "重命名",
|
rename: "重命名",
|
||||||
|
|
|
||||||
|
|
@ -1580,7 +1580,7 @@ export function TopologyPage() {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
job = await fetchLldpDiscoverJob(jobStart.id);
|
job = await fetchLldpDiscoverJob(jobStart.id, { page: 1, pageSize: 5 });
|
||||||
setDiscoverProgress((p) => ({
|
setDiscoverProgress((p) => ({
|
||||||
...p,
|
...p,
|
||||||
index: job.done,
|
index: job.done,
|
||||||
|
|
@ -1619,12 +1619,21 @@ export function TopologyPage() {
|
||||||
if (job.status === "failed") {
|
if (job.status === "failed") {
|
||||||
throw new Error(job.error || "discover_failed");
|
throw new Error(job.error || "discover_failed");
|
||||||
}
|
}
|
||||||
|
// Final page: pull a fuller item slice for the summary panel.
|
||||||
|
try {
|
||||||
|
job = await fetchLldpDiscoverJob(jobStart.id, { page: 1, pageSize: 100 });
|
||||||
|
} catch {
|
||||||
|
/* keep last polled job */
|
||||||
|
}
|
||||||
const projected = discoverProjectNeighbors
|
const projected = discoverProjectNeighbors
|
||||||
? await projectTopologyNeighbors(mapId, {
|
? await projectTopologyNeighbors(mapId, {
|
||||||
seed_fabric_node_ids: scoped.map((n) => n.id),
|
seed_fabric_node_ids: scoped.map((n) => n.id),
|
||||||
})
|
})
|
||||||
: await fetchTopologyGraph(mapId);
|
: await fetchTopologyGraph(mapId);
|
||||||
queryClient.setQueryData(queryKeys.topologyGraph(mapId), projected);
|
queryClient.setQueryData(queryKeys.topologyGraph(mapId), projected);
|
||||||
|
if (discoverProjectNeighbors && projected.truncate_reason === "membership_frozen") {
|
||||||
|
showError(t("topology.truncatedFrozen"));
|
||||||
|
}
|
||||||
appliedMapIdRef.current = mapId;
|
appliedMapIdRef.current = mapId;
|
||||||
let { rfNodes, rfEdges } = graphToFlow(projected.nodes, projected.edges, edgeDefaults);
|
let { rfNodes, rfEdges } = graphToFlow(projected.nodes, projected.edges, edgeDefaults);
|
||||||
// Keep existing node positions when we did not auto-layout.
|
// Keep existing node positions when we did not auto-layout.
|
||||||
|
|
@ -2021,10 +2030,15 @@ export function TopologyPage() {
|
||||||
}, [setNodes, setEdges]);
|
}, [setNodes, setEdges]);
|
||||||
|
|
||||||
const persistDeleteEdges = useCallback(
|
const persistDeleteEdges = useCallback(
|
||||||
async (edgeIds: string[], opts?: { confirmKey?: string; okKey?: string }) => {
|
async (
|
||||||
|
edgeIds: string[],
|
||||||
|
opts?: { confirmKey?: string; okKey?: string; skipConfirm?: boolean },
|
||||||
|
) => {
|
||||||
if (!mapId || !edgeIds.length) return false;
|
if (!mapId || !edgeIds.length) return false;
|
||||||
|
if (!opts?.skipConfirm) {
|
||||||
const confirmMsg = t(opts?.confirmKey || "topology.deleteEdgeConfirm");
|
const confirmMsg = t(opts?.confirmKey || "topology.deleteEdgeConfirm");
|
||||||
if (!window.confirm(confirmMsg)) return false;
|
if (!window.confirm(confirmMsg)) return false;
|
||||||
|
}
|
||||||
pushHistory();
|
pushHistory();
|
||||||
try {
|
try {
|
||||||
await deleteFabricEdges(edgeIds);
|
await deleteFabricEdges(edgeIds);
|
||||||
|
|
@ -2061,10 +2075,44 @@ export function TopologyPage() {
|
||||||
if (e.selected) edgeIds.add(e.id);
|
if (e.selected) edgeIds.add(e.id);
|
||||||
}
|
}
|
||||||
if (!nodeIds.length && !edgeIds.size) return;
|
if (!nodeIds.length && !edgeIds.size) return;
|
||||||
|
|
||||||
|
if (nodeIds.length && edgeIds.size) {
|
||||||
|
const msg = t("topology.deleteSelectionConfirm")
|
||||||
|
.replace("{{nodes}}", String(nodeIds.length))
|
||||||
|
.replace("{{edges}}", String(edgeIds.size));
|
||||||
|
if (!window.confirm(msg)) return;
|
||||||
|
pushHistory();
|
||||||
|
try {
|
||||||
|
if (dirtyRef.current) {
|
||||||
|
await patchTopologyPositions(mapId, flowToPositions(nodes));
|
||||||
|
clearDirty();
|
||||||
|
}
|
||||||
|
await deleteFabricEdges([...edgeIds]);
|
||||||
|
const localPos = new Map(nodes.map((n) => [n.id, n.position]));
|
||||||
|
const graph = await removeTopologyViewNodes(mapId, nodeIds);
|
||||||
|
queryClient.setQueryData(queryKeys.topologyGraph(mapId), graph);
|
||||||
|
appliedMapIdRef.current = mapId;
|
||||||
|
historyLockRef.current = true;
|
||||||
|
applyViewGraph(graph, edgeDefaults, setNodes, setEdges, localPos);
|
||||||
|
historyLockRef.current = false;
|
||||||
|
clearDirty();
|
||||||
|
setSelectedEdgeId(null);
|
||||||
|
showOk(
|
||||||
|
t("topology.selectionDeleted")
|
||||||
|
.replace("{{nodes}}", String(nodeIds.length))
|
||||||
|
.replace("{{edges}}", String(edgeIds.size)),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
showError(String(err));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!nodeIds.length && edgeIds.size) {
|
if (!nodeIds.length && edgeIds.size) {
|
||||||
await persistDeleteEdges([...edgeIds]);
|
await persistDeleteEdges([...edgeIds]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushHistory();
|
pushHistory();
|
||||||
try {
|
try {
|
||||||
if (dirtyRef.current) {
|
if (dirtyRef.current) {
|
||||||
|
|
@ -2095,7 +2143,9 @@ export function TopologyPage() {
|
||||||
edgeDefaults,
|
edgeDefaults,
|
||||||
clearDirty,
|
clearDirty,
|
||||||
showError,
|
showError,
|
||||||
|
showOk,
|
||||||
persistDeleteEdges,
|
persistDeleteEdges,
|
||||||
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const removeEdgeById = (edgeId: string) => {
|
const removeEdgeById = (edgeId: string) => {
|
||||||
|
|
@ -2135,6 +2185,10 @@ export function TopologyPage() {
|
||||||
applyViewGraph(projected, edgeDefaults, setNodes, setEdges, localPos);
|
applyViewGraph(projected, edgeDefaults, setNodes, setEdges, localPos);
|
||||||
historyLockRef.current = false;
|
historyLockRef.current = false;
|
||||||
clearDirty();
|
clearDirty();
|
||||||
|
if (projected.truncate_reason === "membership_frozen") {
|
||||||
|
showError(t("topology.truncatedFrozen"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const added = Math.max(0, projected.nodes.length - before);
|
const added = Math.max(0, projected.nodes.length - before);
|
||||||
showOk(t("topology.projectedNeighbors").replace("{{count}}", String(added)));
|
showOk(t("topology.projectedNeighbors").replace("{{count}}", String(added)));
|
||||||
if (added > 0) {
|
if (added > 0) {
|
||||||
|
|
@ -2668,6 +2722,7 @@ export function TopologyPage() {
|
||||||
const truncateBannerText = useMemo(() => {
|
const truncateBannerText = useMemo(() => {
|
||||||
if (!graphTruncated) return "";
|
if (!graphTruncated) return "";
|
||||||
if (truncateReason === "membership_cap") return t("topology.truncatedMembership");
|
if (truncateReason === "membership_cap") return t("topology.truncatedMembership");
|
||||||
|
if (truncateReason === "membership_frozen") return t("topology.truncatedFrozen");
|
||||||
if (truncateReason === "too_many_view_nodes") return t("topology.truncatedNodes");
|
if (truncateReason === "too_many_view_nodes") return t("topology.truncatedNodes");
|
||||||
if (truncateReason === "too_many_edges") return t("topology.truncatedEdges");
|
if (truncateReason === "too_many_edges") return t("topology.truncatedEdges");
|
||||||
return t("topology.truncatedGeneric");
|
return t("topology.truncatedGeneric");
|
||||||
|
|
|
||||||
|
|
@ -1225,8 +1225,17 @@ export const startLldpDiscover = (body?: {
|
||||||
trigger_mode?: "manual" | "schedule" | "topology";
|
trigger_mode?: "manual" | "schedule" | "topology";
|
||||||
}) => apiPost<TopologyDiscoverJob>("/v1/topology/fabric/discover", body || {});
|
}) => apiPost<TopologyDiscoverJob>("/v1/topology/fabric/discover", body || {});
|
||||||
|
|
||||||
export const fetchLldpDiscoverJob = (jobId: string) =>
|
export const fetchLldpDiscoverJob = (
|
||||||
apiGet<TopologyDiscoverJob>(`/v1/topology/fabric/discover/${encodeURIComponent(jobId)}`);
|
jobId: string,
|
||||||
|
params?: { page?: number; pageSize?: number },
|
||||||
|
) => {
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
p.set("page", String(Math.max(1, Number(params?.page || 1))));
|
||||||
|
p.set("page_size", String(Math.max(1, Math.min(100, Number(params?.pageSize || 20)))));
|
||||||
|
return apiGet<TopologyDiscoverJob>(
|
||||||
|
`/v1/topology/fabric/discover/${encodeURIComponent(jobId)}?${p.toString()}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchLldpCollectDashboard = () =>
|
export const fetchLldpCollectDashboard = () =>
|
||||||
apiGet<LldpCollectDashboard>("/v1/topology/lldp-collect/dashboard");
|
apiGet<LldpCollectDashboard>("/v1/topology/lldp-collect/dashboard");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue