mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 12:40:44 +08:00
Unify directory NE badges as distinct fabric ids across membership and home.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3d86bc7594
commit
4b7d13d62b
5 changed files with 188 additions and 162 deletions
|
|
@ -228,7 +228,7 @@ 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.
|
# Real NE inventory under this folder (distinct fabric ids in subtree).
|
||||||
ne_count: int = 0
|
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)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ from .models import (
|
||||||
TopoViewEdgeStyle,
|
TopoViewEdgeStyle,
|
||||||
TopoViewNode,
|
TopoViewNode,
|
||||||
UmeInventoryNE,
|
UmeInventoryNE,
|
||||||
UmeTopoNode,
|
|
||||||
)
|
)
|
||||||
from .topology_common import (
|
from .topology_common import (
|
||||||
PHYSICAL_VIEW_NAME,
|
PHYSICAL_VIEW_NAME,
|
||||||
|
|
@ -659,82 +658,51 @@ def get_topology_tree(db: Session) -> TopologyTreeOut:
|
||||||
bootstrap_topology_tree(db)
|
bootstrap_topology_tree(db)
|
||||||
folders = db.query(TopoFolder).order_by(TopoFolder.sort_order.asc(), TopoFolder.name.asc()).all()
|
folders = db.query(TopoFolder).order_by(TopoFolder.sort_order.asc(), TopoFolder.name.asc()).all()
|
||||||
views = db.query(TopoView).order_by(TopoView.sort_order.asc(), TopoView.name.asc()).all()
|
views = db.query(TopoView).order_by(TopoView.sort_order.asc(), TopoView.name.asc()).all()
|
||||||
# Cheap directory counts — never hydrate 15k fabric rows (attrs JSON) on tree load.
|
|
||||||
# Full inventory scans belong only to the world flat map graph.
|
# --- NE badge: source-agnostic distinct fabric ids (membership ∪ region_folder_id) ---
|
||||||
nc_map: dict[str, int] = {
|
# Cheap aggregates only — never hydrate attrs JSON on tree load.
|
||||||
str(vid): int(cnt or 0)
|
membership_by_view: dict[str, set[str]] = {}
|
||||||
for vid, cnt in (
|
for vid, fid in (
|
||||||
db.query(TopoViewNode.view_id, func.count(TopoViewNode.id))
|
db.query(TopoViewNode.view_id, TopoViewNode.fabric_node_id)
|
||||||
.group_by(TopoViewNode.view_id)
|
.filter(~TopoViewNode.fabric_node_id.like("region:%"))
|
||||||
.all()
|
.all()
|
||||||
|
):
|
||||||
|
membership_by_view.setdefault(str(vid), set()).add(str(fid))
|
||||||
|
|
||||||
|
fabric_by_region: dict[str, set[str]] = {}
|
||||||
|
for fid, nid in (
|
||||||
|
db.query(TopoFabricNode.region_folder_id, 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 != "",
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
.all()
|
||||||
region_icon_map: dict[str, int] = {
|
):
|
||||||
str(vid): int(cnt or 0)
|
key = str(fid or "").strip()
|
||||||
for vid, cnt in (
|
if key:
|
||||||
db.query(TopoViewNode.view_id, func.count(TopoViewNode.id))
|
fabric_by_region.setdefault(key, set()).add(str(nid))
|
||||||
.filter(TopoViewNode.fabric_node_id.like("region:%"))
|
|
||||||
.group_by(TopoViewNode.view_id)
|
fabric_inventory_total = int(
|
||||||
.all()
|
db.query(func.count(TopoFabricNode.id))
|
||||||
)
|
.filter(
|
||||||
}
|
or_(
|
||||||
# Skip heavy UME rollups on fresh installs (no UME folders / level views yet).
|
and_(TopoFabricNode.ume_ne_id.isnot(None), TopoFabricNode.ume_ne_id != ""),
|
||||||
has_ume_dirs = any(
|
and_(
|
||||||
str(getattr(f, "external_ref", None) or "").strip()
|
TopoFabricNode.managed_ne_id.isnot(None),
|
||||||
for f in folders
|
TopoFabricNode.managed_ne_id != "",
|
||||||
if str(f.kind or "") == "region"
|
|
||||||
) or any(
|
|
||||||
bool(
|
|
||||||
view_filter_dict(v.filter).get("ume_level")
|
|
||||||
or view_filter_dict(v.filter).get("world")
|
|
||||||
or view_filter_dict(v.filter).get("world_flat")
|
|
||||||
)
|
|
||||||
for v in views
|
|
||||||
)
|
|
||||||
# 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_total = 0
|
|
||||||
if has_ume_dirs:
|
|
||||||
ume_ne_by_sbn = {
|
|
||||||
str(parent): int(cnt or 0)
|
|
||||||
for parent, cnt in (
|
|
||||||
db.query(UmeTopoNode.parent_node, func.count(UmeTopoNode.node_id))
|
|
||||||
.filter(
|
|
||||||
UmeTopoNode.node_type == "TOPO_NODE_ME",
|
|
||||||
UmeTopoNode.parent_node.isnot(None),
|
|
||||||
UmeTopoNode.parent_node != "",
|
|
||||||
)
|
|
||||||
.group_by(UmeTopoNode.parent_node)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
if str(parent or "").strip()
|
|
||||||
}
|
|
||||||
ume_ne_total = int(
|
|
||||||
db.query(func.count(TopoFabricNode.id))
|
|
||||||
.filter(TopoFabricNode.ume_ne_id.isnot(None), TopoFabricNode.ume_ne_id != "")
|
|
||||||
.scalar()
|
|
||||||
or 0
|
|
||||||
)
|
)
|
||||||
|
.scalar()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
by_parent: dict[str, list[TopoFolder]] = {}
|
by_parent: dict[str, list[TopoFolder]] = {}
|
||||||
folder_by_id: dict[str, TopoFolder] = {}
|
folder_by_id: dict[str, TopoFolder] = {}
|
||||||
|
|
@ -747,77 +715,60 @@ def get_topology_tree(db: Session) -> TopologyTreeOut:
|
||||||
pid = str(f.parent_id or "")
|
pid = str(f.parent_id or "")
|
||||||
by_parent.setdefault(pid, []).append(f)
|
by_parent.setdefault(pid, []).append(f)
|
||||||
|
|
||||||
def _is_whole_network_folder(folder: TopoFolder) -> bool:
|
|
||||||
"""UME World / World / 世界地图 — same UME inventory, different presentation.
|
|
||||||
|
|
||||||
Manual「根」/「根图」are separate canvases and must use their own subtree counts
|
|
||||||
(empty root map → 0N), never the UME thousands.
|
|
||||||
"""
|
|
||||||
from .ume_topology_world import is_ume_world_container, is_world_drill_folder
|
|
||||||
|
|
||||||
return is_ume_world_container(folder) or is_world_drill_folder(folder)
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
def _view_membership_ne(v: TopoView) -> int:
|
def _is_virtual_full_network_folder(folder: TopoFolder) -> bool:
|
||||||
"""Canvas members that are real NEs (exclude region:* directory icons)."""
|
"""UME World / World — alternate presentation of full fabric inventory."""
|
||||||
raw = int(nc_map.get(v.id, 0) or 0)
|
from .ume_topology_world import is_ume_world_container, is_world_drill_folder
|
||||||
return max(0, raw - int(region_icon_map.get(v.id, 0) or 0))
|
|
||||||
|
|
||||||
def _own_canvas_ne(folder: TopoFolder) -> int:
|
return is_ume_world_container(folder) or is_world_drill_folder(folder)
|
||||||
"""Manual canvas: prefer view membership; also honor fabric region ownership."""
|
|
||||||
mem = sum(
|
|
||||||
_view_membership_ne(v) for v in views_by_folder.get(str(folder.id), [])
|
|
||||||
)
|
|
||||||
fab = int(ne_by_folder.get(str(folder.id), 0))
|
|
||||||
return max(mem, fab)
|
|
||||||
|
|
||||||
def _subtree_inventory(folder_id: str) -> int:
|
def _is_virtual_full_network_view(v: TopoView) -> bool:
|
||||||
"""UME-synced directory: 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 _is_ume_synced_folder(folder: TopoFolder) -> bool:
|
|
||||||
return bool(str(getattr(folder, "external_ref", None) or "").strip())
|
|
||||||
|
|
||||||
def _folder_ne_count(folder: TopoFolder) -> int:
|
|
||||||
if _is_whole_network_folder(folder):
|
|
||||||
return int(ume_ne_total or 0)
|
|
||||||
# UME SBN / synced dirs: fabric ownership tree (not empty manual canvases).
|
|
||||||
if _is_ume_synced_folder(folder):
|
|
||||||
return _subtree_inventory(str(folder.id))
|
|
||||||
# Manual 根/根图/子区域: count NEs placed on canvases (+ nested regions).
|
|
||||||
own = _own_canvas_ne(folder)
|
|
||||||
nested = sum(
|
|
||||||
_folder_ne_count(child) for child in by_parent.get(str(folder.id), [])
|
|
||||||
)
|
|
||||||
return own + nested
|
|
||||||
|
|
||||||
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)
|
filt = view_filter_dict(v.filter)
|
||||||
if filt.get("world_flat") or (
|
if filt.get("world_flat") or (
|
||||||
bool(filt.get("world")) and not filt.get("ume_level")
|
bool(filt.get("world")) and not filt.get("ume_level")
|
||||||
):
|
):
|
||||||
return int(ume_ne_total or 0)
|
return True
|
||||||
if filt.get("ume_level"):
|
if filt.get("ume_level") and str(filt.get("parent") or "") == "md":
|
||||||
sid = str(filt.get("sbn_id") or "").strip()
|
return True
|
||||||
if sid:
|
folder = folder_by_id.get(str(v.folder_id or ""))
|
||||||
return int(ume_ne_by_sbn.get(sid, 0))
|
return bool(folder and _is_virtual_full_network_folder(folder))
|
||||||
if str(filt.get("parent") or "") == "md":
|
|
||||||
return int(ume_ne_total or 0)
|
def _own_ne_ids(folder: TopoFolder) -> set[str]:
|
||||||
mem = _view_membership_ne(v)
|
"""NEs on this folder's canvases ∪ fabric default-home on this folder."""
|
||||||
if mem:
|
ids: set[str] = set(fabric_by_region.get(str(folder.id), set()))
|
||||||
return mem
|
for v in views_by_folder.get(str(folder.id), []):
|
||||||
if folder is not None:
|
ids |= membership_by_view.get(str(v.id), set())
|
||||||
return int(ne_by_folder.get(str(folder.id), 0))
|
return ids
|
||||||
return 0
|
|
||||||
|
# Bottom-up distinct union: folder_id -> ne id set (cached while building).
|
||||||
|
subtree_ne_ids: dict[str, set[str]] = {}
|
||||||
|
|
||||||
|
def _subtree_ne_ids(folder: TopoFolder) -> set[str]:
|
||||||
|
fid = str(folder.id)
|
||||||
|
if fid in subtree_ne_ids:
|
||||||
|
return subtree_ne_ids[fid]
|
||||||
|
if _is_virtual_full_network_folder(folder):
|
||||||
|
# Don't expand full inventory into a giant set; count handled separately.
|
||||||
|
subtree_ne_ids[fid] = set()
|
||||||
|
return subtree_ne_ids[fid]
|
||||||
|
ids = set(_own_ne_ids(folder))
|
||||||
|
for child in by_parent.get(fid, []):
|
||||||
|
ids |= _subtree_ne_ids(child)
|
||||||
|
subtree_ne_ids[fid] = ids
|
||||||
|
return ids
|
||||||
|
|
||||||
|
def _folder_ne_count(folder: TopoFolder) -> int:
|
||||||
|
if _is_virtual_full_network_folder(folder):
|
||||||
|
return fabric_inventory_total
|
||||||
|
return len(_subtree_ne_ids(folder))
|
||||||
|
|
||||||
|
def _view_node_count(v: TopoView) -> int:
|
||||||
|
if _is_virtual_full_network_view(v):
|
||||||
|
return fabric_inventory_total
|
||||||
|
return len(membership_by_view.get(str(v.id), set()))
|
||||||
|
|
||||||
def _flat_views(folder: TopoFolder, folder_views: list[TopoView]) -> list[TopologyTreeViewOut]:
|
def _flat_views(folder: TopoFolder, folder_views: list[TopoView]) -> list[TopologyTreeViewOut]:
|
||||||
from .ume_topology_world import (
|
from .ume_topology_world import (
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,105 @@ class FabricTopologyTests(unittest.TestCase):
|
||||||
self.assertEqual(len(rm.views), 2)
|
self.assertEqual(len(rm.views), 2)
|
||||||
self.assertTrue(all(not getattr(v, "children", None) for v in rm.views))
|
self.assertTrue(all(not getattr(v, "children", None) for v in rm.views))
|
||||||
|
|
||||||
|
def test_tree_ne_count_distinct_membership_and_home(self) -> None:
|
||||||
|
"""Directory N = distinct fabric ids (membership ∪ region_folder_id); parent unions."""
|
||||||
|
site = svc.create_folder(
|
||||||
|
self.db, TopologyFolderCreate(name="NE-Count-Site", kind="region")
|
||||||
|
)
|
||||||
|
tree0 = svc.get_topology_tree(self.db)
|
||||||
|
assert tree0.root is not None
|
||||||
|
site_f = next(c for c in tree0.root.children if c.id == site.id)
|
||||||
|
root_map = site_f.children[0]
|
||||||
|
self.assertEqual(site_f.ne_count, 0)
|
||||||
|
self.assertEqual(root_map.ne_count, 0)
|
||||||
|
self.assertEqual(root_map.views[0].node_count, 0)
|
||||||
|
|
||||||
|
a = svc.create_folder(
|
||||||
|
self.db,
|
||||||
|
TopologyFolderCreate(name="Zone-A", kind="region", parent_id=site.id),
|
||||||
|
)
|
||||||
|
b = svc.create_folder(
|
||||||
|
self.db,
|
||||||
|
TopologyFolderCreate(name="Zone-B", kind="region", parent_id=site.id),
|
||||||
|
)
|
||||||
|
tree1 = svc.get_topology_tree(self.db)
|
||||||
|
assert tree1.root is not None
|
||||||
|
site1 = next(c for c in tree1.root.children if c.id == site.id)
|
||||||
|
rm1 = site1.children[0]
|
||||||
|
za = next(c for c in rm1.children if c.id == a.id)
|
||||||
|
zb = next(c for c in rm1.children if c.id == b.id)
|
||||||
|
# Empty nested regions (+ region icons on parent) still 0 NE.
|
||||||
|
self.assertEqual(za.ne_count, 0)
|
||||||
|
self.assertEqual(zb.ne_count, 0)
|
||||||
|
self.assertEqual(rm1.ne_count, 0)
|
||||||
|
|
||||||
|
suffix = uuid4().hex[:8]
|
||||||
|
shared = TopoFabricNode(
|
||||||
|
id=f"ne-shared-{suffix}",
|
||||||
|
name=f"Shared-{suffix}",
|
||||||
|
ip="10.66.0.1",
|
||||||
|
managed_ne_id=f"mne-shared-{suffix}",
|
||||||
|
)
|
||||||
|
only_a = TopoFabricNode(
|
||||||
|
id=f"ne-a-{suffix}",
|
||||||
|
name=f"OnlyA-{suffix}",
|
||||||
|
ip="10.66.0.2",
|
||||||
|
managed_ne_id=f"mne-a-{suffix}",
|
||||||
|
)
|
||||||
|
home_only = TopoFabricNode(
|
||||||
|
id=f"ne-home-{suffix}",
|
||||||
|
name=f"Home-{suffix}",
|
||||||
|
ip="10.66.0.3",
|
||||||
|
managed_ne_id=f"mne-home-{suffix}",
|
||||||
|
region_folder_id=a.id,
|
||||||
|
)
|
||||||
|
self.db.add_all([shared, only_a, home_only])
|
||||||
|
self.db.add(
|
||||||
|
TopoViewNode(
|
||||||
|
id=uuid4().hex,
|
||||||
|
view_id=za.views[0].id,
|
||||||
|
fabric_node_id=shared.id,
|
||||||
|
x=0,
|
||||||
|
y=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.db.add(
|
||||||
|
TopoViewNode(
|
||||||
|
id=uuid4().hex,
|
||||||
|
view_id=zb.views[0].id,
|
||||||
|
fabric_node_id=shared.id,
|
||||||
|
x=1,
|
||||||
|
y=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.db.add(
|
||||||
|
TopoViewNode(
|
||||||
|
id=uuid4().hex,
|
||||||
|
view_id=za.views[0].id,
|
||||||
|
fabric_node_id=only_a.id,
|
||||||
|
x=2,
|
||||||
|
y=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
tree2 = svc.get_topology_tree(self.db)
|
||||||
|
assert tree2.root is not None
|
||||||
|
site2 = next(c for c in tree2.root.children if c.id == site.id)
|
||||||
|
rm2 = site2.children[0]
|
||||||
|
za2 = next(c for c in rm2.children if c.id == a.id)
|
||||||
|
zb2 = next(c for c in rm2.children if c.id == b.id)
|
||||||
|
|
||||||
|
# Zone-A: shared + only_a + home_only (region_folder_id, no membership)
|
||||||
|
self.assertEqual(za2.ne_count, 3)
|
||||||
|
self.assertEqual(za2.views[0].node_count, 2)
|
||||||
|
# Zone-B: shared only
|
||||||
|
self.assertEqual(zb2.ne_count, 1)
|
||||||
|
self.assertEqual(zb2.views[0].node_count, 1)
|
||||||
|
# Parent 根图: distinct union → shared, only_a, home_only = 3 (not 3+1)
|
||||||
|
self.assertEqual(rm2.ne_count, 3)
|
||||||
|
self.assertEqual(site2.ne_count, 3)
|
||||||
|
|
||||||
def test_manual_root_map_name_follows_locale(self) -> None:
|
def test_manual_root_map_name_follows_locale(self) -> None:
|
||||||
"""English UI creates Root map; zh (default) keeps 根图."""
|
"""English UI creates Root map; zh (default) keeps 根图."""
|
||||||
en_root = svc.create_folder(
|
en_root = svc.create_folder(
|
||||||
|
|
|
||||||
|
|
@ -297,34 +297,10 @@ function regionDisplayName(
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prefer the folder's primary canvas view (physical / world flat / World drill). */
|
/** Directory NE badge: API already returns distinct-subtree ne_count. */
|
||||||
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.
|
|
||||||
* Manual canvases count view membership (placed NEs); UME World surfaces use inventory.
|
|
||||||
*/
|
|
||||||
function folderNeCount(folder: TopologyTreeFolderItem | null | undefined): number {
|
function folderNeCount(folder: TopologyTreeFolderItem | null | undefined): number {
|
||||||
if (!folder) return 0;
|
if (!folder) return 0;
|
||||||
if (folder.ne_count != null && Number.isFinite(Number(folder.ne_count))) {
|
return Math.max(0, Math.floor(Number(folder.ne_count) || 0));
|
||||||
return Math.max(0, Math.floor(Number(folder.ne_count) || 0));
|
|
||||||
}
|
|
||||||
// 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,7 +427,7 @@ 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. */
|
/** Distinct fabric NE count in this folder subtree (API). Region icons excluded. */
|
||||||
ne_count?: number;
|
ne_count?: number;
|
||||||
views: TopologyTreeViewItem[];
|
views: TopologyTreeViewItem[];
|
||||||
children: TopologyTreeFolderItem[];
|
children: TopologyTreeFolderItem[];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue