diff --git a/web/WEB.md b/web/WEB.md index a3ecbf3..20bb221 100644 --- a/web/WEB.md +++ b/web/WEB.md @@ -33,11 +33,25 @@ src/ |------|------|----------| | `/` | 工作台 | — | | `/ume` | UME 同步 | `ume` | -| `/ne` | 网元管理 | `managed-ne` | -| `/collect` | 批量采集 | `collect` | -| `/webcrt` | WebCRT 终端 | `webcrt` | +| `/network` | 网络管理(左栏 + 子路由) | `network` | +| `/network/devices` | 设备列表 | `network` | +| `/network/topology` | 拓扑信息 | `network` | +| `/network/alarms` | 告警信息 | `network` | +| `/network/webcrt` | WebCRT 终端 | `network` | +| `/network/tasks/config-sync` | 采集任务(原批量采集) | `network` | +| `/network/tasks/port-traffic` | 端口流量(占位) | `network` | +| `/ne` | redirect → `/network/devices` | — | +| `/collect` | redirect → `/network/tasks/config-sync` | — | +| `/topology` | redirect → `/network/topology` | — | +| `/webcrt` | redirect → `/network/webcrt`(保留 query) | — | -**新增模块只需改 `config/modules.ts`:** +菜单树见 `config/networkNav.ts`。**新增网络子页:** + +1. 在 `networkNav.ts` 增加叶子项 +2. 在 `App.tsx` `/network` 下增加子 `` +3. 补充 i18n `network.*` + +**新增工作台模块仍改 `config/modules.ts`:** 1. 在 `MODULES` 增加一项(`moduleId`、`path`、`section`、i18n key、`iconTone`) 2. 在 `App.tsx` 增加 `` diff --git a/web/src/App.tsx b/web/src/App.tsx index 7e23861..4a91346 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,4 +1,4 @@ -import { Navigate, Route, Routes } from "react-router-dom"; +import { Navigate, Route, Routes, useLocation } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { AppLayout } from "./layout/AppLayout"; import { queryKeys } from "./constants/queryKeys"; @@ -8,6 +8,9 @@ import { NePage } from "./pages/NePage"; import { UmePage } from "./pages/UmePage"; import { WebcrtPage } from "./pages/WebcrtPage"; import { TopologyPage } from "./pages/TopologyPage"; +import { NetworkLayout } from "./pages/network/NetworkLayout"; +import { NetworkAlarmsPage } from "./pages/network/NetworkAlarmsPage"; +import { NetworkPlaceholderPage } from "./pages/network/NetworkPlaceholderPage"; import { LoginPage } from "./pages/LoginPage"; import { UsersPage } from "./pages/UsersPage"; import { AuditPage } from "./pages/AuditPage"; @@ -16,6 +19,12 @@ 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() { + const { search } = useLocation(); + return ; +} + function ProtectedApp() { const { ready, user } = useAuth(); const integrationsQuery = useQuery({ @@ -71,10 +80,19 @@ function ProtectedApp() { } /> } /> } /> - } /> - } /> - } /> - } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/web/src/config/modules.ts b/web/src/config/modules.ts index b3c8114..053b6ed 100644 --- a/web/src/config/modules.ts +++ b/web/src/config/modules.ts @@ -27,40 +27,13 @@ export const MODULES: readonly ModuleDefinition[] = [ titleKey: "layout.titleUme", }, { - moduleId: "managed-ne", - path: "/ne", + moduleId: "network", + path: "/network", section: "operations", - labelKey: "workbench.cards.managedNe", - descKey: "workbench.cards.managedNeDesc", + labelKey: "workbench.cards.network", + descKey: "workbench.cards.networkDesc", iconTone: "green", - titleKey: "layout.titleManagedNe", - }, - { - moduleId: "collect", - path: "/collect", - section: "operations", - labelKey: "workbench.cards.collect", - descKey: "workbench.cards.collectDesc", - iconTone: "amber", - titleKey: "layout.titleCollect", - }, - { - moduleId: "webcrt", - path: "/webcrt", - section: "operations", - labelKey: "workbench.cards.webcrt", - descKey: "workbench.cards.webcrtDesc", - iconTone: "slate", - titleKey: "layout.titleWebcrt", - }, - { - moduleId: "topology", - path: "/topology", - section: "operations", - labelKey: "workbench.cards.topology", - descKey: "workbench.cards.topologyDesc", - iconTone: "blue", - titleKey: "layout.titleTopology", + titleKey: "layout.titleNetwork", }, { moduleId: "users", diff --git a/web/src/config/networkNav.ts b/web/src/config/networkNav.ts new file mode 100644 index 0000000..1acabb8 --- /dev/null +++ b/web/src/config/networkNav.ts @@ -0,0 +1,50 @@ +/** + * Left-nav tree for the Network Management module. + * Add leaf items here when extending (devices / topology / alarms / tasks). + */ + +export type NetworkNavGroupId = "detail" | "tasks"; + +export type NetworkNavItem = { + id: string; + path: string; + labelKey: string; + group: NetworkNavGroupId; +}; + +export type NetworkNavGroup = { + id: NetworkNavGroupId; + labelKey: string; + items: NetworkNavItem[]; +}; + +export const NETWORK_NAV: readonly NetworkNavGroup[] = [ + { + id: "detail", + 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: "webcrt", path: "/network/webcrt", labelKey: "network.nav.webcrt", group: "detail" }, + ], + }, + { + id: "tasks", + labelKey: "network.nav.tasks", + items: [ + { + id: "config-sync", + path: "/network/tasks/config-sync", + labelKey: "network.nav.configSync", + group: "tasks", + }, + { + id: "port-traffic", + path: "/network/tasks/port-traffic", + labelKey: "network.nav.portTraffic", + group: "tasks", + }, + ], + }, +] as const; diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 3fce43c..4b74c33 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -23,6 +23,8 @@ const en = { cards: { umeSync: "UME Sync", umeSyncDesc: "UME alarms, subscription & inventory", + network: "Network Management", + networkDesc: "Devices, topology, alarms, WebCRT, and monitoring tasks", managedNe: "NE Management", managedNeDesc: "Multi-vendor SSH/Telnet NEs & connectivity test", collect: "Batch Collect", @@ -39,9 +41,33 @@ const en = { apiKeysDesc: "Issue MCP/script tokens per user with expiry", }, }, + network: { + title: "Network Management", + nav: { + detail: "Network details", + tasks: "Monitoring tasks", + devices: "Device list", + topology: "Topology", + alarms: "Alarms", + webcrt: "WebCRT", + configSync: "Collection tasks", + portTraffic: "Port traffic", + }, + placeholder: { + title: "Coming soon", + body: "This capability is not available yet.", + portTrafficTitle: "Port traffic monitoring", + portTrafficBody: "Planned: collect per-port traffic on a schedule and show trends. Suggested path:", + portTrafficStep1: "Task definition: NE/port scope, interval, retention", + portTrafficStep2: "Collection: prefer CLI (display/show interface); SNMP later at scale", + portTrafficStep3: "Storage: time series or hourly rollups (in/out bps, errors)", + portTrafficStep4: "UI: task list + per-port trend charts", + }, + }, layout: { title: "UME Integration", titleUme: "UME Sync", + titleNetwork: "Network Management", titleManagedNe: "NE Management", titleCollect: "Batch Collect", titleWebcrt: "WebCRT", @@ -614,6 +640,7 @@ const en = { }, alarms: { title: "Current alarms", + movedHint: "Current alarm query moved to Network Management →", showPanel: "Expand", hidePanel: "Collapse", keywordPh: "keyword(alarm key/cause/ne_name/host_name/ip…)", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index b26ab7c..65663f1 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -23,6 +23,8 @@ const zh = { cards: { umeSync: "UME同步", umeSyncDesc: "UME 告警同步、订阅与清单", + network: "网络管理", + networkDesc: "设备、拓扑、告警、WebCRT 与监控任务", managedNe: "网元管理", managedNeDesc: "多厂商 SSH/Telnet 网元与连通性测试", collect: "批量采集", @@ -39,9 +41,33 @@ const zh = { apiKeysDesc: "为用户生成 MCP/脚本用 Token,可设有效期", }, }, + network: { + title: "网络管理", + nav: { + detail: "网络详情", + tasks: "监控任务", + devices: "设备列表", + topology: "拓扑信息", + alarms: "告警信息", + webcrt: "WebCRT", + configSync: "采集任务", + portTraffic: "端口流量监控", + }, + placeholder: { + title: "功能规划中", + body: "该能力尚未开放。", + portTrafficTitle: "端口流量监控", + portTrafficBody: "规划中:按任务采集设备端口流量并展示趋势。建议路径如下。", + portTrafficStep1: "任务定义:选择网元/端口范围、采集周期与保留策略", + portTrafficStep2: "采集:优先 CLI(display/show interface),规模上来后再 SNMP", + portTrafficStep3: "存储:端口 in/out bps、error 等时序或按小时汇总", + portTrafficStep4: "界面:任务列表 + 单端口趋势图", + }, + }, layout: { title: "UME 对接", titleUme: "UME同步", + titleNetwork: "网络管理", titleManagedNe: "网元管理", titleCollect: "批量采集", titleWebcrt: "WebCRT", @@ -611,6 +637,7 @@ const zh = { }, alarms: { title: "当前告警", + movedHint: "当前告警查询已迁至网络管理 →", showPanel: "展开", hidePanel: "收起", keywordPh: "keyword(告警键/原因/ne_name/host_name/ip 等)", diff --git a/web/src/index.css b/web/src/index.css index 93f4793..3bec087 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -2612,3 +2612,140 @@ pre { opacity: 0.6; cursor: not-allowed; } + +/* —— Network Management shell (left nav + main) —— */ +.network-shell { + display: flex; + height: calc(100vh - 56px); + min-height: 480px; + margin: -20px -28px -32px; + background: #e8edf3; + overflow: hidden; +} + +.network-nav { + width: 220px; + flex-shrink: 0; + display: flex; + flex-direction: column; + background: #f7f9fc; + border-right: 1px solid #d0d7e2; + min-height: 0; +} + +.network-nav__scroll { + flex: 1; + min-height: 0; + overflow: auto; + padding: 6px 0 16px; +} + +.network-nav__group { + margin: 0; +} + +.app-main .network-nav__group-toggle, +.network-nav__group-toggle { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 10px 12px; + border: 0; + border-radius: 0; + background: transparent; + color: #0f172a; + font-size: 13px; + font-weight: 600; + text-align: left; + cursor: pointer; +} + +.app-main .network-nav__group-toggle:hover, +.network-nav__group-toggle:hover { + background: #eef2f7; +} + +.network-nav__group.is-active-group .network-nav__group-toggle { + color: #1565c0; +} + +.network-nav__chevron { + display: inline-block; + width: 1em; + color: #64748b; + font-size: 12px; + line-height: 1; +} + +.network-nav__group-label { + flex: 1; + min-width: 0; +} + +.network-nav__list { + list-style: none; + margin: 0; + padding: 0 0 6px; +} + +.network-nav__link { + display: block; + padding: 7px 12px 7px 30px; + color: #334155; + text-decoration: none; + font-size: 13px; + border-left: 3px solid transparent; +} + +.network-nav__link:hover { + background: #eef2f7; + color: #0f172a; +} + +.network-nav__link.is-active { + background: #e3effc; + color: #1565c0; + border-left-color: #1565c0; + font-weight: 600; +} + +.network-main { + flex: 1; + min-width: 0; + min-height: 0; + overflow: auto; + padding: 16px 20px 24px; + background: #eef2f7; +} + +.network-main:has(.topo-page), +.network-main:has(.webcrt-shell) { + padding: 0; + overflow: hidden; +} + +.network-main .topo-page, +.network-main .topo-page.is-sidebar-collapsed { + height: 100%; + margin: 0; + min-height: 0; +} + +.network-main .webcrt-shell { + height: 100%; + margin: 0; + min-height: 0; + border: 0; +} + +.network-placeholder h2 { + margin: 0 0 8px; +} + +.network-placeholder__list { + margin: 12px 0 0; + padding-left: 1.25rem; + color: #475569; + line-height: 1.6; +} diff --git a/web/src/pages/NePage.tsx b/web/src/pages/NePage.tsx index a39a6a2..179b6e1 100644 --- a/web/src/pages/NePage.tsx +++ b/web/src/pages/NePage.tsx @@ -26,7 +26,7 @@ import { useToast } from "../hooks/useToast"; import type { ManagedNeItem } from "../types"; import { pageCount } from "../utils/display"; import { formatSystemTime } from "../utils/time"; -import { openNewModuleWindow } from "../utils/moduleWindows"; +import { openOrFocusModule } from "../utils/moduleWindows"; import { defaultHopTemplate, isAutoHopTemplate, @@ -779,9 +779,9 @@ export function NePage() { type="button" className="link-btn" onClick={() => - openNewModuleWindow({ - moduleId: "webcrt", - path: `/webcrt?ne_id=${encodeURIComponent(row.id)}`, + openOrFocusModule({ + moduleId: "network", + path: `/network/webcrt?ne_id=${encodeURIComponent(row.id)}`, }) } title={t("managedNe.openTerminal")} diff --git a/web/src/pages/TopologyPage.tsx b/web/src/pages/TopologyPage.tsx index 4ac565b..463a3fe 100644 --- a/web/src/pages/TopologyPage.tsx +++ b/web/src/pages/TopologyPage.tsx @@ -35,7 +35,7 @@ import { queryKeys } from "../constants/queryKeys"; import { HelpHint } from "../components/HelpHint"; import { useI18n } from "../i18n"; import { useToast } from "../hooks/useToast"; -import { openNewModuleWindow, openOrFocusModule } from "../utils/moduleWindows"; +import { openOrFocusModule } from "../utils/moduleWindows"; import type { ManagedNeItem, TopologyDiscoverNeResult, @@ -675,16 +675,16 @@ export function TopologyPage() { const managedId = node?.data.managed_ne_id; const umeId = node?.data.ume_ne_id; if (managedId) { - openNewModuleWindow({ - moduleId: "webcrt", - path: `/webcrt?ne_id=${encodeURIComponent(managedId)}`, + openOrFocusModule({ + moduleId: "network", + path: `/network/webcrt?ne_id=${encodeURIComponent(managedId)}`, }); return; } if (umeId) { - openNewModuleWindow({ - moduleId: "webcrt", - path: `/webcrt?ne_id=${encodeURIComponent(umeId)}&source=ume`, + openOrFocusModule({ + moduleId: "network", + path: `/network/webcrt?ne_id=${encodeURIComponent(umeId)}&source=ume`, }); return; } @@ -697,8 +697,8 @@ export function TopologyPage() { const umeId = node?.data.ume_ne_id; if (managedId) { openOrFocusModule({ - moduleId: "managed-ne", - path: "/ne", + moduleId: "network", + path: "/network/devices", }); return; } diff --git a/web/src/pages/UmePage.tsx b/web/src/pages/UmePage.tsx index 3726c07..f2ae06d 100644 --- a/web/src/pages/UmePage.tsx +++ b/web/src/pages/UmePage.tsx @@ -3,7 +3,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiPost, disconnectUmeToken, - fetchUmeCurrentAlarms, fetchUmeNe, cancelUmeAlarmSubscription, clearLocalUmeAlarmSubscription, @@ -31,6 +30,7 @@ import type { UmeAlarmSubscriptionStatus, UmeKeyAlertRuleItem } from "../types"; import { pageCount, runtimeIntervalLabel } from "../utils/display"; import { runtimeLastError, wsConnectionLabel } from "../utils/runtimeMessages"; import { formatSystemTime } from "../utils/time"; +import { openOrFocusModule } from "../utils/moduleWindows"; export function UmePage() { const { t } = useI18n(); @@ -48,13 +48,6 @@ export function UmePage() { const [nePanelOpen, setNePanelOpen] = useState(false); const [syncStatusPanelOpen, setSyncStatusPanelOpen] = useState(false); const [keyAlertPanelOpen, setKeyAlertPanelOpen] = useState(false); - const [curSeverity, setCurSeverity] = useState(""); - const [curCleared, setCurCleared] = useState(""); - const [curHostName, setCurHostName] = useState(""); - const [curKeyword, setCurKeyword] = useState(""); - const [curPage, setCurPage] = useState(1); - const [curPageSize, setCurPageSize] = useState(50); - const [alarmsPanelOpen, setAlarmsPanelOpen] = useState(false); const [keyAlertMatchType, setKeyAlertMatchType] = useState<"notification_id" | "keyword">("keyword"); const [keyAlertMatchValue, setKeyAlertMatchValue] = useState(""); const [keyAlertLabel, setKeyAlertLabel] = useState(""); @@ -264,19 +257,6 @@ export function UmePage() { } return map; }, [cliTargetsQuery.data]); - const currentQuery = useQuery({ - queryKey: queryKeys.umeCurrentAlarms(curSeverity, curCleared, curHostName, curKeyword, curPage, curPageSize), - queryFn: () => - fetchUmeCurrentAlarms({ - severity: curSeverity, - isCleared: curCleared, - hostName: curHostName, - keyword: curKeyword, - page: curPage, - pageSize: curPageSize, - }), - staleTime: 5000, - }); const runningTasks = (syncStatusQuery.data?.items || []).filter((x) => String(x.status || "").toLowerCase() === "running"); const runtimeTasks = syncStatusQuery.data?.runtime_tasks || []; @@ -425,8 +405,6 @@ export function UmePage() { const syncPages = pageCount(syncTotal, syncPageSize); const neTotal = Number(neQuery.data?.total || 0); const nePages = pageCount(neTotal, nePageSize); - const curTotal = Number(currentQuery.data?.total || 0); - const curPages = pageCount(curTotal, curPageSize); const keyAlertForwarder = keyAlertMonitorQuery.data?.forwarder; const keyAlertRules = keyAlertMonitorQuery.data?.rules || []; @@ -1350,112 +1328,16 @@ export function UmePage() {

{t("ume.alarms.title")}

-
- {alarmsPanelOpen ? ( - <> -
- setCurKeyword(e.target.value)} /> - setCurHostName(e.target.value)} /> - - - -
- - - - - - - - - - - - - {(currentQuery.data?.items || []).map((x) => ( - - - - - - - - - ))} - -
time_createdseverityne_idhost_namene_typecause
{formatSystemTime(x.time_created)}{x.perceived_severity}{x.ne_id} - {(x.host_name || "").trim() ? ( - - ) : ( - - {t("common.empty")} - - )} - {x.ne_type ?? ""}{x.native_probable_cause}
-
-
{t("common.pagerMeta", { total: curTotal, page: curPage, pages: curPages })}
-
- - - -
-
- - ) : null} +

+ {t("ume.alarms.movedHint")}{" "} + { + e.preventDefault(); + openOrFocusModule({ moduleId: "network", path: "/network/alarms" }); + }}> + {t("network.nav.alarms")} + +

diff --git a/web/src/pages/network/NetworkAlarmsPage.tsx b/web/src/pages/network/NetworkAlarmsPage.tsx new file mode 100644 index 0000000..07d0159 --- /dev/null +++ b/web/src/pages/network/NetworkAlarmsPage.tsx @@ -0,0 +1,171 @@ +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { fetchUmeCurrentAlarms } from "../../services/api"; +import { queryKeys } from "../../constants/queryKeys"; +import { useI18n } from "../../i18n"; +import { pageCount } from "../../utils/display"; +import { formatSystemTime } from "../../utils/time"; + +/** Current UME alarms query view for Network Management. */ +export function NetworkAlarmsPage() { + const { t } = useI18n(); + const [curSeverity, setCurSeverity] = useState(""); + const [curCleared, setCurCleared] = useState(""); + const [curHostName, setCurHostName] = useState(""); + const [curKeyword, setCurKeyword] = useState(""); + const [curPage, setCurPage] = useState(1); + const [curPageSize, setCurPageSize] = useState(50); + + const currentQuery = useQuery({ + queryKey: queryKeys.umeCurrentAlarms(curSeverity, curCleared, curHostName, curKeyword, curPage, curPageSize), + queryFn: () => + fetchUmeCurrentAlarms({ + severity: curSeverity, + isCleared: curCleared, + hostName: curHostName, + keyword: curKeyword, + page: curPage, + pageSize: curPageSize, + }), + staleTime: 5000, + }); + + const curTotal = Number(currentQuery.data?.total || 0); + const curPages = pageCount(curTotal, curPageSize); + const perPage = (n: number) => t("common.perPage", { n: String(n) }); + const hasFilters = Boolean(curKeyword.trim() || curHostName.trim() || curSeverity || curCleared); + + return ( +
+
+

{t("ume.alarms.title")}

+
+
+ { + setCurKeyword(e.target.value); + setCurPage(1); + }} + /> + { + setCurHostName(e.target.value); + setCurPage(1); + }} + /> + + + +
+ {currentQuery.isLoading ?

{t("common.refreshing")}

: null} + {currentQuery.isError ?

{t("common.opFailed")}

: null} + + + + + + + + + + + + + {(currentQuery.data?.items || []).map((x) => ( + + + + + + + + + ))} + +
time_createdseverityne_idhost_namene_typecause
{formatSystemTime(x.time_created)}{x.perceived_severity}{x.ne_id} + {(x.host_name || "").trim() ? ( + + ) : ( + + {t("common.empty")} + + )} + {x.ne_type ?? ""}{x.native_probable_cause}
+
+
{t("common.pagerMeta", { total: curTotal, page: curPage, pages: curPages })}
+
+ + + +
+
+
+ ); +} diff --git a/web/src/pages/network/NetworkLayout.tsx b/web/src/pages/network/NetworkLayout.tsx new file mode 100644 index 0000000..216da16 --- /dev/null +++ b/web/src/pages/network/NetworkLayout.tsx @@ -0,0 +1,88 @@ +import { useEffect, useState } from "react"; +import { NavLink, Outlet, useLocation } from "react-router-dom"; +import { NETWORK_NAV, type NetworkNavGroupId } from "../../config/networkNav"; +import { useI18n } from "../../i18n"; + +function groupContainsPath(groupId: NetworkNavGroupId, pathname: string): boolean { + const group = NETWORK_NAV.find((g) => g.id === groupId); + if (!group) return false; + return group.items.some( + (item) => pathname === item.path || pathname.startsWith(`${item.path}/`), + ); +} + +export function NetworkLayout() { + const { t } = useI18n(); + const { pathname } = useLocation(); + const [openGroups, setOpenGroups] = useState>(() => { + const init = {} as Record; + for (const g of NETWORK_NAV) { + // Default collapsed; only the group owning the current route starts open. + init[g.id] = groupContainsPath(g.id, pathname); + } + return init; + }); + + useEffect(() => { + for (const g of NETWORK_NAV) { + if (groupContainsPath(g.id, pathname)) { + setOpenGroups((prev) => (prev[g.id] ? prev : { ...prev, [g.id]: true })); + } + } + }, [pathname]); + + const toggleGroup = (id: NetworkNavGroupId) => { + setOpenGroups((prev) => ({ ...prev, [id]: !prev[id] })); + }; + + return ( +
+ +
+ +
+
+ ); +} diff --git a/web/src/pages/network/NetworkPlaceholderPage.tsx b/web/src/pages/network/NetworkPlaceholderPage.tsx new file mode 100644 index 0000000..f348541 --- /dev/null +++ b/web/src/pages/network/NetworkPlaceholderPage.tsx @@ -0,0 +1,26 @@ +import { useI18n } from "../../i18n"; + +type Props = { + kind: "port-traffic"; +}; + +export function NetworkPlaceholderPage({ kind }: Props) { + const { t } = useI18n(); + const titleKey = kind === "port-traffic" ? "network.placeholder.portTrafficTitle" : "network.placeholder.title"; + const bodyKey = kind === "port-traffic" ? "network.placeholder.portTrafficBody" : "network.placeholder.body"; + + return ( +
+

{t(titleKey)}

+

{t(bodyKey)}

+ {kind === "port-traffic" ? ( +
    +
  • {t("network.placeholder.portTrafficStep1")}
  • +
  • {t("network.placeholder.portTrafficStep2")}
  • +
  • {t("network.placeholder.portTrafficStep3")}
  • +
  • {t("network.placeholder.portTrafficStep4")}
  • +
+ ) : null} +
+ ); +}