mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
229 lines
7 KiB
Python
229 lines
7 KiB
Python
"""Child-region icons on a parent canvas (folder === canvas drill-down).
|
|
|
|
Synthetic placements use fabric_node_id ``region:<folder_id>`` on the parent's
|
|
TopoViewNode rows — same shape as UME level region nodes, persisted so drag/save works.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from .models import TopoFolder, TopoView, TopoViewNode
|
|
from .topology_common import _utcnow
|
|
from .topology_membership import VIEW_KIND_PHYSICAL, normalize_view_kind
|
|
from .topology_schemas import ViewNodeOut
|
|
|
|
REGION_NODE_PREFIX = "region:"
|
|
|
|
|
|
def is_region_canvas_node_id(fabric_node_id: str | None) -> bool:
|
|
return str(fabric_node_id or "").startswith(REGION_NODE_PREFIX)
|
|
|
|
|
|
def region_folder_id_from_node(fabric_node_id: str) -> str:
|
|
return str(fabric_node_id or "")[len(REGION_NODE_PREFIX) :]
|
|
|
|
|
|
def region_canvas_node_id(folder_id: str) -> str:
|
|
return f"{REGION_NODE_PREFIX}{folder_id}"
|
|
|
|
|
|
def _is_world_flat(view: TopoView) -> bool:
|
|
filt = dict(view.filter or {})
|
|
if filt.get("world_flat"):
|
|
return True
|
|
from .ume_topology_world import is_world_flat_view_name
|
|
|
|
return is_world_flat_view_name(str(view.name or "").strip())
|
|
|
|
|
|
def primary_canvas_view(db: Session, folder_id: str) -> TopoView | None:
|
|
"""Parent canvas used for child-region icons (skip flat world map)."""
|
|
fid = str(folder_id or "").strip()
|
|
if not fid:
|
|
return None
|
|
views = (
|
|
db.query(TopoView)
|
|
.filter(TopoView.folder_id == fid)
|
|
.order_by(TopoView.sort_order.asc(), TopoView.created_at.asc())
|
|
.all()
|
|
)
|
|
usable = [v for v in views if not _is_world_flat(v)]
|
|
if not usable:
|
|
return None
|
|
for v in usable:
|
|
filt = dict(v.filter or {})
|
|
if filt.get("ume_level") or filt.get("world"):
|
|
return v
|
|
for v in usable:
|
|
if normalize_view_kind(v.kind) == VIEW_KIND_PHYSICAL:
|
|
return v
|
|
return usable[0]
|
|
|
|
|
|
def _child_primary_view(db: Session, folder_id: str) -> TopoView | None:
|
|
return primary_canvas_view(db, folder_id)
|
|
|
|
|
|
def _default_region_xy(db: Session, view_id: str) -> tuple[float, float]:
|
|
rows = db.query(TopoViewNode).filter(TopoViewNode.view_id == view_id).all()
|
|
if not rows:
|
|
return 160.0, 160.0
|
|
max_x = max(float(r.x or 0) for r in rows)
|
|
ys = [float(r.y or 0) for r in rows]
|
|
avg_y = sum(ys) / len(ys) if ys else 160.0
|
|
return max_x + 220.0, avg_y
|
|
|
|
|
|
def place_child_region_on_parent_canvas(
|
|
db: Session,
|
|
parent: TopoFolder,
|
|
child: TopoFolder,
|
|
*,
|
|
x: float | None = None,
|
|
y: float | None = None,
|
|
) -> TopoViewNode | None:
|
|
"""Put a region building icon on the parent's canvas. No-op for root parent."""
|
|
if parent is None or child is None:
|
|
return None
|
|
if str(parent.kind or "") != "region":
|
|
return None
|
|
view = primary_canvas_view(db, parent.id)
|
|
if view is None:
|
|
return None
|
|
nid = region_canvas_node_id(child.id)
|
|
existing = (
|
|
db.query(TopoViewNode)
|
|
.filter(TopoViewNode.view_id == view.id, TopoViewNode.fabric_node_id == nid)
|
|
.first()
|
|
)
|
|
if existing is not None:
|
|
if child.name and existing.label != child.name:
|
|
existing.label = str(child.name)[:256]
|
|
existing.updated_at = _utcnow()
|
|
return existing
|
|
px, py = _default_region_xy(db, view.id) if x is None or y is None else (float(x), float(y))
|
|
now = _utcnow()
|
|
row = TopoViewNode(
|
|
id=uuid4().hex,
|
|
view_id=view.id,
|
|
fabric_node_id=nid,
|
|
x=px,
|
|
y=py,
|
|
label=str(child.name or "")[:256],
|
|
locked=False,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
db.add(row)
|
|
return row
|
|
|
|
|
|
def remove_region_canvas_placements(db: Session, folder_id: str) -> int:
|
|
"""Drop this folder's icon from any parent canvas."""
|
|
nid = region_canvas_node_id(folder_id)
|
|
return (
|
|
db.query(TopoViewNode)
|
|
.filter(TopoViewNode.fabric_node_id == nid)
|
|
.delete(synchronize_session=False)
|
|
)
|
|
|
|
|
|
def region_node_out(
|
|
db: Session,
|
|
*,
|
|
folder: TopoFolder,
|
|
x: float,
|
|
y: float,
|
|
locked: bool = False,
|
|
label: str = "",
|
|
) -> ViewNodeOut:
|
|
child_view = _child_primary_view(db, folder.id)
|
|
kids = (
|
|
db.query(TopoFolder)
|
|
.filter(TopoFolder.parent_id == folder.id, TopoFolder.kind == "region")
|
|
.count()
|
|
)
|
|
# node_count: prefer view membership count when available
|
|
node_count = 0
|
|
if child_view is not None:
|
|
node_count = (
|
|
db.query(TopoViewNode)
|
|
.filter(TopoViewNode.view_id == child_view.id)
|
|
.count()
|
|
)
|
|
display = (label or folder.name or folder.id)[:256]
|
|
return ViewNodeOut(
|
|
fabric_node_id=region_canvas_node_id(folder.id),
|
|
managed_ne_id="",
|
|
ume_ne_id="",
|
|
label=display,
|
|
x=float(x),
|
|
y=float(y),
|
|
locked=bool(locked),
|
|
name=str(folder.name or "")[:256],
|
|
ip="",
|
|
vendor="",
|
|
device_type="region",
|
|
kind="region",
|
|
folder_id=folder.id,
|
|
view_id=child_view.id if child_view else "",
|
|
node_count=int(node_count or kids or 0),
|
|
)
|
|
|
|
|
|
def _is_ume_managed_folder(folder: TopoFolder) -> bool:
|
|
"""UME World / drill / SBN folders are drawn by ume_topology_world_graph, not here."""
|
|
ext = str(getattr(folder, "external_ref", None) or "").strip()
|
|
return bool(ext)
|
|
|
|
|
|
def child_region_nodes_for_view(db: Session, view: TopoView) -> list[ViewNodeOut]:
|
|
"""Manual child regions as canvas icons (skip UME-synced SBN folders)."""
|
|
folder_id = str(view.folder_id or "").strip()
|
|
if not folder_id or _is_world_flat(view):
|
|
return []
|
|
children = (
|
|
db.query(TopoFolder)
|
|
.filter(TopoFolder.parent_id == folder_id, TopoFolder.kind == "region")
|
|
.order_by(TopoFolder.sort_order.asc(), TopoFolder.name.asc())
|
|
.all()
|
|
)
|
|
children = [c for c in children if not _is_ume_managed_folder(c)]
|
|
if not children:
|
|
return []
|
|
placements = {
|
|
vn.fabric_node_id: vn
|
|
for vn in db.query(TopoViewNode).filter(TopoViewNode.view_id == view.id).all()
|
|
if is_region_canvas_node_id(vn.fabric_node_id)
|
|
}
|
|
out: list[ViewNodeOut] = []
|
|
auto_x, auto_y = _default_region_xy(db, view.id)
|
|
for i, child in enumerate(children):
|
|
nid = region_canvas_node_id(child.id)
|
|
vn = placements.get(nid)
|
|
if vn is not None:
|
|
out.append(
|
|
region_node_out(
|
|
db,
|
|
folder=child,
|
|
x=float(vn.x or 0),
|
|
y=float(vn.y or 0),
|
|
locked=bool(vn.locked),
|
|
label=str(vn.label or child.name or ""),
|
|
)
|
|
)
|
|
else:
|
|
out.append(
|
|
region_node_out(
|
|
db,
|
|
folder=child,
|
|
x=auto_x + i * 40,
|
|
y=auto_y + i * 20,
|
|
locked=False,
|
|
label=str(child.name or ""),
|
|
)
|
|
)
|
|
return out
|