mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
Unify UME World as hex nav with conditional flat world map.
Seed a unique L2 World canvas under the UME World container, show the flat map only when nested regions have NEs, pack blocks without overlap, allow drag overrides on the world map while forbidding direct NE adds, and hide the world-map truncation banner. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5d705fa7bb
commit
1ef43fdcd4
30 changed files with 4389 additions and 371 deletions
BIN
web/public/topo/region-building.png
Normal file
BIN
web/public/topo/region-building.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
|
|
@ -62,7 +62,8 @@ export const queryKeys = {
|
|||
fabricSummary: ["fabricSummary"] as const,
|
||||
/** @deprecated */
|
||||
topologyMaps: ["topologyViews"] as const,
|
||||
topologyGraph: (viewId: string) => ["topologyViewGraph", viewId] as const,
|
||||
topologyGraph: (viewId: string, focusFolderId = "") =>
|
||||
["topologyViewGraph", viewId, focusFolderId] as const,
|
||||
lldpCollectDashboard: ["lldpCollectDashboard"] as const,
|
||||
lldpCollectJobsAll: ["lldpCollectJobs"] as const,
|
||||
lldpCollectJobs: (page: number) => ["lldpCollectJobs", page] as const,
|
||||
|
|
|
|||
|
|
@ -1423,6 +1423,12 @@ const en = {
|
|||
newRegion: "New region",
|
||||
newRegionName: "New region",
|
||||
newRegionPrompt: "Region name",
|
||||
newSubRegion: "New sub-region",
|
||||
newSubRegionName: "New sub-region",
|
||||
newSubRegionPrompt: "Sub-region name",
|
||||
subRegionNotOnFlat: "The flat world map has no regions. Create sub-regions on World or a lower-level canvas.",
|
||||
worldMapNoDirectNes: "Cannot add NEs on the world map — add them on a sub-region canvas.",
|
||||
subRegionNeedParent: "Open a region canvas first, then create a sub-region.",
|
||||
regionCreated: "Region created",
|
||||
regionDeleted: "Region deleted",
|
||||
deleteRegion: "Delete region",
|
||||
|
|
|
|||
|
|
@ -1414,6 +1414,12 @@ const zh = {
|
|||
newRegion: "新建区域",
|
||||
newRegionName: "新区域",
|
||||
newRegionPrompt: "区域名称",
|
||||
newSubRegion: "新建子区域",
|
||||
newSubRegionName: "新子区域",
|
||||
newSubRegionPrompt: "子区域名称",
|
||||
subRegionNotOnFlat: "完整世界地图不支持子区域,请在 World 或下级区域画布上创建。",
|
||||
worldMapNoDirectNes: "世界地图不能直接添加网元,请到某个子区域画布上添加。",
|
||||
subRegionNeedParent: "请先打开一个区域画布,再新建子区域。",
|
||||
regionCreated: "区域已创建",
|
||||
regionDeleted: "区域已删除",
|
||||
deleteRegion: "删除区域",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
* {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
|
@ -5558,9 +5558,11 @@ pre {
|
|||
|
||||
.topo-region-list__block {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
padding: 2px 0 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.topo-region-list__row {
|
||||
|
|
@ -5581,13 +5583,15 @@ pre {
|
|||
|
||||
.topo-region-list .topo-map-list__row:hover,
|
||||
.topo-region-list__block.is-hot > .topo-region-list__row,
|
||||
.topo-region-list__maps > li.is-hot > .topo-map-list__row {
|
||||
.topo-region-list__maps > li.is-hot > .topo-map-list__row,
|
||||
.topo-region-list__view.is-hot > .topo-map-list__row {
|
||||
background: var(--nm-nav-hover, #152033);
|
||||
}
|
||||
|
||||
/* Selected = deep wash + left accent */
|
||||
.topo-region-list__block.is-active > .topo-region-list__row,
|
||||
.topo-region-list__maps > li.is-active > .topo-map-list__row {
|
||||
.topo-region-list__maps > li.is-active > .topo-map-list__row,
|
||||
.topo-region-list__view.is-active > .topo-map-list__row {
|
||||
border: 0;
|
||||
background: var(--nm-nav-active, #1a2740);
|
||||
box-shadow: inset 3px 0 0 var(--nm-accent, #3b82f6);
|
||||
|
|
@ -5625,7 +5629,8 @@ pre {
|
|||
.topo-region-list__maps {
|
||||
position: relative;
|
||||
margin: 2px 0 0;
|
||||
padding: 2px 0 2px 0;
|
||||
/* One nest level = one indent; do not also set marginLeft on child blocks. */
|
||||
padding: 2px 0 2px 14px;
|
||||
overflow: visible;
|
||||
flex: none;
|
||||
}
|
||||
|
|
@ -5633,7 +5638,7 @@ pre {
|
|||
.topo-region-list__maps::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
left: 6px;
|
||||
top: 0;
|
||||
bottom: 6px;
|
||||
width: 1px;
|
||||
|
|
@ -5642,8 +5647,10 @@ pre {
|
|||
|
||||
.topo-region-list__maps > li {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.topo-region-list__maps > li + li {
|
||||
|
|
@ -5660,9 +5667,10 @@ pre {
|
|||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* View rows use a chevron spacer — same padding as folder rows (no extra 36px). */
|
||||
.app-main .topo-region-list__maps .topo-map-list__item,
|
||||
.topo-region-list__maps .topo-map-list__item {
|
||||
padding: 7px 6px 7px 36px;
|
||||
padding: 8px 6px 8px 8px;
|
||||
}
|
||||
|
||||
.topo-region-list .topo-map-list__item:hover,
|
||||
|
|
@ -7068,6 +7076,31 @@ pre {
|
|||
.topo-node--other {
|
||||
--topo-icon-color: var(--topo-vendor-gray, #94a3b8);
|
||||
}
|
||||
.topo-node--region {
|
||||
--topo-icon-color: #38bdf8;
|
||||
}
|
||||
.topo-region-list__chevron {
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.topo-region-list__chevron:disabled {
|
||||
opacity: 0.25;
|
||||
cursor: default;
|
||||
}
|
||||
.topo-region-list__chevron-spacer {
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.topo-region-list .topo-map-list__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.topo-node__glyph {
|
||||
position: relative;
|
||||
|
|
@ -7076,6 +7109,20 @@ pre {
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.topo-node.is-region .topo-node__glyph {
|
||||
width: 56px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.topo-node.is-region .topo-node__caption {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.topo-node__icon--region {
|
||||
width: 56px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.react-flow__node.is-search-hit .topo-node__glyph {
|
||||
box-shadow: none;
|
||||
outline: 2px solid rgba(245, 158, 11, 0.95);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1106,8 +1106,41 @@ export const populateTopologyView = (
|
|||
graph: TopologyViewGraph | null;
|
||||
}>(`/v1/topology/views/${encodeURIComponent(viewId)}/populate`, body || {});
|
||||
|
||||
export const fetchTopologyViewGraph = (viewId: string) =>
|
||||
apiGet<TopologyViewGraph>(`/v1/topology/views/${encodeURIComponent(viewId)}`);
|
||||
export const fetchTopologyViewGraph = (
|
||||
viewId: string,
|
||||
opts?: {
|
||||
min_x?: number;
|
||||
max_x?: number;
|
||||
min_y?: number;
|
||||
max_y?: number;
|
||||
sbn_id?: string;
|
||||
folder_id?: string;
|
||||
lod?: "auto" | "sbn" | "me";
|
||||
status?: string;
|
||||
},
|
||||
) => {
|
||||
const q = new URLSearchParams();
|
||||
if (opts?.min_x != null) q.set("min_x", String(opts.min_x));
|
||||
if (opts?.max_x != null) q.set("max_x", String(opts.max_x));
|
||||
if (opts?.min_y != null) q.set("min_y", String(opts.min_y));
|
||||
if (opts?.max_y != null) q.set("max_y", String(opts.max_y));
|
||||
if (opts?.sbn_id) q.set("sbn_id", opts.sbn_id);
|
||||
if (opts?.folder_id) q.set("folder_id", opts.folder_id);
|
||||
if (opts?.lod) q.set("lod", opts.lod);
|
||||
if (opts?.status) q.set("status", opts.status);
|
||||
const qs = q.toString();
|
||||
return apiGet<TopologyViewGraph>(
|
||||
`/v1/topology/views/${encodeURIComponent(viewId)}${qs ? `?${qs}` : ""}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchTopologyWorld = () =>
|
||||
apiGet<{ view_id: string; folder_id: string; name: string }>("/v1/topology/world");
|
||||
|
||||
export const fetchTopologyFolderBBox = (folderId: string) =>
|
||||
apiGet<{ folder_id: string; min_x: number; max_x: number; min_y: number; max_y: number; n: number }>(
|
||||
`/v1/topology/folders/${encodeURIComponent(folderId)}/bbox`,
|
||||
);
|
||||
|
||||
export const updateTopologyView = (
|
||||
viewId: string,
|
||||
|
|
|
|||
|
|
@ -413,6 +413,7 @@ export type TopologyTreeFolderItem = {
|
|||
name: string;
|
||||
sort_order: number;
|
||||
is_system: boolean;
|
||||
external_ref?: string;
|
||||
views: TopologyTreeViewItem[];
|
||||
children: TopologyTreeFolderItem[];
|
||||
};
|
||||
|
|
@ -510,6 +511,11 @@ export type TopologyViewNodeItem = {
|
|||
device_type: string;
|
||||
connect_status: string;
|
||||
managed_source?: string;
|
||||
/** ne | region — region drills into a child canvas */
|
||||
kind?: string;
|
||||
folder_id?: string;
|
||||
view_id?: string;
|
||||
node_count?: number;
|
||||
};
|
||||
|
||||
export type TopologyViewEdgeItem = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue