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

View file

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

View file

@ -35,6 +35,24 @@ export const MODULES: readonly ModuleDefinition[] = [
iconTone: "green",
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",
path: "/users",

View file

@ -24,10 +24,8 @@ export const NETWORK_NAV: readonly NetworkNavGroup[] = [
labelKey: "network.nav.detail",
items: [
{ 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: "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",
umeSyncDesc: "UME alarms, subscription & inventory",
network: "Network Management",
networkDesc: "Devices, topology, alarms, WebCRT, and monitoring tasks",
networkDesc: "Devices, alarms, config sync, and monitoring tasks",
managedNe: "NE Management",
managedNeDesc: "Multi-vendor SSH/Telnet NEs & connectivity test",
collect: "Batch Collect",

View file

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

View file

@ -798,6 +798,52 @@ pre {
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 {
padding: 12px 14px;
}
@ -2708,12 +2754,13 @@ pre {
width: 28px;
height: 28px;
padding: 0;
border: 1px solid #cbd5e1;
border: 0;
border-radius: 6px;
background: #fff;
color: #475569;
background: transparent;
color: #64748b;
cursor: pointer;
line-height: 1;
box-shadow: none;
}
.app-main .network-nav__collapse-btn:hover,

View file

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

View file

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

View file

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

View file

@ -125,7 +125,7 @@ export function NetworkConfigsPage() {
{t("networkConfigs.view")}
</button>
<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 }}
>
WebCRT