mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 05:43:20 +08:00
Count directory NE badges from fabric inventory, with shared whole-network totals.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
617ab469ae
commit
104de449cf
4 changed files with 113 additions and 43 deletions
|
|
@ -228,6 +228,8 @@ class TopologyTreeFolderOut(BaseModel):
|
||||||
sort_order: int = 0
|
sort_order: int = 0
|
||||||
is_system: bool = False
|
is_system: bool = False
|
||||||
external_ref: str = ""
|
external_ref: str = ""
|
||||||
|
# Real NE inventory under this folder (subtree). Region folders are not NEs.
|
||||||
|
ne_count: int = 0
|
||||||
views: list[TopologyTreeViewOut] = Field(default_factory=list)
|
views: list[TopologyTreeViewOut] = Field(default_factory=list)
|
||||||
children: list["TopologyTreeFolderOut"] = Field(default_factory=list)
|
children: list["TopologyTreeFolderOut"] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -691,25 +691,30 @@ def get_topology_tree(db: Session) -> TopologyTreeOut:
|
||||||
)
|
)
|
||||||
for v in views
|
for v in views
|
||||||
)
|
)
|
||||||
ume_ne_by_folder: dict[str, int] = {}
|
# Fabric NE inventory by owning region folder (regions themselves are never fabric NEs).
|
||||||
|
ne_by_folder: dict[str, int] = {
|
||||||
|
str(fid): int(cnt or 0)
|
||||||
|
for fid, cnt in (
|
||||||
|
db.query(TopoFabricNode.region_folder_id, func.count(TopoFabricNode.id))
|
||||||
|
.filter(
|
||||||
|
TopoFabricNode.region_folder_id.isnot(None),
|
||||||
|
TopoFabricNode.region_folder_id != "",
|
||||||
|
or_(
|
||||||
|
and_(TopoFabricNode.ume_ne_id.isnot(None), TopoFabricNode.ume_ne_id != ""),
|
||||||
|
and_(
|
||||||
|
TopoFabricNode.managed_ne_id.isnot(None),
|
||||||
|
TopoFabricNode.managed_ne_id != "",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.group_by(TopoFabricNode.region_folder_id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
if str(fid or "").strip()
|
||||||
|
}
|
||||||
ume_ne_by_sbn: dict[str, int] = {}
|
ume_ne_by_sbn: dict[str, int] = {}
|
||||||
ume_ne_total = 0
|
ume_ne_total = 0
|
||||||
if has_ume_dirs:
|
if has_ume_dirs:
|
||||||
ume_ne_by_folder = {
|
|
||||||
str(fid): int(cnt or 0)
|
|
||||||
for fid, cnt in (
|
|
||||||
db.query(TopoFabricNode.region_folder_id, func.count(TopoFabricNode.id))
|
|
||||||
.filter(
|
|
||||||
TopoFabricNode.ume_ne_id.isnot(None),
|
|
||||||
TopoFabricNode.ume_ne_id != "",
|
|
||||||
TopoFabricNode.region_folder_id.isnot(None),
|
|
||||||
TopoFabricNode.region_folder_id != "",
|
|
||||||
)
|
|
||||||
.group_by(TopoFabricNode.region_folder_id)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
if str(fid or "").strip()
|
|
||||||
}
|
|
||||||
ume_ne_by_sbn = {
|
ume_ne_by_sbn = {
|
||||||
str(parent): int(cnt or 0)
|
str(parent): int(cnt or 0)
|
||||||
for parent, cnt in (
|
for parent, cnt in (
|
||||||
|
|
@ -731,38 +736,75 @@ def get_topology_tree(db: Session) -> TopologyTreeOut:
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
|
|
||||||
def _view_node_count(v: TopoView) -> int:
|
|
||||||
raw = int(nc_map.get(v.id, 0) or 0)
|
|
||||||
ne_nc = max(0, raw - int(region_icon_map.get(v.id, 0) or 0))
|
|
||||||
if ne_nc:
|
|
||||||
return ne_nc
|
|
||||||
filt = view_filter_dict(v.filter)
|
|
||||||
if filt.get("world_flat") or (
|
|
||||||
bool(filt.get("world")) and not filt.get("ume_level")
|
|
||||||
):
|
|
||||||
return ume_ne_total
|
|
||||||
if filt.get("ume_level"):
|
|
||||||
sid = str(filt.get("sbn_id") or "").strip()
|
|
||||||
if sid:
|
|
||||||
return int(ume_ne_by_sbn.get(sid, 0))
|
|
||||||
# World drill root: directory hint only (open canvas for real graph).
|
|
||||||
if str(filt.get("parent") or "") == "md":
|
|
||||||
return ume_ne_total
|
|
||||||
if v.folder_id:
|
|
||||||
return int(ume_ne_by_folder.get(str(v.folder_id), 0))
|
|
||||||
if v.folder_id:
|
|
||||||
return int(ume_ne_by_folder.get(str(v.folder_id), 0))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
by_parent: dict[str, list[TopoFolder]] = {}
|
by_parent: dict[str, list[TopoFolder]] = {}
|
||||||
|
folder_by_id: dict[str, TopoFolder] = {}
|
||||||
root: TopoFolder | None = None
|
root: TopoFolder | None = None
|
||||||
for f in folders:
|
for f in folders:
|
||||||
|
folder_by_id[str(f.id)] = f
|
||||||
if str(f.kind or "") == "root":
|
if str(f.kind or "") == "root":
|
||||||
root = f
|
root = f
|
||||||
continue
|
continue
|
||||||
pid = str(f.parent_id or "")
|
pid = str(f.parent_id or "")
|
||||||
by_parent.setdefault(pid, []).append(f)
|
by_parent.setdefault(pid, []).append(f)
|
||||||
|
|
||||||
|
root_id = str(root.id) if root is not None else ""
|
||||||
|
|
||||||
|
def _is_manual_root_map(folder: TopoFolder) -> bool:
|
||||||
|
ext = str(getattr(folder, "external_ref", None) or "").strip()
|
||||||
|
if ext:
|
||||||
|
return False
|
||||||
|
if not bool(folder.is_system):
|
||||||
|
return False
|
||||||
|
return is_manual_root_map_name(folder.name)
|
||||||
|
|
||||||
|
def _is_whole_network_folder(folder: TopoFolder) -> bool:
|
||||||
|
"""根 / 根图 / UME World / World — same inventory, different presentation."""
|
||||||
|
from .ume_topology_world import is_ume_world_container, is_world_drill_folder
|
||||||
|
|
||||||
|
if is_ume_world_container(folder) or is_world_drill_folder(folder):
|
||||||
|
return True
|
||||||
|
ext = str(getattr(folder, "external_ref", None) or "").strip()
|
||||||
|
if ext.startswith("ume:"):
|
||||||
|
return False
|
||||||
|
parent = str(folder.parent_id or "").strip()
|
||||||
|
# Top-level manual「根」(nav-only under system root).
|
||||||
|
if root_id and parent == root_id and not ext:
|
||||||
|
return True
|
||||||
|
return _is_manual_root_map(folder)
|
||||||
|
|
||||||
|
def _subtree_inventory(folder_id: str) -> int:
|
||||||
|
"""Directory semantics: NEs owned by this folder + all descendant folders."""
|
||||||
|
total = int(ne_by_folder.get(folder_id, 0))
|
||||||
|
for child in by_parent.get(folder_id, []):
|
||||||
|
total += _subtree_inventory(str(child.id))
|
||||||
|
return total
|
||||||
|
|
||||||
|
def _folder_ne_count(folder: TopoFolder) -> int:
|
||||||
|
if _is_whole_network_folder(folder):
|
||||||
|
return int(ume_ne_total or 0)
|
||||||
|
return _subtree_inventory(str(folder.id))
|
||||||
|
|
||||||
|
def _view_node_count(v: TopoView) -> int:
|
||||||
|
folder = folder_by_id.get(str(v.folder_id or ""))
|
||||||
|
if folder is not None and _is_whole_network_folder(folder):
|
||||||
|
return int(ume_ne_total or 0)
|
||||||
|
filt = view_filter_dict(v.filter)
|
||||||
|
if filt.get("world_flat") or (
|
||||||
|
bool(filt.get("world")) and not filt.get("ume_level")
|
||||||
|
):
|
||||||
|
return int(ume_ne_total or 0)
|
||||||
|
if filt.get("ume_level"):
|
||||||
|
sid = str(filt.get("sbn_id") or "").strip()
|
||||||
|
if sid:
|
||||||
|
return int(ume_ne_by_sbn.get(sid, 0))
|
||||||
|
if str(filt.get("parent") or "") == "md":
|
||||||
|
return int(ume_ne_total or 0)
|
||||||
|
if folder is not None:
|
||||||
|
return _folder_ne_count(folder)
|
||||||
|
# Fallback: membership minus region icons (never count region placeholders as NEs).
|
||||||
|
raw = int(nc_map.get(v.id, 0) or 0)
|
||||||
|
return max(0, raw - int(region_icon_map.get(v.id, 0) or 0))
|
||||||
|
|
||||||
views_by_folder: dict[str, list[TopoView]] = {}
|
views_by_folder: dict[str, list[TopoView]] = {}
|
||||||
for v in views:
|
for v in views:
|
||||||
views_by_folder.setdefault(str(v.folder_id or ""), []).append(v)
|
views_by_folder.setdefault(str(v.folder_id or ""), []).append(v)
|
||||||
|
|
@ -821,6 +863,7 @@ def get_topology_tree(db: Session) -> TopologyTreeOut:
|
||||||
sort_order=int(folder.sort_order or 0),
|
sort_order=int(folder.sort_order or 0),
|
||||||
is_system=bool(folder.is_system),
|
is_system=bool(folder.is_system),
|
||||||
external_ref=str(getattr(folder, "external_ref", None) or ""),
|
external_ref=str(getattr(folder, "external_ref", None) or ""),
|
||||||
|
ne_count=_folder_ne_count(folder),
|
||||||
views=_flat_views(folder, views_by_folder.get(folder.id, [])),
|
views=_flat_views(folder, views_by_folder.get(folder.id, [])),
|
||||||
children=kids,
|
children=kids,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -297,12 +297,35 @@ function regionDisplayName(
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Real NE count under a folder (own views + nested children). Backend node_count already excludes region icons. */
|
/** Prefer the folder's primary canvas view (physical / world flat / World drill). */
|
||||||
|
function primaryTreeView(
|
||||||
|
folder: TopologyTreeFolderItem | null | undefined,
|
||||||
|
): TopologyTreeViewItem | null {
|
||||||
|
if (!folder) return null;
|
||||||
|
const views = folder.views || [];
|
||||||
|
if (!views.length) return null;
|
||||||
|
if (isWorldDrillFolder(folder)) {
|
||||||
|
return views.find((v) => v.name === "World") || views[0] || null;
|
||||||
|
}
|
||||||
|
if (isUmeWorldContainer(folder)) {
|
||||||
|
return views.find((v) => isWorldFlatViewName(v.name)) || views[0] || null;
|
||||||
|
}
|
||||||
|
return views[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory NE badge from API: fabric inventory under the folder.
|
||||||
|
* 根 / 根图 / UME World / World share the same whole-network total; nested regions
|
||||||
|
* sum owned NEs in their subtree. Region folders themselves never count as NEs.
|
||||||
|
*/
|
||||||
function folderNeCount(folder: TopologyTreeFolderItem | null | undefined): number {
|
function folderNeCount(folder: TopologyTreeFolderItem | null | undefined): number {
|
||||||
if (!folder) return 0;
|
if (!folder) return 0;
|
||||||
const own = (folder.views || []).reduce((sum, v) => sum + (Number(v.node_count) || 0), 0);
|
if (folder.ne_count != null && Number.isFinite(Number(folder.ne_count))) {
|
||||||
const nested = (folder.children || []).reduce((sum, c) => sum + folderNeCount(c), 0);
|
return Math.max(0, Math.floor(Number(folder.ne_count) || 0));
|
||||||
return own + nested;
|
}
|
||||||
|
// Legacy fallback before API ne_count: primary view only (no recursive sum).
|
||||||
|
const primary = primaryTreeView(folder);
|
||||||
|
return primary ? Math.max(0, Math.floor(Number(primary.node_count) || 0)) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNeCount(count: number): string {
|
function formatNeCount(count: number): string {
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,8 @@ export type TopologyTreeFolderItem = {
|
||||||
sort_order: number;
|
sort_order: number;
|
||||||
is_system: boolean;
|
is_system: boolean;
|
||||||
external_ref?: string;
|
external_ref?: string;
|
||||||
|
/** Real NE inventory under this folder (subtree). Region folders are not NEs. */
|
||||||
|
ne_count?: number;
|
||||||
views: TopologyTreeViewItem[];
|
views: TopologyTreeViewItem[];
|
||||||
children: TopologyTreeFolderItem[];
|
children: TopologyTreeFolderItem[];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue