diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index ea3e421..f02a8a0 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -1422,7 +1422,6 @@ const en = {
openRegion: "Open",
kindPhysical: "Physical",
kindCustom: "Custom",
- regionNodeHint: "{{count}}P",
layerNodeHint: "{{count}} NE(s)",
layerNodeEmpty: "Click to create & open",
rootCanvasHint: "Pick a root from the left tree.",
diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts
index 50a2894..fd465e2 100644
--- a/web/src/i18n/zh.ts
+++ b/web/src/i18n/zh.ts
@@ -1412,7 +1412,6 @@ const zh = {
openRegion: "打开",
kindPhysical: "物理拓扑",
kindCustom: "自定义图",
- regionNodeHint: "{{count}}P",
layerNodeHint: "{{count}} 个网元",
layerNodeEmpty: "点击创建并打开",
rootCanvasHint: "从左侧选择根进入目录。",
diff --git a/web/src/pages/TopologyPage.tsx b/web/src/pages/TopologyPage.tsx
index ead1c28..f8c1a5e 100644
--- a/web/src/pages/TopologyPage.tsx
+++ b/web/src/pages/TopologyPage.tsx
@@ -1,4 +1,5 @@
import {
+ Fragment,
createContext,
memo,
useCallback,
@@ -136,7 +137,7 @@ function indexFoldersById(
return acc;
}
-/** Self + ancestors (rootward), for expanding the left tree path. */
+/** Self + ancestors (leaf → rootward), for expanding the left tree path. */
function folderPathIds(
folders: TopologyTreeFolderItem[],
folderId: string,
@@ -157,6 +158,19 @@ function folderPathIds(
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. */
function isUmeWorldContainer(folder: TopologyTreeFolderItem | null | undefined): boolean {
if (!folder) return false;
@@ -1844,36 +1858,45 @@ export function TopologyPage() {
const goBackBrowse = useCallback(() => {
if (!confirmDiscardIfDirty()) return;
const folder = findFolderInTree(regions, selectedFolderId);
- // Leaving a region canvas under UME World → container hex nav.
- if (folder) {
- const path = folderPathIds(regions, folder.id);
- const container = regions.find((r) => isUmeWorldContainer(r));
- if (container && (folder.id === container.id || path.includes(container.id))) {
- setSelectedFolderId(container.id);
+ const parentId = String(folder?.parent_id || "").trim();
+ const parent = parentId ? findFolderInTree(regions, parentId) : null;
+ // One level up: parent canvas if nested, else parent hex nav, else roots.
+ if (parent) {
+ setSelectedFolderId(parent.id);
+ expandFolderPath(parent.id);
+ if (isRegionCanvasFolder(parent, String(treeRoot?.id || ""))) {
setWorldFocusFolderId("");
- setMapId("");
- clearDirty();
- setNodes([]);
- setEdges([]);
- 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("");
+ const view = primaryViewOfFolder(parent);
+ if (view) {
+ setMapId(view.id);
+ clearDirty();
+ return;
+ }
}
+ setWorldFocusFolderId("");
+ setMapId("");
+ clearDirty();
+ setNodes([]);
+ setEdges([]);
+ return;
}
+ setSelectedFolderId("");
setWorldFocusFolderId("");
setMapId("");
clearDirty();
setNodes([]);
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=
useEffect(() => {
@@ -2296,7 +2319,6 @@ export function TopologyPage() {
const focused = viewActiveUnder || (isSelected && Boolean(mapId));
const regionActive = isSelected && !mapId;
const hot = hotBrowseKey === `region:${folder.id}`;
- const childCount = kids.length;
const renderViewRow = (v: TopologyTreeViewItem) => {
const isPhysical = String(v.kind) === "physical";
@@ -2346,9 +2368,6 @@ export function TopologyPage() {
{displayViewName(v.name, t)}
{viewActive && dirty ? " *" : ""}
- {!umeNav ? (
- {v.node_count || 0}N
- ) : null}
{!umeNav ? (
@@ -2433,24 +2452,6 @@ export function TopologyPage() {
{regionDisplayName(folder, t)}
-
- {(() => {
- 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 "";
- })()}
-
{(() => {
@@ -4063,6 +4064,12 @@ export function TopologyPage() {
return displayViewName(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(() => {
if (canvasMode) return activeLeafName || t("topology.selectMap");
if (activeRegion) return regionDisplayName(activeRegion, t);
@@ -4338,27 +4345,37 @@ export function TopologyPage() {
>
{t("topology.rootName")}
- {activeRegion ? (
- <>
- /
- {activeView &&
- isRegionCanvasFolder(activeRegion, rootFolderId) &&
- primaryViewOfFolder(activeRegion)?.id === activeView.id ? (
-
- {regionDisplayName(activeRegion, t)}
- {dirty ? " *" : ""}
-
- ) : (
-
- )}
- >
- ) : null}
+ {breadcrumbFolders.map((folder, idx) => {
+ const isLast = idx === breadcrumbFolders.length - 1;
+ const primaryView = primaryViewOfFolder(folder);
+ const showExtraView =
+ isLast &&
+ Boolean(activeView) &&
+ !(
+ isRegionCanvasFolder(folder, rootFolderId) &&
+ primaryView?.id === activeView?.id
+ );
+ const asCurrent = isLast && !showExtraView;
+ return (
+
+ /
+ {asCurrent ? (
+
+ {regionDisplayName(folder, t)}
+ {dirty ? " *" : ""}
+
+ ) : (
+
+ )}
+
+ );
+ })}
{activeView &&
!(
activeRegion &&
@@ -4368,7 +4385,7 @@ export function TopologyPage() {
<>
/
- {activeView.name}
+ {displayViewName(activeView.name, t)}
{dirty ? " *" : ""}
>
@@ -4614,8 +4631,27 @@ export function TopologyPage() {
>
{t("topology.rootName")}
- /
- {regionDisplayName(hexBrowseRegion, t)}
+ {breadcrumbFolders.map((folder, idx) => {
+ const isLast = idx === breadcrumbFolders.length - 1;
+ return (
+
+ /
+ {isLast ? (
+
+ {regionDisplayName(folder, t)}
+
+ ) : (
+
+ )}
+
+ );
+ })}
) : (
{titleText}
@@ -4715,16 +4751,6 @@ export function TopologyPage() {
{regionDisplayName(region, t)}
-
- {t("topology.regionNodeHint").replace(
- "{{count}}",
- String(
- isUmeWorldContainer(region)
- ? (region.children?.length || 0) + (region.views?.length || 0)
- : region.views?.length || 0,
- ),
- )}
-
))}
@@ -4769,12 +4795,6 @@ export function TopologyPage() {
{regionDisplayName(umeWorldHexModules.drill, t)}
-
- {t("topology.regionNodeHint").replace(
- "{{count}}",
- String(umeWorldHexModules.drill.children?.length || 0),
- )}
-
) : null}
@@ -4805,12 +4825,6 @@ export function TopologyPage() {
{umeWorldHexModules.flatView.name}
-
- {t("topology.regionNodeHint").replace(
- "{{count}}",
- String(umeWorldHexModules.flatView.node_count || 0),
- )}
-
) : null}
@@ -4836,12 +4850,6 @@ export function TopologyPage() {
{regionDisplayName(region, t)}
-
- {t("topology.regionNodeHint").replace(
- "{{count}}",
- String(region.views?.length || region.children?.length || 0),
- )}
-
))}
@@ -4867,7 +4875,6 @@ export function TopologyPage() {
{displayViewName(v.name, t)}
- {v.node_count || 0}N
);