mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 05:30:46 +08:00
Show folder breadcrumb hierarchy with one-level back, and drop unused P/N badges.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f1058f5581
commit
bbbc5a9f25
3 changed files with 105 additions and 100 deletions
|
|
@ -1422,7 +1422,6 @@ const en = {
|
||||||
openRegion: "Open",
|
openRegion: "Open",
|
||||||
kindPhysical: "Physical",
|
kindPhysical: "Physical",
|
||||||
kindCustom: "Custom",
|
kindCustom: "Custom",
|
||||||
regionNodeHint: "{{count}}P",
|
|
||||||
layerNodeHint: "{{count}} NE(s)",
|
layerNodeHint: "{{count}} NE(s)",
|
||||||
layerNodeEmpty: "Click to create & open",
|
layerNodeEmpty: "Click to create & open",
|
||||||
rootCanvasHint: "Pick a root from the left tree.",
|
rootCanvasHint: "Pick a root from the left tree.",
|
||||||
|
|
|
||||||
|
|
@ -1412,7 +1412,6 @@ const zh = {
|
||||||
openRegion: "打开",
|
openRegion: "打开",
|
||||||
kindPhysical: "物理拓扑",
|
kindPhysical: "物理拓扑",
|
||||||
kindCustom: "自定义图",
|
kindCustom: "自定义图",
|
||||||
regionNodeHint: "{{count}}P",
|
|
||||||
layerNodeHint: "{{count}} 个网元",
|
layerNodeHint: "{{count}} 个网元",
|
||||||
layerNodeEmpty: "点击创建并打开",
|
layerNodeEmpty: "点击创建并打开",
|
||||||
rootCanvasHint: "从左侧选择根进入目录。",
|
rootCanvasHint: "从左侧选择根进入目录。",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
Fragment,
|
||||||
createContext,
|
createContext,
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
|
@ -136,7 +137,7 @@ function indexFoldersById(
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Self + ancestors (rootward), for expanding the left tree path. */
|
/** Self + ancestors (leaf → rootward), for expanding the left tree path. */
|
||||||
function folderPathIds(
|
function folderPathIds(
|
||||||
folders: TopologyTreeFolderItem[],
|
folders: TopologyTreeFolderItem[],
|
||||||
folderId: string,
|
folderId: string,
|
||||||
|
|
@ -157,6 +158,19 @@ function folderPathIds(
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ancestors + self (rootward → leaf) for breadcrumb display. */
|
||||||
|
function folderPathFolders(
|
||||||
|
folders: TopologyTreeFolderItem[],
|
||||||
|
folderId: string,
|
||||||
|
): TopologyTreeFolderItem[] {
|
||||||
|
if (!folderId) return [];
|
||||||
|
const byId = indexFoldersById(folders);
|
||||||
|
return folderPathIds(folders, folderId)
|
||||||
|
.map((id) => byId.get(id))
|
||||||
|
.filter((f): f is TopologyTreeFolderItem => Boolean(f))
|
||||||
|
.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
/** UME World container only — hex nav, not a canvas. */
|
/** UME World container only — hex nav, not a canvas. */
|
||||||
function isUmeWorldContainer(folder: TopologyTreeFolderItem | null | undefined): boolean {
|
function isUmeWorldContainer(folder: TopologyTreeFolderItem | null | undefined): boolean {
|
||||||
if (!folder) return false;
|
if (!folder) return false;
|
||||||
|
|
@ -1844,36 +1858,45 @@ export function TopologyPage() {
|
||||||
const goBackBrowse = useCallback(() => {
|
const goBackBrowse = useCallback(() => {
|
||||||
if (!confirmDiscardIfDirty()) return;
|
if (!confirmDiscardIfDirty()) return;
|
||||||
const folder = findFolderInTree(regions, selectedFolderId);
|
const folder = findFolderInTree(regions, selectedFolderId);
|
||||||
// Leaving a region canvas under UME World → container hex nav.
|
const parentId = String(folder?.parent_id || "").trim();
|
||||||
if (folder) {
|
const parent = parentId ? findFolderInTree(regions, parentId) : null;
|
||||||
const path = folderPathIds(regions, folder.id);
|
// One level up: parent canvas if nested, else parent hex nav, else roots.
|
||||||
const container = regions.find((r) => isUmeWorldContainer(r));
|
if (parent) {
|
||||||
if (container && (folder.id === container.id || path.includes(container.id))) {
|
setSelectedFolderId(parent.id);
|
||||||
setSelectedFolderId(container.id);
|
expandFolderPath(parent.id);
|
||||||
|
if (isRegionCanvasFolder(parent, String(treeRoot?.id || ""))) {
|
||||||
setWorldFocusFolderId("");
|
setWorldFocusFolderId("");
|
||||||
setMapId("");
|
const view = primaryViewOfFolder(parent);
|
||||||
clearDirty();
|
if (view) {
|
||||||
setNodes([]);
|
setMapId(view.id);
|
||||||
setEdges([]);
|
clearDirty();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Leaving other region canvases → parent nav container, else root hex.
|
|
||||||
if (isRegionCanvasFolder(folder, String(treeRoot?.id || "")) || isUmeWorldNavFolder(folder)) {
|
|
||||||
const parentId = String(folder?.parent_id || "").trim();
|
|
||||||
const parent = parentId ? findFolderInTree(regions, parentId) : null;
|
|
||||||
if (parent && !isRegionCanvasFolder(parent, String(treeRoot?.id || ""))) {
|
|
||||||
setSelectedFolderId(parent.id);
|
|
||||||
} else {
|
|
||||||
setSelectedFolderId("");
|
|
||||||
}
|
}
|
||||||
|
setWorldFocusFolderId("");
|
||||||
|
setMapId("");
|
||||||
|
clearDirty();
|
||||||
|
setNodes([]);
|
||||||
|
setEdges([]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
setSelectedFolderId("");
|
||||||
setWorldFocusFolderId("");
|
setWorldFocusFolderId("");
|
||||||
setMapId("");
|
setMapId("");
|
||||||
clearDirty();
|
clearDirty();
|
||||||
setNodes([]);
|
setNodes([]);
|
||||||
setEdges([]);
|
setEdges([]);
|
||||||
}, [confirmDiscardIfDirty, clearDirty, setNodes, setEdges, regions, selectedFolderId, treeRoot?.id]);
|
}, [
|
||||||
|
confirmDiscardIfDirty,
|
||||||
|
clearDirty,
|
||||||
|
setNodes,
|
||||||
|
setEdges,
|
||||||
|
regions,
|
||||||
|
selectedFolderId,
|
||||||
|
treeRoot?.id,
|
||||||
|
expandFolderPath,
|
||||||
|
primaryViewOfFolder,
|
||||||
|
]);
|
||||||
|
|
||||||
// Deep link from classify search: /topology?view=&ne=
|
// Deep link from classify search: /topology?view=&ne=
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -2296,7 +2319,6 @@ export function TopologyPage() {
|
||||||
const focused = viewActiveUnder || (isSelected && Boolean(mapId));
|
const focused = viewActiveUnder || (isSelected && Boolean(mapId));
|
||||||
const regionActive = isSelected && !mapId;
|
const regionActive = isSelected && !mapId;
|
||||||
const hot = hotBrowseKey === `region:${folder.id}`;
|
const hot = hotBrowseKey === `region:${folder.id}`;
|
||||||
const childCount = kids.length;
|
|
||||||
|
|
||||||
const renderViewRow = (v: TopologyTreeViewItem) => {
|
const renderViewRow = (v: TopologyTreeViewItem) => {
|
||||||
const isPhysical = String(v.kind) === "physical";
|
const isPhysical = String(v.kind) === "physical";
|
||||||
|
|
@ -2346,9 +2368,6 @@ export function TopologyPage() {
|
||||||
{displayViewName(v.name, t)}
|
{displayViewName(v.name, t)}
|
||||||
{viewActive && dirty ? " *" : ""}
|
{viewActive && dirty ? " *" : ""}
|
||||||
</span>
|
</span>
|
||||||
{!umeNav ? (
|
|
||||||
<span className="topo-map-list__count">{v.node_count || 0}N</span>
|
|
||||||
) : null}
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{!umeNav ? (
|
{!umeNav ? (
|
||||||
|
|
@ -2433,24 +2452,6 @@ export function TopologyPage() {
|
||||||
<RegionGlyph size={14} />
|
<RegionGlyph size={14} />
|
||||||
</span>
|
</span>
|
||||||
<span className="topo-map-list__title">{regionDisplayName(folder, t)}</span>
|
<span className="topo-map-list__title">{regionDisplayName(folder, t)}</span>
|
||||||
<span className="topo-map-list__count">
|
|
||||||
{(() => {
|
|
||||||
if (containerFolder) {
|
|
||||||
return t("topology.regionNodeHint").replace(
|
|
||||||
"{{count}}",
|
|
||||||
String(kids.length + visibleViews.length),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const neCount = (folder.views || []).reduce(
|
|
||||||
(sum, v) => sum + (Number(v.node_count) || 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
// Prefer canvas NE count; fall back to child-region count.
|
|
||||||
if (neCount > 0) return `${neCount}N`;
|
|
||||||
if (childCount > 0) return String(childCount);
|
|
||||||
return "";
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|
@ -4063,6 +4064,12 @@ export function TopologyPage() {
|
||||||
return displayViewName(graphQuery.data?.view?.name, t);
|
return displayViewName(graphQuery.data?.view?.name, t);
|
||||||
}, [mapId, activeView, graphQuery.data?.view?.name, t]);
|
}, [mapId, activeView, graphQuery.data?.view?.name, t]);
|
||||||
|
|
||||||
|
/** Full folder chain for breadcrumb (rootward → leaf). */
|
||||||
|
const breadcrumbFolders = useMemo(
|
||||||
|
() => folderPathFolders(regions, selectedFolderId),
|
||||||
|
[regions, selectedFolderId],
|
||||||
|
);
|
||||||
|
|
||||||
const titleText = useMemo(() => {
|
const titleText = useMemo(() => {
|
||||||
if (canvasMode) return activeLeafName || t("topology.selectMap");
|
if (canvasMode) return activeLeafName || t("topology.selectMap");
|
||||||
if (activeRegion) return regionDisplayName(activeRegion, t);
|
if (activeRegion) return regionDisplayName(activeRegion, t);
|
||||||
|
|
@ -4338,27 +4345,37 @@ export function TopologyPage() {
|
||||||
>
|
>
|
||||||
{t("topology.rootName")}
|
{t("topology.rootName")}
|
||||||
</button>
|
</button>
|
||||||
{activeRegion ? (
|
{breadcrumbFolders.map((folder, idx) => {
|
||||||
<>
|
const isLast = idx === breadcrumbFolders.length - 1;
|
||||||
<span className="topo-breadcrumb__sep">/</span>
|
const primaryView = primaryViewOfFolder(folder);
|
||||||
{activeView &&
|
const showExtraView =
|
||||||
isRegionCanvasFolder(activeRegion, rootFolderId) &&
|
isLast &&
|
||||||
primaryViewOfFolder(activeRegion)?.id === activeView.id ? (
|
Boolean(activeView) &&
|
||||||
<span className="topo-breadcrumb__current">
|
!(
|
||||||
{regionDisplayName(activeRegion, t)}
|
isRegionCanvasFolder(folder, rootFolderId) &&
|
||||||
{dirty ? " *" : ""}
|
primaryView?.id === activeView?.id
|
||||||
</span>
|
);
|
||||||
) : (
|
const asCurrent = isLast && !showExtraView;
|
||||||
<button
|
return (
|
||||||
type="button"
|
<Fragment key={folder.id}>
|
||||||
className="topo-breadcrumb__link"
|
<span className="topo-breadcrumb__sep">/</span>
|
||||||
onClick={() => goRegion(activeRegion.id)}
|
{asCurrent ? (
|
||||||
>
|
<span className="topo-breadcrumb__current">
|
||||||
{regionDisplayName(activeRegion, t)}
|
{regionDisplayName(folder, t)}
|
||||||
</button>
|
{dirty ? " *" : ""}
|
||||||
)}
|
</span>
|
||||||
</>
|
) : (
|
||||||
) : null}
|
<button
|
||||||
|
type="button"
|
||||||
|
className="topo-breadcrumb__link"
|
||||||
|
onClick={() => goRegion(folder.id)}
|
||||||
|
>
|
||||||
|
{regionDisplayName(folder, t)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{activeView &&
|
{activeView &&
|
||||||
!(
|
!(
|
||||||
activeRegion &&
|
activeRegion &&
|
||||||
|
|
@ -4368,7 +4385,7 @@ export function TopologyPage() {
|
||||||
<>
|
<>
|
||||||
<span className="topo-breadcrumb__sep">/</span>
|
<span className="topo-breadcrumb__sep">/</span>
|
||||||
<span className="topo-breadcrumb__current">
|
<span className="topo-breadcrumb__current">
|
||||||
{activeView.name}
|
{displayViewName(activeView.name, t)}
|
||||||
{dirty ? " *" : ""}
|
{dirty ? " *" : ""}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
|
|
@ -4614,8 +4631,27 @@ export function TopologyPage() {
|
||||||
>
|
>
|
||||||
{t("topology.rootName")}
|
{t("topology.rootName")}
|
||||||
</button>
|
</button>
|
||||||
<span className="topo-breadcrumb__sep">/</span>
|
{breadcrumbFolders.map((folder, idx) => {
|
||||||
<span className="topo-breadcrumb__current">{regionDisplayName(hexBrowseRegion, t)}</span>
|
const isLast = idx === breadcrumbFolders.length - 1;
|
||||||
|
return (
|
||||||
|
<Fragment key={folder.id}>
|
||||||
|
<span className="topo-breadcrumb__sep">/</span>
|
||||||
|
{isLast ? (
|
||||||
|
<span className="topo-breadcrumb__current">
|
||||||
|
{regionDisplayName(folder, t)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="topo-breadcrumb__link"
|
||||||
|
onClick={() => goRegion(folder.id)}
|
||||||
|
>
|
||||||
|
{regionDisplayName(folder, t)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<strong>{titleText}</strong>
|
<strong>{titleText}</strong>
|
||||||
|
|
@ -4715,16 +4751,6 @@ export function TopologyPage() {
|
||||||
</span>
|
</span>
|
||||||
<span className="topo-region-hex__title">
|
<span className="topo-region-hex__title">
|
||||||
<span className="topo-region-hex__name">{regionDisplayName(region, t)}</span>
|
<span className="topo-region-hex__name">{regionDisplayName(region, t)}</span>
|
||||||
<span className="topo-region-hex__meta">
|
|
||||||
{t("topology.regionNodeHint").replace(
|
|
||||||
"{{count}}",
|
|
||||||
String(
|
|
||||||
isUmeWorldContainer(region)
|
|
||||||
? (region.children?.length || 0) + (region.views?.length || 0)
|
|
||||||
: region.views?.length || 0,
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
@ -4769,12 +4795,6 @@ export function TopologyPage() {
|
||||||
<span className="topo-region-hex__name">
|
<span className="topo-region-hex__name">
|
||||||
{regionDisplayName(umeWorldHexModules.drill, t)}
|
{regionDisplayName(umeWorldHexModules.drill, t)}
|
||||||
</span>
|
</span>
|
||||||
<span className="topo-region-hex__meta">
|
|
||||||
{t("topology.regionNodeHint").replace(
|
|
||||||
"{{count}}",
|
|
||||||
String(umeWorldHexModules.drill.children?.length || 0),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -4805,12 +4825,6 @@ export function TopologyPage() {
|
||||||
<span className="topo-region-hex__name">
|
<span className="topo-region-hex__name">
|
||||||
{umeWorldHexModules.flatView.name}
|
{umeWorldHexModules.flatView.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="topo-region-hex__meta">
|
|
||||||
{t("topology.regionNodeHint").replace(
|
|
||||||
"{{count}}",
|
|
||||||
String(umeWorldHexModules.flatView.node_count || 0),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -4836,12 +4850,6 @@ export function TopologyPage() {
|
||||||
</span>
|
</span>
|
||||||
<span className="topo-region-hex__title">
|
<span className="topo-region-hex__title">
|
||||||
<span className="topo-region-hex__name">{regionDisplayName(region, t)}</span>
|
<span className="topo-region-hex__name">{regionDisplayName(region, t)}</span>
|
||||||
<span className="topo-region-hex__meta">
|
|
||||||
{t("topology.regionNodeHint").replace(
|
|
||||||
"{{count}}",
|
|
||||||
String(region.views?.length || region.children?.length || 0),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
@ -4867,7 +4875,6 @@ export function TopologyPage() {
|
||||||
</span>
|
</span>
|
||||||
<span className="topo-region-hex__title">
|
<span className="topo-region-hex__title">
|
||||||
<span className="topo-region-hex__name">{displayViewName(v.name, t)}</span>
|
<span className="topo-region-hex__name">{displayViewName(v.name, t)}</span>
|
||||||
<span className="topo-region-hex__meta">{v.node_count || 0}N</span>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue