mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
Fix world-map zoom-out starfield, tree search canvas hits, and sidebar scroll.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
fdab188cc1
commit
467079d6ca
3 changed files with 140 additions and 38 deletions
|
|
@ -297,17 +297,47 @@ def search_fabric_nodes_with_views(
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 50,
|
page_size: int = 50,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
"""Search fabric NEs and resolve which canvases they appear on.
|
||||||
|
|
||||||
|
Membership views (TopoViewNode) come first. UME synthetic canvases usually have
|
||||||
|
no membership rows — fall back to region_folder_id → primary canvas, and to the
|
||||||
|
world map when world_x/y are set.
|
||||||
|
"""
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
|
from .models import UmeInventoryNE
|
||||||
|
from .topology_region_canvas import primary_canvas_view
|
||||||
from .topology_service import _node_out
|
from .topology_service import _node_out
|
||||||
|
from .ume_topology_world import get_world_flat_view, is_world_flat_visible
|
||||||
|
|
||||||
q = db.query(TopoFabricNode)
|
q = db.query(TopoFabricNode)
|
||||||
kw = str(keyword or "").strip()
|
kw = str(keyword or "").strip()
|
||||||
if kw:
|
if kw:
|
||||||
like = f"%{kw}%"
|
like = f"%{kw}%"
|
||||||
q = q.filter(
|
inv_ids = [
|
||||||
(TopoFabricNode.name.ilike(like))
|
str(r[0])
|
||||||
| (TopoFabricNode.ip.ilike(like))
|
for r in db.query(UmeInventoryNE.ne_id)
|
||||||
| (TopoFabricNode.vendor.ilike(like))
|
.filter(
|
||||||
)
|
or_(
|
||||||
|
UmeInventoryNE.user_label.ilike(like),
|
||||||
|
UmeInventoryNE.ne_name.ilike(like),
|
||||||
|
UmeInventoryNE.host_name.ilike(like),
|
||||||
|
UmeInventoryNE.ip_address.ilike(like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(500)
|
||||||
|
.all()
|
||||||
|
if str(r[0] or "").strip()
|
||||||
|
]
|
||||||
|
clauses = [
|
||||||
|
TopoFabricNode.name.ilike(like),
|
||||||
|
TopoFabricNode.ip.ilike(like),
|
||||||
|
TopoFabricNode.vendor.ilike(like),
|
||||||
|
TopoFabricNode.ume_ne_id.ilike(like),
|
||||||
|
]
|
||||||
|
if inv_ids:
|
||||||
|
clauses.append(TopoFabricNode.ume_ne_id.in_(inv_ids))
|
||||||
|
q = q.filter(or_(*clauses))
|
||||||
total = q.count()
|
total = q.count()
|
||||||
rows = (
|
rows = (
|
||||||
q.order_by(TopoFabricNode.name.asc())
|
q.order_by(TopoFabricNode.name.asc())
|
||||||
|
|
@ -317,6 +347,23 @@ def search_fabric_nodes_with_views(
|
||||||
)
|
)
|
||||||
node_ids = [n.id for n in rows]
|
node_ids = [n.id for n in rows]
|
||||||
placements: dict[str, list[dict[str, Any]]] = {nid: [] for nid in node_ids}
|
placements: dict[str, list[dict[str, Any]]] = {nid: [] for nid in node_ids}
|
||||||
|
seen: dict[str, set[str]] = {nid: set() for nid in node_ids}
|
||||||
|
|
||||||
|
def _add_view(nid: str, view: TopoView, folder: TopoFolder | None) -> None:
|
||||||
|
vid = str(view.id or "").strip()
|
||||||
|
if not vid or vid in seen.get(nid, set()):
|
||||||
|
return
|
||||||
|
seen.setdefault(nid, set()).add(vid)
|
||||||
|
placements.setdefault(nid, []).append(
|
||||||
|
{
|
||||||
|
"view_id": view.id,
|
||||||
|
"view_name": view.name,
|
||||||
|
"folder_id": view.folder_id or "",
|
||||||
|
"folder_name": (folder.name if folder else "") or "",
|
||||||
|
"kind": view.kind or "custom",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if node_ids:
|
if node_ids:
|
||||||
vnodes = (
|
vnodes = (
|
||||||
db.query(TopoViewNode, TopoView, TopoFolder)
|
db.query(TopoViewNode, TopoView, TopoFolder)
|
||||||
|
|
@ -326,15 +373,31 @@ def search_fabric_nodes_with_views(
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
for vn, view, folder in vnodes:
|
for vn, view, folder in vnodes:
|
||||||
placements.setdefault(vn.fabric_node_id, []).append(
|
_add_view(vn.fabric_node_id, view, folder)
|
||||||
{
|
|
||||||
"view_id": view.id,
|
folder_ids = {
|
||||||
"view_name": view.name,
|
str(n.region_folder_id or "").strip()
|
||||||
"folder_id": view.folder_id or "",
|
for n in rows
|
||||||
"folder_name": (folder.name if folder else "") or "",
|
if str(n.region_folder_id or "").strip()
|
||||||
"kind": view.kind or "custom",
|
}
|
||||||
}
|
folders_by_id: dict[str, TopoFolder] = {}
|
||||||
)
|
if folder_ids:
|
||||||
|
for f in db.query(TopoFolder).filter(TopoFolder.id.in_(list(folder_ids))).all():
|
||||||
|
folders_by_id[str(f.id)] = f
|
||||||
|
|
||||||
|
flat = get_world_flat_view(db)
|
||||||
|
flat_ok = flat is not None and is_world_flat_visible(flat)
|
||||||
|
flat_folder = db.get(TopoFolder, flat.folder_id) if flat_ok and flat and flat.folder_id else None
|
||||||
|
|
||||||
|
for n in rows:
|
||||||
|
rid = str(n.region_folder_id or "").strip()
|
||||||
|
if rid:
|
||||||
|
view = primary_canvas_view(db, rid)
|
||||||
|
if view is not None:
|
||||||
|
_add_view(n.id, view, folders_by_id.get(rid))
|
||||||
|
if flat_ok and flat is not None and n.world_x is not None and n.world_y is not None:
|
||||||
|
_add_view(n.id, flat, flat_folder)
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for n in rows:
|
for n in rows:
|
||||||
d = _node_out(n).model_dump()
|
d = _node_out(n).model_dump()
|
||||||
|
|
|
||||||
|
|
@ -4494,10 +4494,11 @@ pre {
|
||||||
max-height: min(360px, 50vh);
|
max-height: min(360px, 50vh);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border: 1px solid #b8c4d2;
|
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fff;
|
background: #0b1220;
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
|
color: #e2e8f0;
|
||||||
|
box-shadow: 0 8px 24px rgba(2, 8, 23, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-tree-search__item {
|
.topo-tree-search__item {
|
||||||
|
|
@ -4510,7 +4511,7 @@ pre {
|
||||||
|
|
||||||
.topo-tree-search__item + .topo-tree-search__item {
|
.topo-tree-search__item + .topo-tree-search__item {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
border-top: 1px solid #eef2f6;
|
border-top: 1px solid rgba(148, 163, 184, 0.16);
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4530,14 +4531,14 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-tree-search__ne:hover {
|
.topo-tree-search__ne:hover {
|
||||||
background: #f1f5f9;
|
background: rgba(148, 163, 184, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-tree-search__name {
|
.topo-tree-search__name {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--nm-ink, #0f172a);
|
color: #e2e8f0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
@ -4566,11 +4567,11 @@ pre {
|
||||||
.topo-tree-search__view {
|
.topo-tree-search__view {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border: 1px solid #d0dae6;
|
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #f8fafc;
|
background: rgba(30, 41, 59, 0.9);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--nm-brand, #1d4ed8);
|
color: #93c5fd;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
@ -4578,8 +4579,8 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-tree-search__view:hover {
|
.topo-tree-search__view:hover {
|
||||||
background: #e8eef8;
|
background: rgba(51, 65, 85, 0.95);
|
||||||
border-color: #9eb6d8;
|
border-color: rgba(96, 165, 250, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-tree-search__hint {
|
.topo-tree-search__hint {
|
||||||
|
|
@ -5561,12 +5562,29 @@ pre {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
|
/* Allow deep indent + actions to extend; parent scroll shows a horizontal bar. */
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topo-region-list-scroll {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(2, 8, 23, 0.45);
|
||||||
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-region-list__block {
|
.topo-region-list__block {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 100%;
|
max-width: none;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 2px 0 4px;
|
padding: 2px 0 4px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -5585,6 +5603,8 @@ pre {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
transition: background 0.12s ease;
|
transition: background 0.12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5691,6 +5711,7 @@ pre {
|
||||||
.topo-region-list .topo-map-list__actions {
|
.topo-region-list .topo-map-list__actions {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
flex: 0 0 auto;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
|
|
@ -5730,10 +5751,10 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-region-list .topo-map-list__title {
|
.topo-region-list .topo-map-list__title {
|
||||||
min-width: 0;
|
min-width: max-content;
|
||||||
flex: 1 1 auto;
|
flex: 0 1 auto;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
text-overflow: ellipsis;
|
text-overflow: clip;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
|
|
|
||||||
|
|
@ -276,11 +276,23 @@ function mergeFlatWorldGraph(
|
||||||
next: TopologyViewGraph,
|
next: TopologyViewGraph,
|
||||||
opts?: { centerX?: number; centerY?: number },
|
opts?: { centerX?: number; centerY?: number },
|
||||||
): TopologyViewGraph {
|
): TopologyViewGraph {
|
||||||
if (!prev?.nodes?.length) return next;
|
const scatter = next.scatter?.length ? next.scatter : prev?.scatter;
|
||||||
|
if (!prev?.nodes?.length) {
|
||||||
|
return scatter?.length && !next.scatter?.length ? { ...next, scatter } : next;
|
||||||
|
}
|
||||||
|
// Overview / zoom-out payloads have empty RF nodes — keep (or refresh) scatter,
|
||||||
|
// drop heavy tiles when the server intentionally cleared them.
|
||||||
if (!next?.nodes?.length) {
|
if (!next?.nodes?.length) {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
|
view: next.view || prev.view,
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
scatter: scatter || [],
|
||||||
world_transform: next.world_transform ?? prev.world_transform,
|
world_transform: next.world_transform ?? prev.world_transform,
|
||||||
|
truncated: Boolean(next.truncated || prev.truncated),
|
||||||
|
truncate_reason: next.truncate_reason || prev.truncate_reason || "",
|
||||||
|
outside_peers: next.outside_peers?.length ? next.outside_peers : prev.outside_peers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -320,7 +332,7 @@ function mergeFlatWorldGraph(
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
world_transform: next.world_transform ?? prev.world_transform,
|
world_transform: next.world_transform ?? prev.world_transform,
|
||||||
scatter: next.scatter?.length ? next.scatter : prev.scatter,
|
scatter: scatter || [],
|
||||||
truncated: Boolean(prev.truncated || next.truncated),
|
truncated: Boolean(prev.truncated || next.truncated),
|
||||||
truncate_reason: next.truncate_reason || prev.truncate_reason || "",
|
truncate_reason: next.truncate_reason || prev.truncate_reason || "",
|
||||||
outside_peers: next.outside_peers?.length ? next.outside_peers : prev.outside_peers,
|
outside_peers: next.outside_peers?.length ? next.outside_peers : prev.outside_peers,
|
||||||
|
|
@ -1428,12 +1440,14 @@ export function TopologyPage() {
|
||||||
const cx = bounds ? bounds.x + bounds.width / 2 : 0;
|
const cx = bounds ? bounds.x + bounds.width / 2 : 0;
|
||||||
const cy = bounds ? bounds.y + bounds.height / 2 : 0;
|
const cy = bounds ? bounds.y + bounds.height / 2 : 0;
|
||||||
commit(g, cx, cy);
|
commit(g, cx, cy);
|
||||||
} else if (prev.nodes.length > 0 && zoom < 0.04) {
|
} else if (prev.nodes.length > 0) {
|
||||||
|
// Always shed RF tiles when leaving close-up so scatter is the sole layer.
|
||||||
commit(
|
commit(
|
||||||
{
|
{
|
||||||
...prev,
|
...prev,
|
||||||
nodes: [],
|
nodes: [],
|
||||||
edges: [],
|
edges: [],
|
||||||
|
scatter: prev.scatter,
|
||||||
world_transform: prev.world_transform
|
world_transform: prev.world_transform
|
||||||
? { ...prev.world_transform, lod: "overview" }
|
? { ...prev.world_transform, lod: "overview" }
|
||||||
: prev.world_transform,
|
: prev.world_transform,
|
||||||
|
|
@ -4156,7 +4170,7 @@ export function TopologyPage() {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{views.length > 1 ? (
|
{views.length > 0 ? (
|
||||||
<div className="topo-tree-search__views">
|
<div className="topo-tree-search__views">
|
||||||
{views.map((v) => (
|
{views.map((v) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -4166,7 +4180,9 @@ export function TopologyPage() {
|
||||||
onClick={() => jumpToTreeSearchHit(hit, v)}
|
onClick={() => jumpToTreeSearchHit(hit, v)}
|
||||||
title={`${v.folder_name ? `${v.folder_name} / ` : ""}${v.view_name}`}
|
title={`${v.folder_name ? `${v.folder_name} / ` : ""}${v.view_name}`}
|
||||||
>
|
>
|
||||||
{v.view_name || v.view_id.slice(0, 8)}
|
{v.folder_name
|
||||||
|
? `${v.folder_name} / ${v.view_name || v.view_id.slice(0, 8)}`
|
||||||
|
: v.view_name || v.view_id.slice(0, 8)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -4214,9 +4230,11 @@ export function TopologyPage() {
|
||||||
) : regions.length === 0 ? (
|
) : regions.length === 0 ? (
|
||||||
<p className="panel__hint">{t("topology.emptyMaps")}</p>
|
<p className="panel__hint">{t("topology.emptyMaps")}</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="topo-map-list topo-region-list">
|
<div className="topo-region-list-scroll">
|
||||||
{regions.map((region) => renderWorldNavFolder(region, 0))}
|
<ul className="topo-map-list topo-region-list">
|
||||||
</ul>
|
{regions.map((region) => renderWorldNavFolder(region, 0))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{mapId && outsidePeers.length > 0 && (
|
{mapId && outsidePeers.length > 0 && (
|
||||||
<div className="topo-outside-peers">
|
<div className="topo-outside-peers">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue