Restore topology and WebCRT as workbench modules.

Also tighten config-sync policy form alignment and make the network nav collapse control icon-only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-07-31 10:55:51 +08:00
parent f5b4a14c0f
commit abb5172a8f
11 changed files with 97 additions and 37 deletions

View file

@ -35,17 +35,17 @@ src/
| `/ume` | UME 同步 | `ume` | | `/ume` | UME 同步 | `ume` |
| `/network` | 网络管理(左栏 + 子路由) | `network` | | `/network` | 网络管理(左栏 + 子路由) | `network` |
| `/network/devices` | 设备列表 | `network` | | `/network/devices` | 设备列表 | `network` |
| `/network/topology` | 拓扑信息 | `network` |
| `/network/alarms` | 告警信息 | `network` | | `/network/alarms` | 告警信息 | `network` |
| `/network/configs` | 配置信息(同步快照) | `network` | | `/network/configs` | 配置信息(同步快照) | `network` |
| `/network/webcrt` | WebCRT 终端 | `network` | | `/network/webcrt` | redirect → `/webcrt`(保留 query) | — |
| `/network/topology` | redirect → `/topology` | — |
| `/network/tasks/collect` | 采集任务 | `network` | | `/network/tasks/collect` | 采集任务 | `network` |
| `/network/tasks/config-sync` | 配置同步 | `network` | | `/network/tasks/config-sync` | 配置同步 | `network` |
| `/network/tasks/port-traffic` | 端口流量(占位) | `network` | | `/network/tasks/port-traffic` | 端口流量(占位) | `network` |
| `/topology` | 拓扑管理 | `topology` |
| `/webcrt` | WebCRT 终端 | `webcrt` |
| `/ne` | redirect → `/network/devices` | — | | `/ne` | redirect → `/network/devices` | — |
| `/collect` | redirect → `/network/tasks/collect` | — | | `/collect` | redirect → `/network/tasks/collect` | — |
| `/topology` | redirect → `/network/topology` | — |
| `/webcrt` | redirect → `/network/webcrt`(保留 query) | — |
菜单树见 `config/networkNav.ts`。**新增网络子页:** 菜单树见 `config/networkNav.ts`。**新增网络子页:**

View file

@ -21,10 +21,10 @@ import { ForceChangePasswordPage } from "./pages/ForceChangePasswordPage";
import { fetchIntegrationStatus } from "./services/api"; import { fetchIntegrationStatus } from "./services/api";
import { useAuth } from "./auth/AuthContext"; import { useAuth } from "./auth/AuthContext";
/** Preserve ?ne_id=… when redirecting legacy /webcrt into the network module. */ /** Preserve query when redirecting legacy /network/webcrt → /webcrt. */
function WebcrtLegacyRedirect() { function NetworkWebcrtRedirect() {
const { search } = useLocation(); const { search } = useLocation();
return <Navigate to={`/network/webcrt${search}`} replace />; return <Navigate to={`/webcrt${search}`} replace />;
} }
function ProtectedApp() { function ProtectedApp() {
@ -82,21 +82,21 @@ function ProtectedApp() {
<Route path="/" element={<WorkbenchPage />} /> <Route path="/" element={<WorkbenchPage />} />
<Route path="/workbench" element={<Navigate to="/" replace />} /> <Route path="/workbench" element={<Navigate to="/" replace />} />
<Route path="/ume" element={<UmePage />} /> <Route path="/ume" element={<UmePage />} />
<Route path="/topology" element={<TopologyPage />} />
<Route path="/webcrt" element={<WebcrtPage />} />
<Route path="/network" element={<NetworkLayout />}> <Route path="/network" element={<NetworkLayout />}>
<Route index element={<Navigate to="devices" replace />} /> <Route index element={<Navigate to="devices" replace />} />
<Route path="devices" element={<NePage />} /> <Route path="devices" element={<NePage />} />
<Route path="topology" element={<TopologyPage />} />
<Route path="alarms" element={<NetworkAlarmsPage />} /> <Route path="alarms" element={<NetworkAlarmsPage />} />
<Route path="configs" element={<NetworkConfigsPage />} /> <Route path="configs" element={<NetworkConfigsPage />} />
<Route path="webcrt" element={<WebcrtPage />} /> <Route path="topology" element={<Navigate to="/topology" replace />} />
<Route path="webcrt" element={<NetworkWebcrtRedirect />} />
<Route path="tasks/collect" element={<CollectPage />} /> <Route path="tasks/collect" element={<CollectPage />} />
<Route path="tasks/config-sync" element={<ConfigSyncPage />} /> <Route path="tasks/config-sync" element={<ConfigSyncPage />} />
<Route path="tasks/port-traffic" element={<NetworkPlaceholderPage kind="port-traffic" />} /> <Route path="tasks/port-traffic" element={<NetworkPlaceholderPage kind="port-traffic" />} />
</Route> </Route>
<Route path="/ne" element={<Navigate to="/network/devices" replace />} /> <Route path="/ne" element={<Navigate to="/network/devices" replace />} />
<Route path="/collect" element={<Navigate to="/network/tasks/collect" replace />} /> <Route path="/collect" element={<Navigate to="/network/tasks/collect" replace />} />
<Route path="/topology" element={<Navigate to="/network/topology" replace />} />
<Route path="/webcrt" element={<WebcrtLegacyRedirect />} />
<Route path="/users" element={<UsersPage />} /> <Route path="/users" element={<UsersPage />} />
<Route path="/audit" element={<AuditPage />} /> <Route path="/audit" element={<AuditPage />} />
<Route path="/api-keys" element={<ApiTokensPage />} /> <Route path="/api-keys" element={<ApiTokensPage />} />

View file

@ -35,6 +35,24 @@ export const MODULES: readonly ModuleDefinition[] = [
iconTone: "green", iconTone: "green",
titleKey: "layout.titleNetwork", titleKey: "layout.titleNetwork",
}, },
{
moduleId: "topology",
path: "/topology",
section: "operations",
labelKey: "workbench.cards.topology",
descKey: "workbench.cards.topologyDesc",
iconTone: "amber",
titleKey: "layout.titleTopology",
},
{
moduleId: "webcrt",
path: "/webcrt",
section: "operations",
labelKey: "workbench.cards.webcrt",
descKey: "workbench.cards.webcrtDesc",
iconTone: "slate",
titleKey: "layout.titleWebcrt",
},
{ {
moduleId: "users", moduleId: "users",
path: "/users", path: "/users",

View file

@ -24,10 +24,8 @@ export const NETWORK_NAV: readonly NetworkNavGroup[] = [
labelKey: "network.nav.detail", labelKey: "network.nav.detail",
items: [ items: [
{ id: "devices", path: "/network/devices", labelKey: "network.nav.devices", group: "detail" }, { id: "devices", path: "/network/devices", labelKey: "network.nav.devices", group: "detail" },
{ id: "topology", path: "/network/topology", labelKey: "network.nav.topology", group: "detail" },
{ id: "alarms", path: "/network/alarms", labelKey: "network.nav.alarms", group: "detail" }, { id: "alarms", path: "/network/alarms", labelKey: "network.nav.alarms", group: "detail" },
{ id: "configs", path: "/network/configs", labelKey: "network.nav.configs", group: "detail" }, { id: "configs", path: "/network/configs", labelKey: "network.nav.configs", group: "detail" },
{ id: "webcrt", path: "/network/webcrt", labelKey: "network.nav.webcrt", group: "detail" },
], ],
}, },
{ {

View file

@ -24,7 +24,7 @@ const en = {
umeSync: "UME Sync", umeSync: "UME Sync",
umeSyncDesc: "UME alarms, subscription & inventory", umeSyncDesc: "UME alarms, subscription & inventory",
network: "Network Management", network: "Network Management",
networkDesc: "Devices, topology, alarms, WebCRT, and monitoring tasks", networkDesc: "Devices, alarms, config sync, and monitoring tasks",
managedNe: "NE Management", managedNe: "NE Management",
managedNeDesc: "Multi-vendor SSH/Telnet NEs & connectivity test", managedNeDesc: "Multi-vendor SSH/Telnet NEs & connectivity test",
collect: "Batch Collect", collect: "Batch Collect",

View file

@ -24,7 +24,7 @@ const zh = {
umeSync: "UME同步", umeSync: "UME同步",
umeSyncDesc: "UME 告警同步、订阅与清单", umeSyncDesc: "UME 告警同步、订阅与清单",
network: "网络管理", network: "网络管理",
networkDesc: "设备、拓扑、告警、WebCRT 与监控任务", networkDesc: "设备、告警、配置同步与监控任务",
managedNe: "网元管理", managedNe: "网元管理",
managedNeDesc: "多厂商 SSH/Telnet 网元与连通性测试", managedNeDesc: "多厂商 SSH/Telnet 网元与连通性测试",
collect: "批量采集", collect: "批量采集",

View file

@ -798,6 +798,52 @@ pre {
width: 100%; width: 100%;
} }
.config-sync-policy-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 16px;
}
.config-sync-policy-check {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
font-size: 13px;
color: #334155;
white-space: nowrap;
cursor: pointer;
}
.config-sync-policy-check input {
width: 14px;
height: 14px;
margin: 0;
flex-shrink: 0;
}
.config-sync-policy-field {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
font-size: 13px;
color: #334155;
white-space: nowrap;
}
.config-sync-policy-field input[type="number"] {
width: 64px;
margin: 0;
}
.config-sync-policy-field select {
width: auto;
min-width: 110px;
margin: 0;
}
.ne-stats-card { .ne-stats-card {
padding: 12px 14px; padding: 12px 14px;
} }
@ -2708,12 +2754,13 @@ pre {
width: 28px; width: 28px;
height: 28px; height: 28px;
padding: 0; padding: 0;
border: 1px solid #cbd5e1; border: 0;
border-radius: 6px; border-radius: 6px;
background: #fff; background: transparent;
color: #475569; color: #64748b;
cursor: pointer; cursor: pointer;
line-height: 1; line-height: 1;
box-shadow: none;
} }
.app-main .network-nav__collapse-btn:hover, .app-main .network-nav__collapse-btn:hover,

View file

@ -235,46 +235,43 @@ export function ConfigSyncPage() {
<div className="panel" style={{ marginBottom: 16 }}> <div className="panel" style={{ marginBottom: 16 }}>
<h3>{t("configSync.policyTitle")}</h3> <h3>{t("configSync.policyTitle")}</h3>
<div className="filter-inline" style={{ flexWrap: "wrap", gap: 12 }}> <div className="config-sync-policy-row">
<label> <label className="config-sync-policy-check">
<input type="checkbox" checked={enabled} onChange={(e) => setEnabled(e.target.checked)} />{" "} <input type="checkbox" checked={enabled} onChange={(e) => setEnabled(e.target.checked)} />
{t("configSync.enabled")} <span>{t("configSync.enabled")}</span>
</label> </label>
<label> <label className="config-sync-policy-field">
{t("configSync.intervalDays")}{" "} <span>{t("configSync.intervalDays")}</span>
<input <input
type="number" type="number"
min={1} min={1}
max={365} max={365}
value={intervalDays} value={intervalDays}
onChange={(e) => setIntervalDays(Math.max(1, Number(e.target.value) || 1))} onChange={(e) => setIntervalDays(Math.max(1, Number(e.target.value) || 1))}
style={{ width: 72 }}
/> />
</label> </label>
<label> <label className="config-sync-policy-field">
{t("configSync.concurrency")}{" "} <span>{t("configSync.concurrency")}</span>
<input <input
type="number" type="number"
min={1} min={1}
max={30} max={30}
value={concurrency} value={concurrency}
onChange={(e) => setConcurrency(Math.max(1, Math.min(30, Number(e.target.value) || 1)))} onChange={(e) => setConcurrency(Math.max(1, Math.min(30, Number(e.target.value) || 1)))}
style={{ width: 72 }}
/> />
</label> </label>
<label> <label className="config-sync-policy-field">
{t("configSync.historyKeep")}{" "} <span>{t("configSync.historyKeep")}</span>
<input <input
type="number" type="number"
min={0} min={0}
max={30} max={30}
value={historyKeep} value={historyKeep}
onChange={(e) => setHistoryKeep(Math.max(0, Math.min(30, Number(e.target.value) || 0)))} onChange={(e) => setHistoryKeep(Math.max(0, Math.min(30, Number(e.target.value) || 0)))}
style={{ width: 72 }}
/> />
</label> </label>
<label> <label className="config-sync-policy-field">
{t("configSync.scope")}{" "} <span>{t("configSync.scope")}</span>
<select <select
value={scopeMode} value={scopeMode}
onChange={(e) => setScopeMode(e.target.value === "selected" ? "selected" : "all")} onChange={(e) => setScopeMode(e.target.value === "selected" ? "selected" : "all")}

View file

@ -781,7 +781,7 @@ export function NePage() {
onClick={() => onClick={() =>
openOrFocusModule({ openOrFocusModule({
moduleId: "network", moduleId: "network",
path: `/network/webcrt?ne_id=${encodeURIComponent(row.id)}`, path: `/webcrt?ne_id=${encodeURIComponent(row.id)}`,
}) })
} }
title={t("managedNe.openTerminal")} title={t("managedNe.openTerminal")}

View file

@ -677,14 +677,14 @@ export function TopologyPage() {
if (managedId) { if (managedId) {
openOrFocusModule({ openOrFocusModule({
moduleId: "network", moduleId: "network",
path: `/network/webcrt?ne_id=${encodeURIComponent(managedId)}`, path: `/webcrt?ne_id=${encodeURIComponent(managedId)}`,
}); });
return; return;
} }
if (umeId) { if (umeId) {
openOrFocusModule({ openOrFocusModule({
moduleId: "network", moduleId: "network",
path: `/network/webcrt?ne_id=${encodeURIComponent(umeId)}&source=ume`, path: `/webcrt?ne_id=${encodeURIComponent(umeId)}&source=ume`,
}); });
return; return;
} }

View file

@ -125,7 +125,7 @@ export function NetworkConfigsPage() {
{t("networkConfigs.view")} {t("networkConfigs.view")}
</button> </button>
<Link <Link
to={`/network/webcrt?${row.source === "ume" ? "ume_ne_id" : "ne_id"}=${encodeURIComponent(row.target_id)}`} to={`/webcrt?${row.source === "ume" ? "ume_ne_id" : "ne_id"}=${encodeURIComponent(row.target_id)}`}
style={{ marginLeft: 8 }} style={{ marginLeft: 8 }}
> >
WebCRT WebCRT