Refactor web UI: feature-first structure, tokenized styles, modern blue theme.

Structure
- pages/ -> features/<domain>/{Page, components, hooks, lib}; 95 files moved,
  333 imports rewritten, verified with tsc.
- services/api.ts -> services/api/<domain>.ts (250 exports preserved),
  types.ts -> types/<domain>.ts, i18n zh/en -> locales/<lang>/<namespace>.ts
  (2696 messages deep-equal before/after).
- ESLint rule: shared layers must not import features/.

Styles
- index.css (16k lines) split losslessly into styles/{base,layout,components,
  pages,legacy,theme}; compiled CSS was byte-identical before tokenizing.
- ~930 hard-coded colors replaced by --nx-* tokens (theme/tokens.css); WebCRT
  off-palette colors unified; fixed dark-on-dark text introduced by mapping.
- theme/theme.css: modern blue design language (rounded shapes, gradients,
  unified buttons/fields/tables/modals), form-grid alignment fix, flat nav list
  without tree indent, soft selected state instead of left accent bar.

Docs: WEB.md documents code structure and style rules.
Not touched: the large Biz*/Webcrt/Topology page components (to be split later).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-12 03:17:51 +08:00
parent 3b45d6abe3
commit 0d894efe5e
196 changed files with 26947 additions and 28091 deletions

View file

@ -7,26 +7,68 @@
- TanStack Query(服务端状态)
- 自研 i18n(`src/i18n/`,无额外依赖)
## 目录结构
## 代码结构
**按领域(feature)组织,依赖方向 `features → 共享层`,反向禁止**(ESLint `no-restricted-imports` 强制)。
```
src/
config/modules.ts # 模块注册表(工作台卡片、路由、标题 i18n)
constants/queryKeys.ts # React Query key
components/ # 可复用 UI(无业务 API 调用)
hooks/ # 通用 hooks(Toast、窗口注册)
i18n/ # 文案 zh / en
layout/ # AppLayout、ErrorBoundary
pages/ # 页面(按路由)
services/api.ts # HTTP 封装与 API 函数
types.ts # 与后端契约类型
utils/
tabChannel.ts # 跨标签页聚焦(BroadcastChannel)
workbench.ts # 返回工作台
moduleWindows.ts # 打开/聚焦模块页签
display.ts # 纯展示辅助
main.tsx App.tsx # 入口、路由表(lazy 引入各 feature 页面)
features/<领域>/ # 业务模块,kebab-case 目录名
XxxPage.tsx # 路由入口(XxxLayout.tsx 为带子路由的壳)
components/ # 该领域私有组件(含 modals/)
hooks/ # 该领域私有 hooks(useXxx.ts)
lib/ # 纯逻辑(无 React),可单测
layout/ # 应用壳:AppLayout、顶栏、ErrorBoundary
components/ (+ ui/) # 跨领域复用 UI(无业务 API 调用)
hooks/ utils/ # 跨领域 hooks / 纯工具
services/api/ # 后端 API,按领域分文件,index.ts 汇总导出
types/ # 后端契约类型,按领域分文件,index.ts 汇总导出
i18n/locales/{zh,en}/<ns>.ts # 文案,一个命名空间一个文件
config/ # 导航与模块注册表(modules / networkNav / auditNav)
constants/queryKeys.ts # React Query key
styles/ # 见「样式规范」
```
现有领域:`auth` `workbench` `managed-ne` `ume` `collect` `config-sync` `webcrt` `topology`
`network` `port-traffic` `cutover`(业务状态/对比/割接) `audit` `users` `api-tokens` `sessions`。
约定:
- 只被**一个**领域用到的代码放进该领域的 `components/ hooks/ lib/`;被**两个以上**领域用到才上移到共享层。
- 跨领域只引用对方的页面级导出或 `components/ lib/` 下的文件;现有合理依赖:`* → auth`、`topology/ume → managed-ne`。
- 共享层(`components hooks utils services i18n types constants`)**不得** import `features/`。
- 新增 API:写进 `services/api/<领域>.ts`;新增类型:写进 `types/<领域>.ts`;调用方仍 `from "…/services/api"` / `"…/types"`。
- 单个组件函数超过 ~500 行视为需要拆分:状态/副作用抽成 `hooks/`,纯计算抽成 `lib/`,区块抽成 `components/`。
(`BizComparePage` `WebcrtPage` `BizStatePage` `BizMigrationPage` `TopologyPage` 等仍是待拆分的单体组件。)
## 样式规范
`src/index.css` 只是入口:按顺序 `@import` `styles/` 下的文件,**导入顺序即层叠顺序,不要调换**。
```
styles/
theme/tokens.css # 唯一的色板/圆角/阴影来源(--nx-*),并把旧 --nm-* 映射到它
base/ # 全局基础(滚动条、body)
layout/ # 应用壳、网络/审计壳(左侧导航)
components/ # 共享原语(按钮、输入、表格、卡片、pill…)
pages/<领域>.css # 页面样式,类名带领域前缀(wb-* topo-* webcrt-* bs-* pt-*…)
legacy/ # 历史遗留的覆盖层,逐步并入 components/,不要往里加新规则
theme/theme.css # 设计语言层,必须最后加载
```
设计方向:**现代蓝**——沿用 NetX 原有深海军蓝色板、柔和渐变面板、8/12px 圆角、蓝色强调色(主按钮用渐变),
胶囊状态标签;hover 有轻微抬升和蓝色光晕,focus 用柔和焦点环。
规则:
- 颜色一律用 `var(--nx-*)`;半透明用 `rgb(var(--nx-ink-rgb) / .6)`,不要写 `#0f172a` / `rgba(15,23,42,.6)`。
- 状态色用语义变量 `--nx-ok / --nx-warn / --nx-bad`。
- 次要文字至少用 `--nx-text-faint`(在 `--nx-surface-1` 上对比度 ≥ 4.5:1)。
- 顶栏高度固定 48px(`--nx-header-h`),多个页面用 `calc(100vh - 48px)` 布局。
- 新样式放进所属 `pages/` 或 `components/` 文件;只有跨页面的视觉语言才改 `theme/theme.css`。
- 避免 `!important` 和"覆盖全局 button"式规则;需要时先在 `components/` 里改原语。
## 路由与模块
| 路径 | 页面 | moduleId |
@ -61,7 +103,7 @@ src/
1. 在 `MODULES` 增加一项(`moduleId`、`path`、`section`、i18n key、`iconTone`)
2. 在 `App.tsx` 增加 `<Route path="..." element={...} />`
3. 在 `i18n/zh.ts` 与 `en.ts` 补充文案
3. 在 `i18n/locales/zh/<ns>.ts` 与 `en/<ns>.ts` 补充文案(新命名空间还需在各自 `index.ts` 注册)
4. 工作台卡片由 `modulesInSection` 自动生成,点击走 `openOrFocusModule`
## 跨标签页导航
@ -79,7 +121,7 @@ src/
## 数据请求
- API 仅写在 `services/api.ts`
- API 仅写在 `services/api/<领域>.ts`
- Query key 集中在 `constants/queryKeys.ts`
- 失效缓存用 prefix key(如 `queryKeys.umeSyncStatusAll`)
- 顶栏连接状态:`App` 轮询 `GET /v1/integrations/status`(5s),展示 **netx api** 与 **DSH alarm hub**
@ -184,7 +226,7 @@ src/
## 样式
- 全局样式:`index.css`(工作台浅色主题)
- 全局样式:见「样式规范」(`index.css` 为入口,令牌在 `styles/theme/tokens.css`)
- 不使用 CSS Modules;类名 BEM 风格:`app-brand__logo`、`wb-card`
## 构建

View file

@ -19,4 +19,21 @@ export default defineConfig([
globals: globals.browser,
},
},
{
// Dependency direction: features -> shared, never the reverse (see WEB.md "代码结构").
files: ['src/{components,hooks,utils,services,i18n,types,constants}/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/features/**'],
message: '共享层不得依赖 features/(依赖方向:features → 共享层)。把共用部分下沉到共享层。',
},
],
},
],
},
},
])

View file

@ -1,75 +1,75 @@
import { lazy, Suspense } from "react";
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
import { AppLayout } from "./layout/AppLayout";
import { WorkbenchPage } from "./pages/WorkbenchPage";
import { LoginPage } from "./pages/LoginPage";
import { ForceChangePasswordPage } from "./pages/ForceChangePasswordPage";
import { useAuth } from "./auth/AuthContext";
import { WorkbenchPage } from "./features/workbench/WorkbenchPage";
import { LoginPage } from "./features/auth/LoginPage";
import { ForceChangePasswordPage } from "./features/auth/ForceChangePasswordPage";
import { useAuth } from "./features/auth/AuthContext";
const CollectPage = lazy(() => import("./pages/CollectPage").then((m) => ({ default: m.CollectPage })));
const CollectPage = lazy(() => import("./features/collect/CollectPage").then((m) => ({ default: m.CollectPage })));
const ConfigSyncPage = lazy(() =>
import("./pages/ConfigSyncPage").then((m) => ({ default: m.ConfigSyncPage })),
import("./features/config-sync/ConfigSyncPage").then((m) => ({ default: m.ConfigSyncPage })),
);
const NePage = lazy(() => import("./pages/NePage").then((m) => ({ default: m.NePage })));
const UmePage = lazy(() => import("./pages/UmePage").then((m) => ({ default: m.UmePage })));
const WebcrtPage = lazy(() => import("./pages/WebcrtPage").then((m) => ({ default: m.WebcrtPage })));
const TopologyPage = lazy(() => import("./pages/TopologyPage").then((m) => ({ default: m.TopologyPage })));
const NePage = lazy(() => import("./features/managed-ne/NePage").then((m) => ({ default: m.NePage })));
const UmePage = lazy(() => import("./features/ume/UmePage").then((m) => ({ default: m.UmePage })));
const WebcrtPage = lazy(() => import("./features/webcrt/WebcrtPage").then((m) => ({ default: m.WebcrtPage })));
const TopologyPage = lazy(() => import("./features/topology/TopologyPage").then((m) => ({ default: m.TopologyPage })));
const NetworkLayout = lazy(() =>
import("./pages/network/NetworkLayout").then((m) => ({ default: m.NetworkLayout })),
import("./features/network/NetworkLayout").then((m) => ({ default: m.NetworkLayout })),
);
const NetworkDevicesPage = lazy(() =>
import("./pages/network/NetworkDevicesPage").then((m) => ({ default: m.NetworkDevicesPage })),
import("./features/network/NetworkDevicesPage").then((m) => ({ default: m.NetworkDevicesPage })),
);
const NetworkAlarmsPage = lazy(() =>
import("./pages/network/NetworkAlarmsPage").then((m) => ({ default: m.NetworkAlarmsPage })),
import("./features/network/NetworkAlarmsPage").then((m) => ({ default: m.NetworkAlarmsPage })),
);
const NetworkConfigsPage = lazy(() =>
import("./pages/network/NetworkConfigsPage").then((m) => ({ default: m.NetworkConfigsPage })),
import("./features/network/NetworkConfigsPage").then((m) => ({ default: m.NetworkConfigsPage })),
);
const LldpLinksPage = lazy(() =>
import("./pages/network/LldpLinksPage").then((m) => ({ default: m.LldpLinksPage })),
import("./features/network/LldpLinksPage").then((m) => ({ default: m.LldpLinksPage })),
);
const TopologyClassifyPage = lazy(() =>
import("./pages/network/TopologyClassifyPage").then((m) => ({ default: m.TopologyClassifyPage })),
import("./features/network/TopologyClassifyPage").then((m) => ({ default: m.TopologyClassifyPage })),
);
const PortTrafficPage = lazy(() =>
import("./pages/network/PortTrafficPage").then((m) => ({ default: m.PortTrafficPage })),
import("./features/port-traffic/PortTrafficPage").then((m) => ({ default: m.PortTrafficPage })),
);
const PortTrafficBoardListPage = lazy(() =>
import("./pages/network/PortTrafficBoardListPage").then((m) => ({
import("./features/port-traffic/PortTrafficBoardListPage").then((m) => ({
default: m.PortTrafficBoardListPage,
})),
);
const PortTrafficWallPage = lazy(() =>
import("./pages/network/PortTrafficWallPage").then((m) => ({ default: m.PortTrafficWallPage })),
import("./features/port-traffic/PortTrafficWallPage").then((m) => ({ default: m.PortTrafficWallPage })),
);
const BizStatePage = lazy(() =>
import("./pages/network/BizStatePage").then((m) => ({ default: m.BizStatePage })),
import("./features/cutover/BizStatePage").then((m) => ({ default: m.BizStatePage })),
);
const BizComparePage = lazy(() =>
import("./pages/network/BizComparePage").then((m) => ({ default: m.BizComparePage })),
import("./features/cutover/BizComparePage").then((m) => ({ default: m.BizComparePage })),
);
const BizMigrationPage = lazy(() =>
import("./pages/network/BizMigrationPage").then((m) => ({ default: m.BizMigrationPage })),
import("./features/cutover/BizMigrationPage").then((m) => ({ default: m.BizMigrationPage })),
);
const BizMonitorTemplatesPage = lazy(() =>
import("./pages/network/BizMonitorTemplatesPage").then((m) => ({
import("./features/cutover/BizMonitorTemplatesPage").then((m) => ({
default: m.BizMonitorTemplatesPage,
})),
);
const UsersPage = lazy(() => import("./pages/UsersPage").then((m) => ({ default: m.UsersPage })));
const UsersPage = lazy(() => import("./features/users/UsersPage").then((m) => ({ default: m.UsersPage })));
const AuditLayout = lazy(() =>
import("./pages/audit/AuditLayout").then((m) => ({ default: m.AuditLayout })),
import("./features/audit/AuditLayout").then((m) => ({ default: m.AuditLayout })),
);
const TaskOverviewPage = lazy(() =>
import("./pages/audit/TaskOverviewPage").then((m) => ({ default: m.TaskOverviewPage })),
import("./features/audit/TaskOverviewPage").then((m) => ({ default: m.TaskOverviewPage })),
);
const AuditPage = lazy(() => import("./pages/AuditPage").then((m) => ({ default: m.AuditPage })));
const AuditPage = lazy(() => import("./features/audit/AuditPage").then((m) => ({ default: m.AuditPage })));
const ApiTokensPage = lazy(() =>
import("./pages/ApiTokensPage").then((m) => ({ default: m.ApiTokensPage })),
import("./features/api-tokens/ApiTokensPage").then((m) => ({ default: m.ApiTokensPage })),
);
const SessionsPage = lazy(() =>
import("./pages/SessionsPage").then((m) => ({ default: m.SessionsPage })),
import("./features/sessions/SessionsPage").then((m) => ({ default: m.SessionsPage })),
);
/** Preserve query when redirecting legacy /network/webcrt → /webcrt. */

View file

@ -1,520 +0,0 @@
import { Button, Checkbox, Input, Label, TextField } from "@heroui/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
apiDelete,
apiPatch,
apiPost,
fetchCliMeta,
fetchCliProfiles,
fetchManagedNeMeta,
fetchUmeCliOverride,
fetchUmeNe,
postUmeConnectTest,
} from "../services/api";
import { HopProxyFields, emptyHopProxyFields, type HopProxyFieldsState } from "./HopProxyFields";
import { HelpHint } from "./HelpHint";
import { FieldSelect } from "./ui/FieldSelect";
import { queryKeys } from "../constants/queryKeys";
import { useI18n } from "../i18n";
import { useToast } from "../hooks/useToast";
import type { CliConnectProfileItem, UmeCliOverrideItem, UmeNeItem } from "../types";
import { defaultHopTemplate, isAutoHopTemplate, patchHopVendorChange } from "../utils/hopProxy";
import { formatSystemTime } from "../utils/time";
function connectPillLevel(status: string): "up" | "down" | "unknown" | "warn" {
const s = String(status || "").toLowerCase();
if (s === "pass" || s === "ok") return "up";
if (s === "fail" || s === "error") return "down";
if (s === "testing") return "warn";
return "unknown";
}
function formatUmeNePickLabel(row: UmeNeItem): string {
const name = String(row.user_label || row.ne_name || row.host_name || "").trim();
const ip = String(row.ip_address || "").trim();
const id = String(row.ne_id || "").trim();
const head = [name, ip].filter(Boolean).join(" · ");
return head ? `${head} · ${id}` : id;
}
type ProfileForm = {
id: string;
name: string;
username: string;
password: string;
port: number;
protocol: string;
device_type_default: string;
vendor_default: string;
is_default: boolean;
hop_enabled: boolean;
hop: HopProxyFieldsState;
};
function applyHopTemplate(hop: HopProxyFieldsState, protocol: string, vrf: string, force = false): Partial<HopProxyFieldsState> {
if (!force && !isAutoHopTemplate(hop.hop_command_template, hop.hop_vendor, hop.hop_protocol, hop.hop_vrf)) {
return {};
}
return { hop_command_template: defaultHopTemplate(hop.hop_vendor, protocol, vrf) };
}
const emptyForm = (): ProfileForm => ({
id: "",
name: "default",
username: "",
password: "",
port: 22,
protocol: "ssh",
device_type_default: "zte_zxros",
vendor_default: "ZTE",
is_default: true,
hop_enabled: false,
hop: emptyHopProxyFields(),
});
function profileToForm(row: CliConnectProfileItem): ProfileForm {
return {
id: row.id,
name: row.name,
username: row.username,
password: "",
port: row.port,
protocol: row.protocol,
device_type_default: row.device_type_default,
vendor_default: row.vendor_default,
is_default: row.is_default,
hop_enabled: row.hop_enabled,
hop: {
hop_vendor: (row.hop_vendor || "bastion") as HopProxyFieldsState["hop_vendor"],
hop_host: row.hop_host ?? "",
hop_port: row.hop_port ?? 22,
hop_protocol: row.hop_protocol ?? "ssh",
hop_username: row.hop_username ?? "",
hop_password: "",
hop_command_template: row.hop_command_template ?? "",
hop_vrf: row.hop_vrf ?? "",
hop_target_auth_mode: (row.hop_target_auth_mode || "bastion_managed") as HopProxyFieldsState["hop_target_auth_mode"],
hop_enter_system_view: Boolean(row.hop_enter_system_view),
},
};
}
export function UmeCliConnectPanel({ enabled = true, embedded = false }: { enabled?: boolean; embedded?: boolean }) {
const { t } = useI18n();
const { showOk, showError } = useToast();
const queryClient = useQueryClient();
const [form, setForm] = useState<ProfileForm>(emptyForm);
const [sampleUmeNeId, setSampleUmeNeId] = useState("");
const [sampleFilter, setSampleFilter] = useState("");
const [sampleOpen, setSampleOpen] = useState(false);
const [debouncedFilter, setDebouncedFilter] = useState("");
const [testPending, setTestPending] = useState(false);
const sampleSuggestRef = useRef<HTMLDivElement | null>(null);
const testNeId = sampleUmeNeId.trim();
useEffect(() => {
const timer = window.setTimeout(() => setDebouncedFilter(sampleFilter.trim()), 220);
return () => window.clearTimeout(timer);
}, [sampleFilter]);
useEffect(() => {
setTestPending(false);
}, [testNeId]);
const metaQuery = useQuery({ queryKey: queryKeys.cliMeta, queryFn: fetchCliMeta, enabled });
const neMetaQuery = useQuery({ queryKey: queryKeys.managedNeMeta, queryFn: fetchManagedNeMeta, enabled });
const profilesQuery = useQuery({ queryKey: queryKeys.cliProfiles, queryFn: fetchCliProfiles, enabled });
const sampleNeQuery = useQuery({
queryKey: queryKeys.umeNE(debouncedFilter, 1, 5),
queryFn: () => fetchUmeNe({ keyword: debouncedFilter, page: 1, pageSize: 5 }),
enabled: enabled && sampleOpen,
staleTime: 5000,
});
const sampleHits = useMemo(
() => (sampleNeQuery.data?.items || []).slice(0, 5),
[sampleNeQuery.data],
);
useEffect(() => {
if (!sampleOpen) return;
const el = sampleSuggestRef.current;
if (!el) return;
const timer = window.setTimeout(() => {
const body = el.closest<HTMLElement>('[data-slot="modal-body"], .modal__body');
if (body) {
const elRect = el.getBoundingClientRect();
const bodyRect = body.getBoundingClientRect();
const overflow = elRect.bottom - bodyRect.bottom + 16;
if (overflow > 0) {
body.scrollBy({ top: overflow, behavior: "smooth" });
return;
}
}
el.scrollIntoView({ block: "nearest", behavior: "smooth" });
}, 50);
return () => window.clearTimeout(timer);
}, [sampleOpen, sampleHits.length, sampleNeQuery.isFetching]);
const overrideQuery = useQuery({
queryKey: queryKeys.umeCliOverride(testNeId),
queryFn: () => fetchUmeCliOverride(testNeId),
enabled: Boolean(testNeId),
refetchInterval: (q) => {
const status = String(q.state.data?.connect_status || "").toLowerCase();
return status === "testing" || testPending ? 2000 : false;
},
});
const overrideResult: UmeCliOverrideItem | null = overrideQuery.data ?? null;
const overrideStatus = String(overrideResult?.connect_status || "").toLowerCase();
const overrideTesting = overrideStatus === "testing" || (testPending && !overrideResult?.connect_tested_at);
useEffect(() => {
const items = profilesQuery.data?.items || [];
const current = items.find((x) => x.id === form.id);
const picked = current || items.find((x) => x.is_default) || items[0];
if (picked && (!form.id || !current)) {
setForm(profileToForm(picked));
}
}, [profilesQuery.data, form.id]);
const saveMutation = useMutation({
mutationFn: async () => {
if (form.hop_enabled) {
if (!form.hop.hop_host.trim()) throw new Error(t("managedNe.hop.hostRequired"));
if (!form.hop.hop_username.trim()) throw new Error(t("managedNe.hop.userRequired"));
if (!form.id && !form.hop.hop_password) throw new Error(t("managedNe.hop.passwordRequired"));
}
const body = {
name: form.name.trim(),
username: form.username.trim(),
password: form.password || undefined,
port: form.port,
protocol: form.protocol,
device_type_default: form.device_type_default,
vendor_default: form.vendor_default,
is_default: form.is_default,
hop_enabled: form.hop_enabled,
hop_vendor: form.hop.hop_vendor,
hop_host: form.hop.hop_host,
hop_port: form.hop.hop_port,
hop_protocol: form.hop.hop_protocol,
hop_username: form.hop.hop_username,
hop_password: form.hop_enabled ? form.hop.hop_password || undefined : undefined,
hop_command_template: form.hop.hop_command_template,
hop_vrf: form.hop.hop_vrf,
hop_target_auth_mode: form.hop.hop_target_auth_mode,
hop_enter_system_view: form.hop.hop_enter_system_view,
};
if (form.id) {
return apiPatch<CliConnectProfileItem>(`/v1/cli/profiles/${form.id}`, body);
}
return apiPost<CliConnectProfileItem>("/v1/cli/profiles", { ...body, password: form.password });
},
onSuccess: async (row) => {
setForm(profileToForm(row));
await queryClient.invalidateQueries({ queryKey: queryKeys.cliProfiles });
await queryClient.invalidateQueries({ queryKey: queryKeys.cliMeta });
showOk(t("ume.cli.saved"));
},
onError: (e: Error) => showError(e.message),
});
const connectTestMutation = useMutation({
mutationFn: async () => {
const id = sampleUmeNeId.trim();
if (!id) throw new Error(t("ume.cli.sampleNeRequired"));
return postUmeConnectTest([id]);
},
onSuccess: async () => {
const id = sampleUmeNeId.trim();
setTestPending(true);
showOk(t("ume.cli.connectTestSubmitted"));
if (id) {
await queryClient.invalidateQueries({ queryKey: queryKeys.umeCliOverride(id) });
}
},
onError: (e: Error) => {
setTestPending(false);
showError(e.message);
},
});
useEffect(() => {
if (!testPending) return;
if (!overrideResult) return;
if (overrideStatus === "testing") return;
setTestPending(false);
void queryClient.invalidateQueries({ queryKey: queryKeys.cliTargetsAll });
}, [testPending, overrideResult, overrideStatus, queryClient]);
const deviceTypes = neMetaQuery.data?.device_types ?? [];
const vendors = neMetaQuery.data?.vendors ?? [];
const cliReady = Boolean(metaQuery.data?.cli_profile_ready);
const body = (
<div className={embedded ? "cli-connect-panel" : undefined}>
{!embedded ? (
<div className="panel__toolbar">
<h2>{t("ume.cli.title")}</h2>
<HelpHint text={t("ume.cli.hint")} ariaLabel={t("common.help")} />
</div>
) : null}
<p className="form-field-hint" style={{ marginBottom: 12 }}>
{cliReady ? t("ume.cli.statusReady") : t("ume.cli.statusNotReady")}
</p>
<div className="form-grid">
<TextField fullWidth value={form.name} onChange={(name) => setForm({ ...form, name })}>
<Label>{t("ume.cli.profileName")}</Label>
<Input />
</TextField>
<TextField
fullWidth
isRequired
value={form.username}
onChange={(username) => setForm({ ...form, username })}
>
<Label>{t("ume.cli.targetUsername")}</Label>
<Input placeholder="ca-oper" />
</TextField>
<TextField
fullWidth
type="password"
value={form.password}
onChange={(password) => setForm({ ...form, password })}
>
<Label>{t("ume.cli.targetPassword")}</Label>
<Input placeholder={form.id ? t("ume.cli.passwordKeep") : ""} />
</TextField>
<FieldSelect
label={t("managedNe.form.deviceType")}
value={form.device_type_default}
onChange={(e) => setForm({ ...form, device_type_default: e.target.value })}
>
{deviceTypes.map((dt) => (
<option key={dt} value={dt}>
{dt}
</option>
))}
</FieldSelect>
<FieldSelect
label={t("managedNe.form.vendor")}
value={form.vendor_default}
onChange={(e) => setForm({ ...form, vendor_default: e.target.value })}
>
{vendors.map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</FieldSelect>
</div>
<div className="form-fieldset" style={{ marginTop: 16 }}>
<div className="form-fieldset__title">{t("managedNe.hop.sectionTitle")}</div>
<Checkbox
isSelected={form.hop_enabled}
onChange={(hop_enabled) => {
setForm((prev) => ({
...prev,
hop_enabled,
...(hop_enabled
? {
hop: {
...prev.hop,
...patchHopVendorChange(prev.hop.hop_vendor, prev.hop),
...applyHopTemplate(prev.hop, prev.hop.hop_protocol, prev.hop.hop_vrf, true),
},
}
: {}),
}));
}}
>
<Checkbox.Control>
<Checkbox.Indicator />
</Checkbox.Control>
<Checkbox.Content>{t("managedNe.hop.enable")}</Checkbox.Content>
</Checkbox>
{form.hop_enabled ? (
<div className="hop-proxy-fields">
<HopProxyFields
value={form.hop}
onChange={(patch) => setForm((prev) => ({ ...prev, hop: { ...prev.hop, ...patch } }))}
hopPasswordRequired={!form.id}
hopPasswordOptional={Boolean(form.id)}
/>
</div>
) : null}
</div>
<div className="actions-row actions-row--inline" style={{ marginTop: 16 }}>
<Button
size="sm"
variant="primary"
isDisabled={saveMutation.isPending}
onPress={() => saveMutation.mutate()}
>
{saveMutation.isPending ? t("managedNe.form.saving") : t("managedNe.form.save")}
</Button>
<Button size="sm" variant="secondary" onPress={() => setForm(emptyForm())}>
{t("ume.cli.newProfile")}
</Button>
{form.id ? (
<Button
size="sm"
variant="danger"
onPress={() => {
void (async () => {
if (!window.confirm(t("ume.cli.deleteConfirm"))) return;
try {
await apiDelete(`/v1/cli/profiles/${form.id}`);
setForm(emptyForm());
await queryClient.invalidateQueries({ queryKey: queryKeys.cliProfiles });
showOk(t("ume.cli.deleted"));
} catch (e) {
showError(e instanceof Error ? e.message : String(e));
}
})();
}}
>
{t("managedNe.delete")}
</Button>
) : null}
</div>
<div className={embedded ? undefined : "panel"} style={{ marginTop: 20 }}>
<h3 className={embedded ? "card__section-title" : undefined}>{t("ume.cli.connectTestTitle")}</h3>
<div className="cli-connect-test">
<div className="cli-connect-test-row">
<div className="cli-connect-test-pick">
<Input
value={sampleFilter}
onChange={(e) => {
setSampleFilter(e.target.value);
setSampleUmeNeId("");
setSampleOpen(true);
}}
onFocus={() => setSampleOpen(true)}
onBlur={() => {
window.setTimeout(() => setSampleOpen(false), 160);
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
setSampleOpen(false);
(e.target as HTMLInputElement).blur();
}
if (e.key === "Enter" && sampleHits[0]) {
e.preventDefault();
const row = sampleHits[0];
setSampleUmeNeId(row.ne_id);
setSampleFilter(formatUmeNePickLabel(row));
setSampleOpen(false);
}
}}
placeholder={t("ume.cli.sampleNePh")}
aria-label={t("ume.cli.connectTestTitle")}
aria-autocomplete="list"
aria-expanded={sampleOpen}
/>
</div>
<Button
size="sm"
variant="secondary"
isDisabled={connectTestMutation.isPending || !testNeId}
onPress={() => connectTestMutation.mutate()}
>
{connectTestMutation.isPending || overrideTesting
? t("managedNe.connect.running")
: t("managedNe.connect.run")}
</Button>
</div>
{sampleOpen ? (
<div ref={sampleSuggestRef} className="cli-connect-test-suggest" role="listbox">
{sampleNeQuery.isLoading ? (
<div className="cli-connect-test-suggest__empty">{t("ume.cli.sampleNeLoading")}</div>
) : sampleHits.length === 0 ? (
<div className="cli-connect-test-suggest__empty">{t("ume.cli.sampleNeEmpty")}</div>
) : (
sampleHits.map((row) => {
const title =
String(row.user_label || row.ne_name || row.ne_id).trim() || row.ne_id;
const meta = [row.ip_address, row.ne_type].filter(Boolean).join(" · ");
return (
<button
key={row.ne_id}
type="button"
className={`cli-connect-test-suggest__item${
row.ne_id === sampleUmeNeId ? " is-active" : ""
}`}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
setSampleUmeNeId(row.ne_id);
setSampleFilter(formatUmeNePickLabel(row));
setSampleOpen(false);
}}
>
<span className="cli-connect-test-suggest__name">{title}</span>
<span className="cli-connect-test-suggest__meta">
{meta || row.ne_id}
</span>
</button>
);
})
)}
</div>
) : null}
</div>
{testNeId ? (
<div style={{ marginTop: 12 }}>
<div className="form-label">{t("ume.cli.connectTestResult")}</div>
{overrideQuery.isLoading && !overrideResult ? (
<p className="form-field-hint">{t("ume.cli.connectTestWaiting")}</p>
) : overrideTesting ? (
<p className="form-field-hint">
<span className="inline-spinner" aria-hidden /> {t("ume.cli.connectTestRunning")}
</p>
) : overrideResult ? (
<>
<p style={{ marginTop: 8 }}>
<span className={`conn-pill conn-pill--${connectPillLevel(overrideResult.connect_status)}`}>
{overrideResult.connect_status}
</span>
{overrideResult.connect_message ? (
<span className="connect-detail-summary"> — {overrideResult.connect_message}</span>
) : null}
{overrideResult.connect_tested_at ? (
<span className="muted" style={{ marginLeft: 8 }}>
{formatSystemTime(overrideResult.connect_tested_at, { assumeUtcNaive: true })}
</span>
) : null}
</p>
<pre className="connect-log">
{overrideResult.connect_detail?.trim() ||
overrideResult.connect_message?.trim() ||
t("managedNe.connectDetailEmpty")}
</pre>
</>
) : testPending ? (
<p className="form-field-hint">{t("ume.cli.connectTestWaiting")}</p>
) : (
<p className="form-field-hint">{t("managedNe.connectDetailEmpty")}</p>
)}
</div>
) : null}
</div>
{(profilesQuery.data?.items || []).length > 1 ? (
<div style={{ marginTop: 16 }}>
<span className="form-label">{t("ume.cli.existingProfiles")}</span>
<div className="actions-row actions-row--inline">
{(profilesQuery.data?.items || []).map((p) => (
<Button key={p.id} size="sm" variant="ghost" onPress={() => setForm(profileToForm(p))}>
{p.name}
{p.is_default ? ` (${t("ume.cli.default")})` : ""}
</Button>
))}
</div>
</div>
) : null}
</div>
);
if (embedded) return body;
return <section className="panel">{body}</section>;
}

View file

@ -2,13 +2,13 @@ import { Button, Checkbox, Input, Modal } from "@heroui/react";
import { useMemo, useState, type FormEvent } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAuth } from "../auth/AuthContext";
import { HelpHint } from "../components/HelpHint";
import { AppModalShell } from "../components/ui/AppModalShell";
import { FieldSelect } from "../components/ui/FieldSelect";
import { useI18n } from "../i18n";
import { useToast } from "../hooks/useToast";
import { apiDelete, apiGet, apiPatch, apiPost } from "../services/api";
import { formatSystemTime } from "../utils/time";
import { HelpHint } from "../../components/HelpHint";
import { AppModalShell } from "../../components/ui/AppModalShell";
import { FieldSelect } from "../../components/ui/FieldSelect";
import { useI18n } from "../../i18n";
import { useToast } from "../../hooks/useToast";
import { apiDelete, apiGet, apiPatch, apiPost } from "../../services/api";
import { formatSystemTime } from "../../utils/time";
type TokenRow = {
id: string;

View file

@ -2,9 +2,9 @@ import { Button, Input } from "@heroui/react";
import { Fragment, useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "../auth/AuthContext";
import { useI18n } from "../i18n";
import { apiGet } from "../services/api";
import { formatSystemTime } from "../utils/time";
import { useI18n } from "../../i18n";
import { apiGet } from "../../services/api";
import { formatSystemTime } from "../../utils/time";
type AuditItem = {
id: string;

View file

@ -7,7 +7,7 @@ import {
useState,
type ReactNode,
} from "react";
import { apiGet, apiPost, clearAuthToken } from "../services/api";
import { apiGet, apiPost, clearAuthToken } from "../../services/api";
export type AuthUser = {
id: string;

View file

@ -1,8 +1,8 @@
import { Alert, Button, Card, Form, Input, Label, TextField } from "@heroui/react";
import { useState, type FormEvent } from "react";
import { useAuth } from "../auth/AuthContext";
import { useI18n } from "../i18n";
import { apiPost } from "../services/api";
import { useAuth } from "./AuthContext";
import { useI18n } from "../../i18n";
import { apiPost } from "../../services/api";
import { LoginShell } from "./LoginShell";
export function ForceChangePasswordPage() {

View file

@ -1,8 +1,8 @@
import { Alert, Button, Card, Form, Input, Label, TextField } from "@heroui/react";
import { useState, type FormEvent } from "react";
import { Navigate, useSearchParams } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";
import { useI18n } from "../i18n";
import { useAuth } from "./AuthContext";
import { useI18n } from "../../i18n";
import { LoginShell } from "./LoginShell";
export function LoginPage() {

View file

@ -2,8 +2,8 @@ import { Button, Input, Modal } from "@heroui/react";
import { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import { ListPager } from "../components/ListPager";
import { AppModalShell } from "../components/ui/AppModalShell";
import { ListPager } from "../../components/ListPager";
import { AppModalShell } from "../../components/ui/AppModalShell";
import {
createNeCollection,
deleteCollectionJob,
@ -20,16 +20,16 @@ import {
updateCollectionPolicy,
collectionJobDownloadUrl,
collectionRunDownloadUrl,
} from "../services/api";
import { queryKeys } from "../constants/queryKeys";
import { useDebouncedValue } from "../hooks/useDebouncedValue";
import { useI18n } from "../i18n";
import { useToast } from "../hooks/useToast";
import type { CollectionJobItem, CollectionRunItem, CollectionTargetRef, EligibleNeItem } from "../types";
import { downloadCsv, fetchAllPages } from "../utils/csvExport";
import { pageCount } from "../utils/display";
import { jobChipColor, NmStatusChip, sourceChipColor } from "./network/nmChips";
import { formatSystemTime } from "../utils/time";
} from "../../services/api";
import { queryKeys } from "../../constants/queryKeys";
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
import { useI18n } from "../../i18n";
import { useToast } from "../../hooks/useToast";
import type { CollectionJobItem, CollectionRunItem, CollectionTargetRef, EligibleNeItem } from "../../types";
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
import { pageCount } from "../../utils/display";
import { jobChipColor, NmStatusChip, sourceChipColor } from "../../components/NmChips";
import { formatSystemTime } from "../../utils/time";
const POLL_MS = 2000;
const ELIGIBLE_PAGE_SIZE = 10;

View file

@ -2,13 +2,13 @@ import { Button, Input, Modal } from "@heroui/react";
import { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import { ListPager } from "../components/ListPager";
import { AppModalShell } from "../components/ui/AppModalShell";
import { FieldSelect } from "../components/ui/FieldSelect";
import { queryKeys } from "../constants/queryKeys";
import { useDebouncedValue } from "../hooks/useDebouncedValue";
import { useToast } from "../hooks/useToast";
import { useI18n } from "../i18n";
import { ListPager } from "../../components/ListPager";
import { AppModalShell } from "../../components/ui/AppModalShell";
import { FieldSelect } from "../../components/ui/FieldSelect";
import { queryKeys } from "../../constants/queryKeys";
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
import { useToast } from "../../hooks/useToast";
import { useI18n } from "../../i18n";
import {
createConfigSyncCycle,
fetchCliTargets,
@ -20,12 +20,12 @@ import {
resumeConfigSyncCycle,
stopConfigSyncCycle,
updateConfigSyncPolicy,
} from "../services/api";
import type { CliTargetItem, ConfigSyncCycle, ConfigSyncTargetRef, ConfigSyncTask } from "../types";
import { downloadCsv, fetchAllPages } from "../utils/csvExport";
import { pageCount } from "../utils/display";
import { formatSystemTime } from "../utils/time";
import { jobChipColor, NmStatusChip, sourceChipColor } from "./network/nmChips";
} from "../../services/api";
import type { CliTargetItem, ConfigSyncCycle, ConfigSyncTargetRef, ConfigSyncTask } from "../../types";
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
import { pageCount } from "../../utils/display";
import { formatSystemTime } from "../../utils/time";
import { jobChipColor, NmStatusChip, sourceChipColor } from "../../components/NmChips";
const POLL_MS = 2500;
const TARGET_PAGE_SIZE = 10;

View file

@ -1,8 +1,8 @@
import { Button, Input, Modal } from "@heroui/react";
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { parseTemplateImport, parseIfaceNormText, toOrGroups, fromOrGroups, validateTemplateDraft } from "./compareTemplateModel";
import type { MetricSheet, FieldRule, RowFilter } from "./compareTemplateModel";
import { parseTemplateImport, parseIfaceNormText, toOrGroups, fromOrGroups, validateTemplateDraft } from "./lib/compareTemplateModel";
import type { MetricSheet, FieldRule, RowFilter } from "./lib/compareTemplateModel";
import { ListPager } from "../../components/ListPager";
import { AppModalShell } from "../../components/ui/AppModalShell";
import { FieldSelect } from "../../components/ui/FieldSelect";
@ -35,8 +35,8 @@ import {
formatErr,
} from "../../services/api";
import { formatSystemTime } from "../../utils/time";
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./cutoverDataCache";
import { jobChipColor, NmStatusChip } from "./nmChips";
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./lib/cutoverDataCache";
import { jobChipColor, NmStatusChip } from "../../components/NmChips";
type PageTab = "templates" | "jobs";
type JobDetailTab = "config" | "runs" | "result";

View file

@ -40,8 +40,8 @@ import type { CliTargetItem } from "../../types";
import { writeClipboardText } from "../../utils/clipboard";
import { pageCount } from "../../utils/display";
import { formatSystemTime, localDatetimeInputToUtcIso, utcIsoToLocalDatetimeInput } from "../../utils/time";
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./cutoverDataCache";
import { jobChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./lib/cutoverDataCache";
import { jobChipColor, NmStatusChip, sourceChipColor } from "../../components/NmChips";
type NeSourceFilter = "all" | "managed" | "ume";
type CreatePickSide = "old" | "new";

View file

@ -16,7 +16,7 @@ import {
bizMonitorUpdateTemplate,
formatErr,
} from "../../services/api";
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./cutoverDataCache";
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./lib/cutoverDataCache";
type MetricField = { name: string; display_name?: string };
type MetricSchema = { metric_id: string; fields: MetricField[] };

View file

@ -43,8 +43,8 @@ import type { CliTargetItem } from "../../types";
import { pageCount } from "../../utils/display";
import { writeClipboardText } from "../../utils/clipboard";
import { formatSystemTime } from "../../utils/time";
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./cutoverDataCache";
import { jobChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./lib/cutoverDataCache";
import { jobChipColor, NmStatusChip, sourceChipColor } from "../../components/NmChips";
type TaskRow = {
id: string;

View file

@ -6,8 +6,8 @@ export function prefetchCutoverPages(): void {
if (started) return;
started = true;
// Fire-and-forget dynamic imports (same modules as App.tsx lazy()).
void import("./BizStatePage");
void import("./BizComparePage");
void import("./BizMigrationPage");
void import("./BizMonitorTemplatesPage");
void import("../BizStatePage");
void import("../BizComparePage");
void import("../BizMigrationPage");
void import("../BizMonitorTemplatesPage");
}

View file

@ -18,28 +18,28 @@ import {
downloadManagedNeImportTemplate,
syncUmeManagedNe,
type ManagedNeStats,
} from "../services/api";
import { HelpHint } from "../components/HelpHint";
import { HopProxyFields, emptyHopProxyFields, type HopProxyFieldsState } from "../components/HopProxyFields";
import { AppModalShell } from "../components/ui/AppModalShell";
import { FieldSelect } from "../components/ui/FieldSelect";
import { queryKeys } from "../constants/queryKeys";
import { useI18n } from "../i18n";
import { useToast } from "../hooks/useToast";
} from "../../services/api";
import { HelpHint } from "../../components/HelpHint";
import { HopProxyFields, emptyHopProxyFields, type HopProxyFieldsState } from "./components/HopProxyFields";
import { AppModalShell } from "../../components/ui/AppModalShell";
import { FieldSelect } from "../../components/ui/FieldSelect";
import { queryKeys } from "../../constants/queryKeys";
import { useI18n } from "../../i18n";
import { useToast } from "../../hooks/useToast";
import { useAuth } from "../auth/AuthContext";
import type { ManagedNeItem } from "../types";
import { pageCount } from "../utils/display";
import { formatSystemTime } from "../utils/time";
import { openOrFocusModule } from "../utils/moduleWindows";
import { ManagedNeFormDialog } from "./managedNe/ManagedNeFormDialog";
import { ManagedNeConnectDetailDialog } from "./managedNe/ManagedNeConnectDetailDialog";
import { connectStatusClass } from "./managedNe/connectStatus";
import type { ManagedNeItem } from "../../types";
import { pageCount } from "../../utils/display";
import { formatSystemTime } from "../../utils/time";
import { openOrFocusModule } from "../../utils/moduleWindows";
import { ManagedNeFormDialog } from "./components/ManagedNeFormDialog";
import { ManagedNeConnectDetailDialog } from "./components/ManagedNeConnectDetailDialog";
import { connectStatusClass } from "./lib/connectStatus";
import {
deviceTypeForVendor,
emptyManagedNeForm,
managedSourceKey,
type ManagedNeFormState,
} from "./managedNe/formState";
} from "./lib/formState";
/** Hide UME→managed sync/delete controls until needed again. APIs remain available. */
const SHOW_UME_MANAGED_SYNC = false;

View file

@ -1,6 +1,6 @@
import { Input, Label, TextField } from "@heroui/react";
import { useI18n } from "../i18n";
import { FieldSelect } from "./ui/FieldSelect";
import { useI18n } from "../../../i18n";
import { FieldSelect } from "../../../components/ui/FieldSelect";
import {
HOP_VENDORS,
defaultHopTemplate,
@ -11,7 +11,7 @@ import {
patchHopVendorChange,
type HopTargetAuthMode,
type HopVendor,
} from "../utils/hopProxy";
} from "../lib/hopProxy";
export type HopProxyFieldsState = {
hop_vendor: HopVendor;

View file

@ -1,11 +1,11 @@
import { Alert, Button, Chip, Modal } from "@heroui/react";
import { useMutation } from "@tanstack/react-query";
import { connectTestManagedNe } from "../../services/api";
import { AppModalShell } from "../../components/ui/AppModalShell";
import { useI18n } from "../../i18n";
import { formatSystemTime } from "../../utils/time";
import type { ManagedNeItem } from "../../types";
import { connectPillLevel } from "./connectStatus";
import { connectTestManagedNe } from "../../../services/api";
import { AppModalShell } from "../../../components/ui/AppModalShell";
import { useI18n } from "../../../i18n";
import { formatSystemTime } from "../../../utils/time";
import type { ManagedNeItem } from "../../../types";
import { connectPillLevel } from "../lib/connectStatus";
export type ManagedNeConnectDetailDialogProps = {
row: ManagedNeItem | null;

View file

@ -1,14 +1,14 @@
import { Alert, Button, Checkbox, Input, Label, Modal, TextField } from "@heroui/react";
import { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { createManagedNe, fetchManagedNeMeta, updateManagedNe } from "../../services/api";
import { HopProxyFields } from "../../components/HopProxyFields";
import { AppModalShell } from "../../components/ui/AppModalShell";
import { FieldSelect } from "../../components/ui/FieldSelect";
import { queryKeys } from "../../constants/queryKeys";
import { useI18n } from "../../i18n";
import { patchHopVendorChange } from "../../utils/hopProxy";
import type { ManagedNeItem } from "../../types";
import { createManagedNe, fetchManagedNeMeta, updateManagedNe } from "../../../services/api";
import { HopProxyFields } from "./HopProxyFields";
import { AppModalShell } from "../../../components/ui/AppModalShell";
import { FieldSelect } from "../../../components/ui/FieldSelect";
import { queryKeys } from "../../../constants/queryKeys";
import { useI18n } from "../../../i18n";
import { patchHopVendorChange } from "../lib/hopProxy";
import type { ManagedNeItem } from "../../../types";
import {
applyHopTemplate,
buildManagedNeSaveBody,
@ -16,7 +16,7 @@ import {
formFromManagedNe,
allowsOpenExecPolicy,
type ManagedNeFormState,
} from "./formState";
} from "../lib/formState";
export type ManagedNeFormDialogProps = {
open: boolean;

View file

@ -2,8 +2,8 @@ import {
defaultHopTemplate,
isAutoHopTemplate,
type HopVendor,
} from "../../utils/hopProxy";
import type { ManagedNeItem } from "../../types";
} from "./hopProxy";
import type { ManagedNeItem } from "../../../types";
export type ManagedNeFormState = {
name: string;

View file

@ -38,7 +38,7 @@ import {
jobChipColor,
NmStatusChip,
sourceChipColor,
} from "./nmChips";
} from "../../components/NmChips";
const POLL_MS = 2500;
const TARGET_PAGE_SIZE = 10;

View file

@ -13,7 +13,7 @@ import type { UmeAlarmItem } from "../../types";
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
import { pageCount } from "../../utils/display";
import { formatSystemTime } from "../../utils/time";
import { NmStatusChip, severityChipColor } from "./nmChips";
import { NmStatusChip, severityChipColor } from "../../components/NmChips";
/** Current UME alarms query view for Network Management. */
export function NetworkAlarmsPage() {

View file

@ -20,7 +20,7 @@ import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
import { pageCount } from "../../utils/display";
import { openNewModuleWindow } from "../../utils/moduleWindows";
import { formatSystemTime } from "../../utils/time";
import { NmStatusChip, sourceChipColor } from "./nmChips";
import { NmStatusChip, sourceChipColor } from "../../components/NmChips";
function fmtBytes(n: number): string {
if (!n) return "0 B";

View file

@ -12,7 +12,7 @@ import type { CliTargetItem } from "../../types";
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
import { pageCount } from "../../utils/display";
import { openNewModuleWindow } from "../../utils/moduleWindows";
import { connectChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
import { connectChipColor, NmStatusChip, sourceChipColor } from "../../components/NmChips";
function openWebcrt(row: CliTargetItem) {
const path =

View file

@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react";
import { NavLink, Outlet, useLocation } from "react-router-dom";
import { NETWORK_NAV, type NetworkNavGroupId } from "../../config/networkNav";
import { useI18n } from "../../i18n";
import { prefetchCutoverPages } from "./cutoverPrefetch";
import { prefetchCutoverPages } from "../cutover/lib/cutoverPrefetch";
function groupContainsPath(groupId: NetworkNavGroupId, pathname: string): boolean {
const group = NETWORK_NAV.find((g) => g.id === groupId);

View file

@ -8,7 +8,7 @@ import {
import { queryKeys } from "../../constants/queryKeys";
import { useI18n } from "../../i18n";
import { useToast } from "../../hooks/useToast";
import { openPortTrafficBoardWindow } from "../../utils/portTrafficBoardWindow";
import { openPortTrafficBoardWindow } from "./lib/portTrafficBoardWindow";
import { formatSystemTime } from "../../utils/time";
export function PortTrafficBoardListPage() {

View file

@ -36,7 +36,7 @@ import type {
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
import { pageCount } from "../../utils/display";
import { formatSystemTime } from "../../utils/time";
import { jobChipColor, NmStatusChip } from "./nmChips";
import { jobChipColor, NmStatusChip } from "../../components/NmChips";
const POLL_MS = 5000;
const TARGET_PAGE_SIZE = 10;

View file

@ -16,8 +16,8 @@ import { queryKeys } from "../../constants/queryKeys";
import { useI18n } from "../../i18n";
import { useToast } from "../../hooks/useToast";
import type { PortTrafficBoardPanel, PortTrafficBoardPanelIn } from "../../types";
import { PortTrafficBoardPanelCell } from "./PortTrafficBoardPanelCell";
import type { WallYMode } from "./PortTrafficWall";
import { PortTrafficBoardPanelCell } from "./components/PortTrafficBoardPanelCell";
import type { WallYMode } from "./components/PortTrafficWall";
type BaselineMode = "off" | "shift" | "day" | "week" | "custom";

View file

@ -1,9 +1,9 @@
import { Button } from "@heroui/react";
import { useQuery, keepPreviousData } from "@tanstack/react-query";
import { fetchPortTrafficCompare } from "../../services/api";
import { queryKeys } from "../../constants/queryKeys";
import { useI18n } from "../../i18n";
import type { PortTrafficBoardPanel } from "../../types";
import { fetchPortTrafficCompare } from "../../../services/api";
import { queryKeys } from "../../../constants/queryKeys";
import { useI18n } from "../../../i18n";
import type { PortTrafficBoardPanel } from "../../../types";
import { PortTrafficWall, type WallYMode } from "./PortTrafficWall";
const POLL_MS = 5000;

View file

@ -1,9 +1,9 @@
import { useEffect, useMemo, useRef } from "react";
import uPlot from "uplot";
import "uplot/dist/uPlot.min.css";
import type { PortTrafficSamplePoint, PortTrafficTarget } from "../../types";
import { formatSystemTime, parseApiTime } from "../../utils/time";
import { useI18n } from "../../i18n";
import type { PortTrafficSamplePoint, PortTrafficTarget } from "../../../types";
import { formatSystemTime, parseApiTime } from "../../../utils/time";
import { useI18n } from "../../../i18n";
export type WallYMode = "auto" | "current" | "util";

View file

@ -1,4 +1,4 @@
import { openNewModuleWindow } from "./moduleWindows";
import { openNewModuleWindow } from "../../../utils/moduleWindows";
export const PORT_TRAFFIC_WALL_MODULE_ID = "port-traffic-wall";

View file

@ -1,10 +1,10 @@
import { Button } from "@heroui/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { HelpHint } from "../components/HelpHint";
import { useI18n } from "../i18n";
import { useToast } from "../hooks/useToast";
import { apiDelete, apiGet, apiPost } from "../services/api";
import { formatSystemTime } from "../utils/time";
import { HelpHint } from "../../components/HelpHint";
import { useI18n } from "../../i18n";
import { useToast } from "../../hooks/useToast";
import { apiDelete, apiGet, apiPost } from "../../services/api";
import { formatSystemTime } from "../../utils/time";
type SessionRow = {
id: string;

View file

@ -10,16 +10,16 @@ import {
SHOW_ALARM_OVERLAY_KEY,
SHOW_LEVEL_COLORS_KEY,
WORLD_MAP_ENABLED,
} from "./topology/constants";
import type { CtxMenu, PaletteItem, PaletteSource } from "./topology/pageTypes";
import { clampSidebarWidth, loadSidebarWidth, saveSidebarWidth } from "./topology/sidebarLayout";
} from "./lib/constants";
import type { CtxMenu, PaletteItem, PaletteSource } from "./lib/pageTypes";
import { clampSidebarWidth, loadSidebarWidth, saveSidebarWidth } from "./lib/sidebarLayout";
import {
canvasBgLuminance,
canvasFloatChromeVars,
findViewInRegion,
isWorldFlatViewName,
regionDisplayName,
} from "./topology/treeUtils";
} from "./lib/treeUtils";
import {
loadAutoLayoutAfterDiscover,
loadBoolFlag,
@ -29,7 +29,7 @@ import {
persistBoolFlag,
type LabelColors,
type VendorColors,
} from "./topology/displayPrefs";
} from "./lib/displayPrefs";
import {
BUILTIN_EDGE_DEFAULTS,
type EdgeDefaultStyle,
@ -40,24 +40,24 @@ import {
persistEdgeDefaults,
resolveEdgeStyle,
withEdgeVisual,
} from "./topology/edgeStyle";
import { applyViewGraph, flowToPositions } from "./topology/graphFlow";
import { isLocalPendingEdgeId, newLocalEdgeId } from "./topology/idUtils";
import { fuzzyIncludes, isPlaceholderSource, nodeMatchesQuery } from "./topology/searchUtils";
import { TopologyDiscoverPanel } from "./topology/TopologyDiscoverPanel";
import { TopologyHexBrowser } from "./topology/TopologyHexBrowser";
import { TopologyToolbar } from "./topology/TopologyToolbar";
import { TopologyViewTools } from "./topology/TopologyViewTools";
import { TopologySidebar } from "./topology/TopologySidebar";
import { TopologyModals } from "./topology/TopologyModals";
import { TopologyCtxMenu } from "./topology/TopologyCtxMenu";
import { useTopologyHistory } from "./topology/hooks/useTopologyHistory";
import { useTopologyDiscover } from "./topology/hooks/useTopologyDiscover";
import { useTopologyTreeNav } from "./topology/hooks/useTopologyTreeNav";
import { useTopologyCanvas } from "./topology/hooks/useTopologyCanvas";
import { useTopologyCreateNe } from "./topology/hooks/useTopologyCreateNe";
import { useTopologyConnectTest } from "./topology/hooks/useTopologyConnectTest";
import { useTopologyAlarmOverlay } from "./topology/hooks/useTopologyAlarmOverlay";
} from "./lib/edgeStyle";
import { applyViewGraph, flowToPositions } from "./lib/graphFlow";
import { isLocalPendingEdgeId, newLocalEdgeId } from "./lib/idUtils";
import { fuzzyIncludes, isPlaceholderSource, nodeMatchesQuery } from "./lib/searchUtils";
import { TopologyDiscoverPanel } from "./components/TopologyDiscoverPanel";
import { TopologyHexBrowser } from "./components/TopologyHexBrowser";
import { TopologyToolbar } from "./components/TopologyToolbar";
import { TopologyViewTools } from "./components/TopologyViewTools";
import { TopologySidebar } from "./components/TopologySidebar";
import { TopologyModals } from "./components/TopologyModals";
import { TopologyCtxMenu } from "./components/TopologyCtxMenu";
import { useTopologyHistory } from "./hooks/useTopologyHistory";
import { useTopologyDiscover } from "./hooks/useTopologyDiscover";
import { useTopologyTreeNav } from "./hooks/useTopologyTreeNav";
import { useTopologyCanvas } from "./hooks/useTopologyCanvas";
import { useTopologyCreateNe } from "./hooks/useTopologyCreateNe";
import { useTopologyConnectTest } from "./hooks/useTopologyConnectTest";
import { useTopologyAlarmOverlay } from "./hooks/useTopologyAlarmOverlay";
import {
lazy,
Suspense,
@ -91,33 +91,33 @@ import {
patchTopologyPositions,
removeTopologyViewNodes,
applyUmeTopologyToFabric,
} from "../services/api";
import { queryKeys } from "../constants/queryKeys";
import { useI18n } from "../i18n";
import { useToast } from "../hooks/useToast";
} from "../../services/api";
import { queryKeys } from "../../constants/queryKeys";
import { useI18n } from "../../i18n";
import { useToast } from "../../hooks/useToast";
import { useAuth } from "../auth/AuthContext";
import { formatErr } from "../services/api";
import { openOrFocusModule } from "../utils/moduleWindows";
import { formatErr } from "../../services/api";
import { openOrFocusModule } from "../../utils/moduleWindows";
import type {
FabricNodeSearchHit,
TopologyViewItem,
} from "../types";
import { alignNodes } from "./topology/layoutGraph";
} from "../../types";
import { alignNodes } from "./lib/layoutGraph";
import {
formatPortPairLabel,
isAggregateEdgeId,
physicalIdsForDisplayEdge,
} from "./topology/linkDisplay";
import { behaviorForMode, toolModeFromKey, type ToolMode } from "./topology/toolMode";
import type { NeNodeData } from "./topology/TopologyReactFlowView";
} from "./lib/linkDisplay";
import { behaviorForMode, toolModeFromKey, type ToolMode } from "./lib/toolMode";
import type { NeNodeData } from "./components/TopologyReactFlowView";
import {
buildExportGraph,
exportTopologySvg,
exportTopologyXml,
} from "./topology/exportTopology";
} from "./lib/exportTopology";
/** xyflow + canvas nodes — only loaded when a map canvas is open. */
const TopologyReactFlowView = lazy(() => import("./topology/TopologyReactFlowView"));
const TopologyReactFlowView = lazy(() => import("./components/TopologyReactFlowView"));
export function TopologyPage() {
const { t, locale } = useI18n();

View file

@ -1 +1 @@
export { AppModalShell, TopoModalShell, type AppModalShellProps } from "./AppModalShell";
export { AppModalShell, TopoModalShell, type AppModalShellProps } from "../../../components/ui/AppModalShell";

View file

@ -2,11 +2,11 @@ import type { RefObject } from "react";
import { createPortal } from "react-dom";
import { Button } from "@heroui/react";
import type { Edge, Node } from "@xyflow/react";
import { useI18n } from "../../i18n";
import type { CtxMenu } from "./pageTypes";
import type { EdgeStyleData } from "./edgeStyle";
import { formatPortPairLabel } from "./linkDisplay";
import { isPlaceholderSource } from "./searchUtils";
import { useI18n } from "../../../i18n";
import type { CtxMenu } from "../lib/pageTypes";
import type { EdgeStyleData } from "../lib/edgeStyle";
import { formatPortPairLabel } from "../lib/linkDisplay";
import { isPlaceholderSource } from "../lib/searchUtils";
import type { NeNodeData } from "./TopologyReactFlowView";
export type ResolvedEdgeStyle = {

View file

@ -1,7 +1,7 @@
import { Button } from "@heroui/react";
import type { TopologyDiscoverOut } from "../../types";
import { useI18n } from "../../i18n";
import { openOrFocusModule } from "../../utils/moduleWindows";
import type { TopologyDiscoverOut } from "../../../types";
import { useI18n } from "../../../i18n";
import { openOrFocusModule } from "../../../utils/moduleWindows";
export type DiscoverProgress = {
index: number;

View file

@ -1,14 +1,14 @@
import { Button } from "@heroui/react";
import { Fragment } from "react";
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../types";
import { useI18n } from "../../i18n";
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../../types";
import { useI18n } from "../../../i18n";
import {
displayViewName,
folderNeCount,
formatNeCount,
isUmeWorldContainer,
regionDisplayName,
} from "./treeUtils";
} from "../lib/treeUtils";
import { LayerGlyph, RegionGlyph } from "./topologyIcons";
export type UmeWorldHexModules = {

View file

@ -1,6 +1,6 @@
import type { DragEvent } from "react";
import type { TopologyOutsidePeer } from "../../types";
import type { PaletteItem, PaletteSource } from "./pageTypes";
import type { TopologyOutsidePeer } from "../../../types";
import type { PaletteItem, PaletteSource } from "../lib/pageTypes";
import { AddNePaletteDialog } from "./modals/AddNePaletteDialog";
import { CreateNeModeDialog } from "./modals/CreateNeModeDialog";
import { NewRootDialog } from "./modals/NewRootDialog";
@ -9,10 +9,10 @@ import {
type PlaceholderCreateDialogState,
} from "./modals/PlaceholderCreateDialog";
import { OutsidePeersDialog } from "./modals/OutsidePeersDialog";
import { ManagedNeFormDialog } from "../managedNe/ManagedNeFormDialog";
import { ManagedNeConnectDetailDialog } from "../managedNe/ManagedNeConnectDetailDialog";
import type { ManagedNeItem } from "../../types";
import type { ManagedNeFormState } from "../managedNe/formState";
import { ManagedNeFormDialog } from "../../managed-ne/components/ManagedNeFormDialog";
import { ManagedNeConnectDetailDialog } from "../../managed-ne/components/ManagedNeConnectDetailDialog";
import type { ManagedNeItem } from "../../../types";
import type { ManagedNeFormState } from "../../managed-ne/lib/formState";
export type { PlaceholderCreateDialogState as CreateNeDialogState };

View file

@ -24,13 +24,13 @@ import {
import "@xyflow/react/dist/style.css";
import { WorldScatterLayer, type WorldScatterPoint } from "./WorldScatterLayer";
import { ParallelEdge } from "./ParallelEdge";
import { isAggregateEdgeId } from "./linkDisplay";
import type { ToolMode, ToolModeBehavior } from "./toolMode";
import type { TopologyViewRole, TopologyWorldTransform } from "../../types";
import { isPlaceholderSource } from "./searchUtils";
import { nodeIconTone } from "./vendorTone";
import { levelToneKey } from "./levelTone";
import type { NodeAlarmSummary } from "./alarmOverlay";
import { isAggregateEdgeId } from "../lib/linkDisplay";
import type { ToolMode, ToolModeBehavior } from "../lib/toolMode";
import type { TopologyViewRole, TopologyWorldTransform } from "../../../types";
import { isPlaceholderSource } from "../lib/searchUtils";
import { nodeIconTone } from "../lib/vendorTone";
import { levelToneKey } from "../lib/levelTone";
import type { NodeAlarmSummary } from "../lib/alarmOverlay";
export { isPlaceholderSource };

View file

@ -1,9 +1,9 @@
import type { MouseEvent as ReactMouseEvent, RefObject } from "react";
import { Button, Input } from "@heroui/react";
import type { FabricNodeSearchHit, TopologyTreeFolderItem } from "../../types";
import { useI18n } from "../../i18n";
import { WORLD_MAP_ENABLED } from "./constants";
import { isWorldFlatViewName } from "./treeUtils";
import type { FabricNodeSearchHit, TopologyTreeFolderItem } from "../../../types";
import { useI18n } from "../../../i18n";
import { WORLD_MAP_ENABLED } from "../lib/constants";
import { isWorldFlatViewName } from "../lib/treeUtils";
import { PlusIcon, SidebarFoldIcon } from "./topologyIcons";
import {
TopologyTreeNavFolder,

View file

@ -1,12 +1,12 @@
import { Button, Dropdown } from "@heroui/react";
import { Fragment, type Key, type RefObject, useMemo } from "react";
import type { Edge, Node, ReactFlowInstance } from "@xyflow/react";
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../types";
import { HelpHint } from "../../components/HelpHint";
import { useI18n } from "../../i18n";
import { displayViewName, isRegionCanvasFolder, regionDisplayName } from "./treeUtils";
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../../types";
import { HelpHint } from "../../../components/HelpHint";
import { useI18n } from "../../../i18n";
import { displayViewName, isRegionCanvasFolder, regionDisplayName } from "../lib/treeUtils";
import type { NeNodeData } from "./TopologyReactFlowView";
import type { ToolMode } from "./toolMode";
import type { ToolMode } from "../lib/toolMode";
export type TopologyToolbarProps = {
readOnly?: boolean;

View file

@ -1,6 +1,6 @@
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../types";
import { useI18n } from "../../i18n";
import { WORLD_MAP_ENABLED } from "./constants";
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../../types";
import { useI18n } from "../../../i18n";
import { WORLD_MAP_ENABLED } from "../lib/constants";
import {
displayViewName,
folderNeCount,
@ -13,7 +13,7 @@ import {
isWorldDrillFolder,
isWorldFlatViewName,
regionDisplayName,
} from "./treeUtils";
} from "../lib/treeUtils";
import {
ChevronIcon,
CloseIcon,

View file

@ -3,10 +3,10 @@ import { useQueryClient } from "@tanstack/react-query";
import type { RefObject } from "react";
import type { Node } from "@xyflow/react";
import { createPortal } from "react-dom";
import { queryKeys } from "../../constants/queryKeys";
import { useI18n } from "../../i18n";
import { useToast } from "../../hooks/useToast";
import { updateLldpCollectPolicy } from "../../services/api";
import { queryKeys } from "../../../constants/queryKeys";
import { useI18n } from "../../../i18n";
import { useToast } from "../../../hooks/useToast";
import { updateLldpCollectPolicy } from "../../../services/api";
import {
DEFAULT_CANVAS_BG,
DISCOVER_AUTO_ADD_KEY,
@ -17,7 +17,7 @@ import {
SHOW_CONNECT_STATUS_KEY,
SHOW_ALARM_OVERLAY_KEY,
SHOW_LEVEL_COLORS_KEY,
} from "./constants";
} from "../lib/constants";
import {
DEFAULT_LABEL_COLORS,
DEFAULT_VENDOR_COLORS,
@ -33,9 +33,9 @@ import {
persistVendorColors,
type LabelColors,
type VendorColors,
} from "./displayPrefs";
import type { EdgeDefaultStyle, EdgeDefaults, EdgeLineStyle, EdgeSourceKind } from "./edgeStyle";
import type { alignNodes } from "./layoutGraph";
} from "../lib/displayPrefs";
import type { EdgeDefaultStyle, EdgeDefaults, EdgeLineStyle, EdgeSourceKind } from "../lib/edgeStyle";
import type { alignNodes } from "../lib/layoutGraph";
import type { NeNodeData } from "./TopologyReactFlowView";
export type AlignKind = Parameters<typeof alignNodes>[2];

View file

@ -1,8 +1,8 @@
import type { DragEvent } from "react";
import { Button, Chip, Input, Modal } from "@heroui/react";
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
import { useI18n } from "../../../i18n";
import type { PaletteItem, PaletteSource } from "../pageTypes";
import { TopoModalShell } from "../TopoModalShell";
import { useI18n } from "../../../../i18n";
import type { PaletteItem, PaletteSource } from "../../lib/pageTypes";
export type AddNePaletteDialogProps = {
open: boolean;

View file

@ -1,6 +1,6 @@
import { Button, Modal } from "@heroui/react";
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
import { useI18n } from "../../../i18n";
import { TopoModalShell } from "../TopoModalShell";
import { useI18n } from "../../../../i18n";
export type CreateNeModeDialogProps = {
open: boolean;

View file

@ -1,6 +1,6 @@
import { Button, Input, Label, Modal, TextField } from "@heroui/react";
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
import { useI18n } from "../../../i18n";
import { TopoModalShell } from "../TopoModalShell";
import { useI18n } from "../../../../i18n";
export type NewRootDialogProps = {
dialog: { name: string } | null;

View file

@ -1,7 +1,7 @@
import { Button, Chip, Input, Modal } from "@heroui/react";
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
import type { TopologyOutsidePeer } from "../../../types";
import { useI18n } from "../../../i18n";
import { TopoModalShell } from "../TopoModalShell";
import type { TopologyOutsidePeer } from "../../../../types";
import { useI18n } from "../../../../i18n";
export type OutsidePeersDialogProps = {
open: boolean;

View file

@ -1,6 +1,6 @@
import { Button, Input, Label, Modal, TextField } from "@heroui/react";
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
import { useI18n } from "../../../i18n";
import { TopoModalShell } from "../TopoModalShell";
import { useI18n } from "../../../../i18n";
export type PlaceholderCreateDialogState = {
flowX: number;

View file

@ -12,8 +12,8 @@ import {
normalizeHostKey,
worseSeverity,
type NodeAlarmSummary,
} from "../alarmOverlay";
import type { NeNodeData } from "../TopologyReactFlowView";
} from "../lib/alarmOverlay";
import type { NeNodeData } from "../components/TopologyReactFlowView";
function accumulate(
map: Map<string, NodeAlarmSummary>,

View file

@ -9,25 +9,25 @@ import {
WORLD_LOCATE_HALF,
WORLD_LOCATE_ZOOM,
WORLD_MAP_ENABLED,
} from "../constants";
import type { EdgeDefaults, EdgeStyleData } from "../edgeStyle";
import { withEdgeVisual } from "../edgeStyle";
import { graphFingerprint, graphToFlow, mergeOverlayIntoNodes, overlayFingerprint } from "../graphFlow";
} from "../lib/constants";
import type { EdgeDefaults, EdgeStyleData } from "../lib/edgeStyle";
import { withEdgeVisual } from "../lib/edgeStyle";
import { graphFingerprint, graphToFlow, mergeOverlayIntoNodes, overlayFingerprint } from "../lib/graphFlow";
import {
buildLinkDisplayEdges,
type LinkMember,
} from "../linkDisplay";
import type { NeNodeData } from "../TopologyReactFlowView";
import { TOPO_HANDLE_X, TOPO_HANDLE_Y } from "../topoGeometry";
import type { ToolMode } from "../toolMode";
} from "../lib/linkDisplay";
import type { NeNodeData } from "../components/TopologyReactFlowView";
import { TOPO_HANDLE_X, TOPO_HANDLE_Y } from "../lib/topoGeometry";
import type { ToolMode } from "../lib/toolMode";
import {
displayViewName,
isWorldFlatViewName,
worldDisplayBounds,
worldVisualLodFromZoom,
} from "../treeUtils";
import { mergeFlatWorldGraph } from "../worldGraph";
import type { HistorySnap } from "../pageTypes";
} from "../lib/treeUtils";
import { mergeFlatWorldGraph } from "../lib/worldGraph";
import type { HistorySnap } from "../lib/pageTypes";
export type UseTopologyCanvasOptions = {
mapId: string;
@ -300,7 +300,7 @@ export function useTopologyCanvas(opts: UseTopologyCanvasOptions) {
useEffect(() => {
if (!canvasMode) return;
void import("../TopologyReactFlowView");
void import("../components/TopologyReactFlowView");
}, [canvasMode]);
useEffect(() => {

View file

@ -6,7 +6,7 @@ import { queryKeys } from "../../../constants/queryKeys";
import { useI18n } from "../../../i18n";
import { useToast } from "../../../hooks/useToast";
import type { ManagedNeItem } from "../../../types";
import type { NeNodeData } from "../TopologyReactFlowView";
import type { NeNodeData } from "../components/TopologyReactFlowView";
type Args = {
closeCtxMenu: () => void;

View file

@ -18,12 +18,12 @@ import { queryKeys } from "../../../constants/queryKeys";
import { useI18n } from "../../../i18n";
import { useToast } from "../../../hooks/useToast";
import type { ManagedNeItem } from "../../../types";
import { applyViewGraph } from "../graphFlow";
import type { EdgeDefaults } from "../edgeStyle";
import type { PlaceholderCreateDialogState } from "../modals/PlaceholderCreateDialog";
import type { ManagedNeFormState } from "../../managedNe/formState";
import { isWorldFlatViewName } from "../treeUtils";
import type { NeNodeData } from "../TopologyReactFlowView";
import { applyViewGraph } from "../lib/graphFlow";
import type { EdgeDefaults } from "../lib/edgeStyle";
import type { PlaceholderCreateDialogState } from "../components/modals/PlaceholderCreateDialog";
import type { ManagedNeFormState } from "../../managed-ne/lib/formState";
import { isWorldFlatViewName } from "../lib/treeUtils";
import type { NeNodeData } from "../components/TopologyReactFlowView";
type FocusNode = (fabricNodeId: string, fit: boolean) => void;

View file

@ -21,12 +21,12 @@ import type {
TopologyDiscoverOut,
TopologyViewGraph,
} from "../../../types";
import type { DiscoverProgress, DiscoverSummary } from "../TopologyDiscoverPanel";
import type { EdgeDefaults, EdgeStyleData } from "../edgeStyle";
import { flowToPositions, graphToFlow } from "../graphFlow";
import { discoverResultKind, isLocalPendingEdgeId } from "../idUtils";
import { layoutGraph } from "../layoutGraph";
import type { NeNodeData } from "../TopologyReactFlowView";
import type { DiscoverProgress, DiscoverSummary } from "../components/TopologyDiscoverPanel";
import type { EdgeDefaults, EdgeStyleData } from "../lib/edgeStyle";
import { flowToPositions, graphToFlow } from "../lib/graphFlow";
import { discoverResultKind, isLocalPendingEdgeId } from "../lib/idUtils";
import { layoutGraph } from "../lib/layoutGraph";
import type { NeNodeData } from "../components/TopologyReactFlowView";
export type UseTopologyDiscoverOptions = {
mapId: string;

View file

@ -1,8 +1,8 @@
import { useCallback, useRef, useState } from "react";
import type { Edge, Node } from "@xyflow/react";
import { UNDO_MAX } from "../constants";
import type { HistorySnap } from "../pageTypes";
import type { NeNodeData } from "../TopologyReactFlowView";
import { UNDO_MAX } from "../lib/constants";
import type { HistorySnap } from "../lib/pageTypes";
import type { NeNodeData } from "../components/TopologyReactFlowView";
export function useTopologyHistory(
nodes: Node<NeNodeData>[],

View file

@ -14,8 +14,8 @@ import {
updateTopologyMap,
} from "../../../services/api";
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../../types";
import { LAST_LEAF_KEY, TREE_EXPAND_KEY, WORLD_MAP_ENABLED } from "../constants";
import type { CtxMenu } from "../pageTypes";
import { LAST_LEAF_KEY, TREE_EXPAND_KEY, WORLD_MAP_ENABLED } from "../lib/constants";
import type { CtxMenu } from "../lib/pageTypes";
import {
findFolderInTree,
findViewInRegion,
@ -26,8 +26,8 @@ import {
isWorldDrillFolder,
isWorldFlatViewName,
regionDisplayName,
} from "../treeUtils";
import type { NeNodeData } from "../TopologyReactFlowView";
} from "../lib/treeUtils";
import type { NeNodeData } from "../components/TopologyReactFlowView";
export type UseTopologyTreeNavOptions = {
liveSync: boolean;

View file

@ -1,7 +1,7 @@
import { getStraightPath } from "@xyflow/react";
import type { Edge, Node } from "@xyflow/react";
import type { TopologyViewGraph, TopologyViewItem, TopologyViewNodeItem } from "../../types";
import type { NeNodeData } from "./TopologyReactFlowView";
import type { TopologyViewGraph, TopologyViewItem, TopologyViewNodeItem } from "../../../types";
import type { NeNodeData } from "../components/TopologyReactFlowView";
import { isAggregateEdgeId, type LinkEdgeData } from "./linkDisplay";
import {
TOPO_HANDLE_X,

View file

@ -1,6 +1,6 @@
import type { Edge, Node, Position } from "@xyflow/react";
import type { TopologyViewEdgeItem, TopologyViewGraph, TopologyViewNodeItem } from "../../types";
import type { NeNodeData } from "./TopologyReactFlowView";
import type { TopologyViewEdgeItem, TopologyViewGraph, TopologyViewNodeItem } from "../../../types";
import type { NeNodeData } from "../components/TopologyReactFlowView";
import { formatPortPairLabel } from "./linkDisplay";
import type { EdgeDefaults, EdgeStyleData } from "./edgeStyle";
import { withEdgeVisual } from "./edgeStyle";

View file

@ -1,4 +1,4 @@
import type { TopologyDiscoverNeResult } from "../../types";
import type { TopologyDiscoverNeResult } from "../../../types";
export function newId(): string {
if (typeof crypto !== "undefined" && crypto.randomUUID) {

View file

@ -1,5 +1,5 @@
import type { Edge, Node } from "@xyflow/react";
import type { NeNodeData } from "./TopologyReactFlowView";
import type { NeNodeData } from "../components/TopologyReactFlowView";
export type HistorySnap = {
nodes: Node<NeNodeData>[];

View file

@ -1,5 +1,5 @@
import type { Node } from "@xyflow/react";
import type { NeNodeData } from "./TopologyReactFlowView";
import type { NeNodeData } from "../components/TopologyReactFlowView";
export function normalizeSearchText(s: string): string {
return String(s || "")

View file

@ -1,5 +1,5 @@
import type { CSSProperties } from "react";
import type { TopologyTreeFolderItem, TopologyTreeViewItem, TopologyWorldTransform } from "../../types";
import type { TopologyTreeFolderItem, TopologyTreeViewItem, TopologyWorldTransform } from "../../../types";
import { WORLD_MAP_ENABLED } from "./constants";
export { WORLD_MAP_ENABLED };

View file

@ -1,4 +1,4 @@
import type { NeNodeData } from "./TopologyReactFlowView";
import type { NeNodeData } from "../components/TopologyReactFlowView";
import { VENDOR_TONE_KEYS, type VendorColors, type VendorToneKey } from "./displayPrefs";
export type { VendorToneKey, VendorColors };

View file

@ -2,7 +2,7 @@ import type {
TopologyViewEdgeItem,
TopologyViewGraph,
TopologyViewNodeItem,
} from "../../types";
} from "../../../types";
import { WORLD_FLAT_ACCUM_CAP } from "./constants";
/** Merge newly fetched flat LOD into the existing cache so visited areas stay. */

View file

@ -3,10 +3,10 @@ import { useMemo, useState, type FormEvent } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Navigate } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";
import { FieldSelect } from "../components/ui/FieldSelect";
import { useI18n } from "../i18n";
import { useToast } from "../hooks/useToast";
import { apiGet, apiPatch, apiPost } from "../services/api";
import { FieldSelect } from "../../components/ui/FieldSelect";
import { useI18n } from "../../i18n";
import { useToast } from "../../hooks/useToast";
import { apiGet, apiPatch, apiPost } from "../../services/api";
type UserRow = {
id: string;

View file

@ -13,10 +13,10 @@ import {
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import { Button, Input, Modal } from "@heroui/react";
import type { WebTerminalHandle } from "../components/WebTerminal";
import { AppModalShell } from "../components/ui/AppModalShell";
import { useI18n } from "../i18n";
import { useToast } from "../hooks/useToast";
import type { WebTerminalHandle } from "./components/WebTerminal";
import { AppModalShell } from "../../components/ui/AppModalShell";
import { useI18n } from "../../i18n";
import { useToast } from "../../hooks/useToast";
import {
ApiRequestError,
closeWebcrtSession,
@ -35,10 +35,10 @@ import {
webcrtSftpRename,
webcrtSftpUpload,
type WebcrtSftpItem,
} from "../services/api";
import { pageCount } from "../utils/display";
import { writeClipboardText } from "../utils/clipboard";
import type { CliTargetItem, ManagedNeItem } from "../types";
} from "../../services/api";
import { pageCount } from "../../utils/display";
import { writeClipboardText } from "../../utils/clipboard";
import type { CliTargetItem, ManagedNeItem } from "../../types";
import {
defaultKeywordHighlightConfig,
loadKeywordHighlightConfig,
@ -46,11 +46,11 @@ import {
saveKeywordHighlightConfig,
type KeywordHighlightConfig,
type KeywordRule,
} from "../utils/webcrtKeywordHighlight";
} from "./lib/webcrtKeywordHighlight";
/** xterm is large — only pull it when a session pane actually mounts. */
const WebTerminal = lazy(() =>
import("../components/WebTerminal").then((m) => ({ default: m.WebTerminal })),
import("./components/WebTerminal").then((m) => ({ default: m.WebTerminal })),
);
const PAGE_SIZE = 10;
const SESSION_OPTS_KEY = "netx.webcrt.sessionOptions";

View file

@ -3,13 +3,13 @@ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "re
import { FitAddon } from "@xterm/addon-fit";
import { Terminal } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
import { useI18n } from "../i18n";
import { webcrtWsUrl } from "../services/api";
import { useI18n } from "../../../i18n";
import { webcrtWsUrl } from "../../../services/api";
import {
applyKeywordHighlight,
type KeywordHighlightConfig,
} from "../utils/webcrtKeywordHighlight";
import { readClipboardText, writeClipboardText } from "../utils/clipboard";
} from "../lib/webcrtKeywordHighlight";
import { readClipboardText, writeClipboardText } from "../../../utils/clipboard";
export type WebTerminalHandle = {
clear: () => void;

View file

@ -1,15 +1,15 @@
import { useMemo, type ReactNode } from "react";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@heroui/react";
import { useI18n } from "../i18n";
import { WorkbenchCardIcon } from "../components/WorkbenchCardIcon";
import { modulesInSection, type ModuleDefinition, type WorkbenchSection } from "../config/modules";
import { openOrFocusModule } from "../utils/moduleWindows";
import { useI18n } from "../../i18n";
import { WorkbenchCardIcon } from "./components/WorkbenchCardIcon";
import { modulesInSection, type ModuleDefinition, type WorkbenchSection } from "../../config/modules";
import { openOrFocusModule } from "../../utils/moduleWindows";
import { useAuth } from "../auth/AuthContext";
import { fetchFabricSummary, fetchManagedNeStats, fetchOpsTasks, fetchRuntimeMetrics, fetchTopologyTree, type OpsTaskItem } from "../services/api";
import { formatSystemTime } from "../utils/time";
import { isUmeWorldContainer, isWorldDrillFolder } from "./topology/treeUtils";
import type { TopologyTreeFolderItem } from "../types";
import { fetchFabricSummary, fetchManagedNeStats, fetchOpsTasks, fetchRuntimeMetrics, fetchTopologyTree, type OpsTaskItem } from "../../services/api";
import { formatSystemTime } from "../../utils/time";
import { isUmeWorldContainer, isWorldDrillFolder } from "../topology/lib/treeUtils";
import type { TopologyTreeFolderItem } from "../../types";
const SECTIONS: WorkbenchSection[] = ["monitoring", "operations", "system"];

View file

@ -1,4 +1,4 @@
import type { ModuleIconKind, ModuleIconTone } from "../config/modules";
import type { ModuleIconKind, ModuleIconTone } from "../../../config/modules";
type Props = {
tone: ModuleIconTone;

View file

@ -1,6 +1,6 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
import en from "./en";
import zh from "./zh";
import en from "./locales/en";
import zh from "./locales/zh";
export type Locale = "zh" | "en";

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
const audit = {
title: "Audit & ops",
logsTitle: "Operation logs",
colSummary: "Summary",
colDetail: "Detail",
showDetail: "Expand",
hideDetail: "Collapse",
filterBusiness: "Business",
filterAll: "All",
filterWebcrt: "Device terminal",
filterNeExec: "Device exec",
filterAuth: "Auth",
filterHttp: "HTTP",
summary: {
connecting: "Connecting to {{device}}",
loginOk: "Logged in to {{device}}",
loginFail: "Login failed {{device}}: {{error}}",
logout: "Disconnected {{device}}",
logoutReason: "Disconnected {{device}} ({{reason}})",
command: "{{device}} · cmd: {{command}}",
neExec: "{{device}} · exec: {{commands}}",
neExecBatch: "Batch exec {{n}} NE(s) (ok {{ok}} / fail {{fail}})",
},
nav: {
tasks: "Task overview",
logs: "Operation logs",
},
tasks: {
title: "Task overview",
hint: "Live view of running or pending background work. Actor prefers recent audit entries; scheduled work shows as scheduler/system.",
empty: "No tasks to show right now.",
open: "Open",
inflight: "running {{n}}",
kpiActive: "Active / in-flight",
kpiTotal: "Total rows",
kpiKinds: "By kind",
kpiStatuses: "By status",
colKind: "Kind",
colTitle: "Task",
colStatus: "Status",
colActor: "Actor",
colTrigger: "Trigger",
colProgress: "Progress",
colStarted: "Started",
colUpdated: "Updated",
colLink: "Open",
kind: {
port_traffic: "Port traffic",
config_sync: "Config sync",
ne_collect: "Batch collect",
lldp_discover: "LLDP discover",
ume_sync: "UME sync",
ne_connect: "Connect test",
ume_runtime: "UME runtime",
webcrt: "WebCRT",
},
trigger: {
manual: "Manual",
schedule: "Schedule",
topology: "Topology",
system: "System",
retry_failed: "Retry failed",
},
status: {
collecting: "Collecting",
running: "Running",
testing: "Testing",
connecting: "Logging in",
ready: "Connected",
detached: "Detached (grace)",
pending: "Pending",
paused: "Paused",
idle: "Idle",
error: "Error",
failed: "Failed",
},
},
};
export default audit;

View file

@ -0,0 +1,112 @@
const auth = {
loginTitle: "Sign in",
username: "Username",
password: "Password",
login: "Sign in",
loggingIn: "Signing in…",
loginFailed: "Login failed",
logout: "Sign out",
sessionsTitle: "Login sessions",
sessionsHelp:
"Behind a proxy, IP is the real client; use Client to tell browsers apart. Revoked sessions must sign in again.",
sessionsHint:
"Manage browser/device logins for this account. Behind a reverse proxy, IP is taken from trusted forwarded headers; use the Client column to tell browsers apart on the same public IP. Revoked sessions must sign in again.",
sessionsEmpty: "No active sessions.",
revokeOtherSessions: "Revoke other sessions",
revokeSession: "Revoke",
sessionCurrent: "Current",
sessionRevoked: "Session revoked",
sessionsRevokedOthers: "Revoked {{count}} other session(s)",
revokeCurrentConfirm: "This is your current session; revoking it requires signing in again. Continue?",
colSession: "Session",
colClient: "Client",
colLastSeen: "Last seen",
colCreated: "Created",
usersTitle: "User management",
usersHint: "Only admins can create and manage local accounts.",
addUser: "Add user",
role: "Role",
roleAdmin: "Admin",
roleUser: "User",
status: "Status",
active: "Active",
disabled: "Disabled",
enable: "Enable",
disable: "Disable",
actions: "Actions",
newPassword: "New password",
resetPassword: "Reset password",
userCreated: "User created",
userUpdated: "User updated",
auditTitle: "Audit log",
auditHintAdmin: "View login and operation records for all users.",
auditHintUser: "View your own operation records.",
filterUsername: "Filter username",
filterAction: "Filter action",
colTime: "Time",
colUser: "User",
colAction: "Action",
colMethod: "Method",
colPath: "Path",
colStatus: "Status",
colIp: "IP",
apiKeysTitle: "API Key management",
apiKeysHelp:
"For MCP/scripts and netxops (web UI uses sessions). Secret shown once; Edit scopes later. Cap {{max}} non-revoked keys.",
tokenQuota: "Active {{active}} / {{max}}",
tokenLimitReached: "API key limit reached ({{max}}). Revoke or delete unused keys first.",
tokenName: "Name",
expiresIn: "Expiry",
expire7d: "7 days",
expire30d: "30 days",
expire90d: "90 days",
expire365d: "1 year",
expireNever: "Never",
tokenOwner: "Owner",
tokenOwnerSelf: "Myself ({{user}})",
createToken: "Create key",
tokenCreated: "API key created",
tokenUpdated: "Scopes updated",
tokenRevoked: "Revoked",
tokenDeleted: "Deleted",
tokenOnceHint: "Copy and store this secret now; it will not be shown again:",
copyToken: "Copy",
tokenCopied: "Copied",
tokenCopyFailed: "Copy failed",
expiresAt: "Expires",
lastUsed: "Last used",
tokenStatusActive: "Active",
tokenStatusExpired: "Expired",
tokenStatusRevoked: "Revoked",
revokeToken: "Revoke",
revokeConfirm: "Revoke this API key? It stops working immediately; the record remains.",
deleteToken: "Delete",
deleteConfirm: "Permanently delete this API key? This cannot be undone.",
editScopes: "Edit scopes",
saveScopes: "Save scopes",
scopesUnsetLegacy: "Unrestricted (same as owner)",
scopesCol: "Scopes",
scopesRequired: "Select at least one scope",
scopesNoneAvailable: "This owner has no grantable scopes",
scopeAlarmsRead: "alarms:read — Alarms read (netx MCP)",
scopeNeRead: "ne:read — NE / topology read (MCP)",
scopeNeWrite: "ne:write — NE / topology write / draw (MCP)",
scopeNeExec: "ne:exec — Managed NE CLI (netx MCP)",
scopeBizMonitorRead: "biz-monitor:read — Biz monitor / cutover read (netxops)",
scopeSql: "sql:query — UME SQL (netx MCP)",
scopeWebcrt: "webcrt:session — WebCRT (reserved MCP)",
scopeAdminUsers: "admin:users — User admin (reserved MCP)",
scopeOpsWrite: "ops:write — Ops write / subscriptions (reserved MCP)",
forceChangeTitle: "Change initial password",
forceChangeHint: "Account {{user}} is still using the default password. You must change it before continuing.",
oldPassword: "Current password",
confirmPassword: "Confirm new password",
savePassword: "Save new password",
savingPassword: "Saving…",
passwordTooShort: "New password must be at least 8 characters",
passwordMismatch: "New passwords do not match",
passwordMustChange: "New password must differ from the default/old password",
};
export default auth;

View file

@ -0,0 +1,331 @@
const bizCompare = {
title: "Business compare",
templates: "Compare templates",
newTemplate: "New template",
editTemplate: "Edit template",
saveTemplate: "Save template",
templateSaved: "Template saved",
templateDeleted: "Template deleted",
confirmDeleteTemplate: "Delete this compare template?",
confirmDeleteJob: "Delete this compare job and all runs?",
confirmDeleteRun: "Delete this compare run? Job config is kept.",
deleteJob: "Delete job",
deleteRun: "Delete run",
runDeleted: "Compare run deleted",
emptyTemplates: "No templates yet. Create one or import a JSON file.",
exportTemplate: "Export",
importTemplate: "Import",
templateExported: "Template exported",
templateImported: "Template imported",
templateImportInvalid: "Invalid file: need name and metrics",
templateValidationFailed: "Template validation failed; correct the indicated settings",
filterOperator: "Filter operator",
complexFiltersPreserved: "These nested filters are preserved in full. Export, edit the JSON and import it to update them.",
portPolicy: "Port change policy",
portPolicyHint: "Port mappings take priority. This policy controls whether interface identity fields participate in matching without a mapping.",
portPolicyAuto: "Automatic",
portPolicyStrict: "Interfaces must match",
portPolicyIgnore: "Allow port changes",
missingSheets: "Configured checks were removed or renamed; select the scope again",
enableCurrentSheets: "Enable all current checks",
templateUsage: "Used by {{n}} jobs",
templateImportHint: "Import/export JSON to share templates privately (no job/batch data)",
templateHint:
"Pick metrics, then set Key / interface / compare / display. Compare modes: exact, abs delta, or relative %. Display can include context-only columns. Empty compare = presence-only.",
stepPickMetrics: "① Pick metrics",
stepConfigRules: "② Configure compare rules",
pickMetricsHint: "Check metrics to add. Then configure each sheet below.",
pickMetricsHintShort: "Click to add/remove; switch sheets in the left list below.",
showMetrics: "Show metric picker",
hideMetrics: "Hide metric picker",
noMetricsYet: "Select at least one metric first",
selectedCount: "{{n}} selected",
tabFields: "Field rules",
tabFilters: "Row filters",
fieldsHintShort:
"Key aligns rows; Compare fields take part in value diff; empty compare = presence-only.",
tagCompareN: "{{n}} cmp",
colFields: "Fields",
keyFields: "Key",
ifaceFields: "Interface",
compareFields: "Compare",
compareMode: "Mode",
tolerance: "Tolerance",
displayField: "Display",
keyAlwaysDisplay: "Key columns are always shown",
modeEq: "Exact equal",
modeNumeric: "Abs delta",
modePercent: "Relative %",
normalizeField: "Norm",
ignoreFields: "Ignore",
field: "Field",
note: "Note",
keyRequired: "Select at least one key field",
keyIsIdentity: "Key aligns rows; not used as a value-compare field",
metricsRequired: "Add at least one metric sheet",
noMetricFields: "No fields for this metric",
rowFilters: "Row filter rules",
rowFiltersHint:
"Filter rows before compare. Conditions in a group are AND; groups are OR. Selecting ARP seeds editable default rules you can change.",
rowFiltersHintShort: "AND in group · OR across groups · empty = all rows",
rowFiltersEmpty: "No filters → compare all rows.",
filterClear: "Clear",
filterAddOr: "Add group (OR)",
filterAddAnd: "Add condition (AND)",
filterAddOrShort: "+ OR group",
filterAddAndShort: "+ AND",
filterRemoveGroup: "Remove group",
filterGroupN: "Group {{n}}",
filterOr: "OR",
filterAnd: "AND",
filterField: "Field",
filterOp: "Op",
filterValue: "Value",
normalizeNone: "None",
normalizeLower: "lower",
normalizeUpper: "upper",
normalizeMac: "MAC",
normalizeEmpty: "empty→blank",
templateFilterPh: "Filter template / metric",
jobFilterPh: "Filter job / template / status",
addMetric: "Add metric",
pickMetric: "Pick metric…",
addSheet: "Add to template",
removeSheet: "Remove sheet",
sheetIdRequired: "sheet_id is required (unique per split)",
duplicateSheetId: "sheet_id must be unique",
sheetTitle: "Sheet title",
sheetId: "sheet_id",
splitSheet: "Split / duplicate",
colSheets: "Sheets",
sheetsUnit: "sheets",
presenceShort: "presence",
modeFields: "Mode: compare selected field values",
modeFieldsShort: "fields",
modePresence: "Mode: presence-only (entry set match)",
mapping: "Port mapping (optional)",
newMapping: "New mapping…",
mapName: "Mapping name",
saveMapping: "Save mapping",
validateMapping: "Validate",
validateNeed: "Select mapping and before/after batches first",
mapHint:
"One pair per line: before_if,after_if (comma / tab / pipe). Example:\ngei-0/1/1,gei-0/1/2\nxgei-0/1/0,xgei-0/2/0\n# comments start with #. Not slash-grouped A,B/C,D.",
mappingSaved: "Mapping saved",
mappingOptionalHint:
"Optional. Empty = ignore local port renames (match on non-interface keys). Fill to apply port map. Main-port pairs also cover .subinterfaces (suffix kept).",
ifaceNormalize: "Interface normalize",
ifaceNormalizeHint:
"Template rules, one per line: alias,canonical. Applied before port mapping. Example:\nGE,gei\nSG,smartgroup\nXGE,xgei",
createJob: "Create compare job",
defaultJobName: "Cutover compare",
jobName: "Name",
template: "Template",
mode: "Mode",
modeManual: "Manual batches",
modeAuto: "Auto (pin before; latest after batch)",
beforeTask: "Before task",
beforeBatch: "Before batch",
afterTask: "After task",
afterBatch: "After batch",
sideBefore: "Before",
sideAfter: "After",
sidesVs: "vs",
sidesTitle: "Compare sides",
configSnapshot: "Execution settings saved",
configSnapshotHint: "Template rules and port mapping are fixed for this run. Later edits do not affect historical lookup. The downloaded report includes the execution settings.",
configLegacy: "Legacy run: settings not saved",
configLegacyHint: "This run has no configuration snapshot. Success lookup and compact row reconstruction use the current settings, which may differ from the original execution.",
pick: "Select…",
sameAsBefore: "Same as before / select…",
autoHint: "auto: new after-task batches trigger compare",
createCompare: "Create job",
createSteps: "Create steps",
stepBasics: "Basics",
stepSheets: "Sheets",
stepBatches: "Batches",
stepMapping: "Port mapping",
next: "Next",
back: "Back",
enabledSheets: "Compare sheets",
enabledSheetsHint: "Toggle sheets; all on by default. Off sheets are skipped.",
noTemplateSheets: "This template has no sheets",
needJobName: "Enter a job name",
needTemplate: "Select a compare template",
needSheets: "Enable at least one sheet",
needBeforeBatch: "Select before task and batch",
needAfterBatch: "Select after batch",
runNow: "Run now",
runStarted: "Compare started in background — you can close this page; check run history for progress",
runFailed: "Compare failed",
runStatusRunning: "Running",
runStatusFailed: "Failed",
runStatusDoneSec: "Done {{s}}s",
runProgress: "{{phase}} · sheet {{i}}/{{n}} · {{sheet}} · {{s}}s elapsed",
runSheetProgress: "sheet {{i}}/{{n}}",
runElapsed: "{{s}}s elapsed",
runRowsLoaded: "Loaded {{side}} {{n}} rows",
runRowsSqlCount: "DB count {{side}} {{n}} rows",
runPersisting: "Wrote {{done}}/{{total}} rows",
runEngineSql: "engine SQL",
runEnginePython: "engine Python",
runEngineNote: "reason {{note}}",
phaseQueued: "Queued",
phaseLoading: "Loading",
phaseSqlCount: "Counting",
phaseSqlProject: "Preparing tables",
phaseSqlJoin: "Joining",
phaseSqlFailFetch: "Fetching fails",
phaseComparing: "Comparing",
phasePersisting: "Writing",
phasePersistingFail: "Writing fails",
phasePersistingOk: "Writing passes",
phaseDone: "Done",
phaseFailed: "Failed",
phaseCancelled: "Cancelled",
sheetPending: "Pending",
colProgress: "Progress / result",
passRateScope: "all rows",
keyFiltersToggle: "Field filters",
hideDisplayCols: "Hide display cols",
showDisplayCols: "Show display cols",
displayColsToggle: "Display cols",
hideDisplayColsHint: "Compact: display-only columns are hidden by default; highlight means they are shown",
ranWithDuration: "Compare finished ({{s}}s)",
unchangedNotStored: "Success rows were counted but not stored. Set “Store success rows” to sample and re-run for spot-check.",
unchangedSampleHint: "{{total}} success rows total; browsing a stratified sample of {{listed}}. Search any route for live source lookup. Pass rate uses all {{total}}.",
liveSearchHint: "Pairs are completed from source batches before filtering. Pass rate still uses the full sheet.",
liveSearchTruncatedHint: "Search limit reached; some entries are omitted. Narrow the filters. Incomplete key groups were skipped.",
clearSearch: "Clear filters",
resultLoading: "Loading comparison details…",
resultRange: "{{from}}–{{to}} of {{total}}",
resultLoadFailed: "Could not load comparison details",
retry: "Retry",
searchEmpty: "No results match these filters. Clear filters or select another result type.",
clearKeyFilters: "Clear fields",
storeUnchanged: "Store success rows",
storeUnchangedAuto: "Auto (full if small / sample 5k + hydrate if large)",
storeUnchangedSample: "Sample only (up to 5k keys + hydrate)",
storeUnchangedKeys: "All success keys (full browse; large write still slow)",
storeUnchangedAlways: "Always full JSON (slow on huge sheets)",
storeUnchangedNever: "Never (counts only)",
storeUnchangedHint: "Cutover focuses on fails and pass rate; success defaults to a stratified sample. Type a filter to look up any route from source tables. Pass rate uses the full success count.",
saveJob: "Save config",
jobSaved: "Job config saved",
jobDeleted: "Compare job deleted",
jobList: "Compare jobs",
detail: "Details",
edit: "Edit",
delete: "Delete",
tabConfig: "Job config",
tabRuns: "Compare batches",
tabResult: "Result",
detailTabs: "Business comparison workspace",
jobReady: "Ready",
configBasics: "Job parameters",
configSources: "Before and after sources",
configScope: "Check scope",
selectMapping: "Select mapping",
mappingPairs: "Interface pairs",
sourceBaseline: "Fixed baseline",
sourceTarget: "Acceptance target",
sourceLatest: "Latest batch",
sourceRows: "{{n}} collected rows",
autoSourceHint: "Keep the before batch fixed. Compare against the latest completed batch from the after task.",
runHistoryTitle: "Execution history",
runHistoryHint: "Showing the latest 50 runs. Execution status and business verdicts are displayed separately. Open results to inspect.",
refreshRuns: "Refresh history",
loadedRuns: "Loaded runs",
activeRuns: "In progress",
completedRuns: "Completed",
stoppedRuns: "Failed / cancelled",
runFinished: "Completed",
runStatusQueued: "Queued",
runsLoading: "Loading execution history…",
runsLoadError: "Could not load execution history",
runs: "Run history",
runsHint: "Each “Run now” creates a batch. Stuck “Running” after restart is auto-cancelled; you can also cancel manually and re-run.",
noRuns: "No runs yet — run a compare first",
result: "Result",
viewResult: "View result",
cancelRun: "Cancel",
confirmCancelRun: "Cancel this compare batch? You can start a new run afterward.",
runCancelled: "Compare cancelled",
runStatusCancelled: "Cancelled",
compareAlreadyRunning: "A compare is already running — cancel it under Compare batches or wait",
runBatchSummary: "Fail {{fail}} · Pass {{ok}} · Added {{added}}",
colTime: "Time",
pickBatchRun: "Select compare run",
pickRun: "Select run…",
runCount: "{{n}} runs",
kindAll: "All",
kindDiff: "Fail",
kindAdded: "Added",
kindRemoved: "Missing only",
kindChanged: "Mismatch only",
kindUnchanged: "Pass",
kindAddedShort: "Added",
kindRemovedShort: "Missing",
kindChangedShort: "Mismatch",
kindUnchangedShort: "Pass",
kindFail: "Fail",
kindPass: "Pass",
kindSuccess: "Pass",
successCount: "Pass",
passRateShort: "Pass %",
verdictPass: "Pass",
verdictFail: "Fail",
failCount: "Failed rows",
missCount: "Missing",
extraCount: "Extra",
mismatchCount: "Mismatch",
matchCount: "Match",
failFields: "Failed fields",
failFieldsEmpty: "—",
failWholeRow: "Whole row",
pairBefore: "pre",
pairAfter: "post",
sheetAllPass: "All match",
filterFailField: "Failed compare field",
filterFailFieldAll: "Any failed field",
resultEmpty: "No matching rows",
resultFilterPh: "Free text, or direction:out network:1.1.1.1",
diffCount: "{{n}} failed",
passRate: "Pass rate",
passOk: "All passed",
passWarn: "Has failures",
runOverview: "Verdict",
sheetCurrent: "Current check",
diffCountLabel: "Failed rows",
diffRate: "Fail rate",
beforeCount: "Before rows",
afterCount: "After rows",
matchedRows: "Matched",
totalRows: "Total rows",
sheetCount: "Sheets",
sheetNavTitle: "Checks",
sheetNavHint: "{{n}} checks · click to switch",
collapseSheetNav: "Collapse checks",
expandSheetNav: "Expand checks",
sheetFailCount: "{{n}} failed",
topChangedFields: "Top failed fields",
exportTables: "Download tables",
exportOk: "Compare tables exported",
fullscreen: "Fullscreen board",
exitFullscreen: "Exit fullscreen",
emptyJobs: "No compare jobs yet",
created: "Compare job created",
ran: "Compare finished",
colName: "Name",
colMode: "Mode",
colStatus: "Status",
colKind: "Verdict",
colResult: "Result",
colDetail: "Detail",
colFailFields: "Failed fields",
colActions: "Actions",
colKey: "Identity",
colChange: "Changes",
};
export default bizCompare;

View file

@ -0,0 +1,302 @@
const bizMigration = {
fullscreen: "Wall mode",
exitFullscreen: "Exit wall mode",
fullscreenHint: "Display cutover status fullscreen. Press Esc to return; use a full-window layout when fullscreen is unavailable.",
wallTitle: "Cutover monitoring · Service migration",
wallProgress: "Wave migration progress · Current snapshot",
wallFilters: "Show filters and evaluation controls",
wallState_waiting: "Awaiting evaluation",
wallState_unknown: "Data needs confirmation",
wallState_anomaly: "Service anomalies detected",
wallState_normal: "No service anomalies in current samples",
pairCollectionHint: "Each metric has a paired collection round. Saving settings updates task bindings. Route tables are manual-only for final checks; use a separate business comparison for millions of routes. Actual cadence depends on command duration and device availability.",
pairedSampling: "Paired sampling · Timeliness",
manualRoute: "Manual final check",
pairedPeriodic: "Paired periodic",
samplingMode: "Sampling mode",
samplingDuration: "Duration old / new",
samplingQueue: "Queue old / new",
startSkew: "Start skew",
sameRound: "Round",
metricBaselineMissing: "This metric needs a matching baseline receipt",
samplePhase_idle: "Not collected",
samplePhase_queued: "Queued",
samplePhase_running: "Collecting",
samplePhase_success: "Complete",
samplePhase_partial: "Partial",
samplePhase_failed: "Failed",
samplePhase_cancelled: "Stopped",
collect_pair_invalid: "Incomplete collection pair",
collect_pair_missing: "No complete paired sample",
collect_pair_pending: "Waiting for both sides of this round",
collect_pair_failed: "Current round failed; older samples cannot replace it",
collect_round_mismatch: "Samples belong to different rounds",
sample_start_skew: "Actual collection starts are too far apart",
latestSnapshot: "Latest snapshot",
steadyOutside: "{{n}} stable items outside scope",
"followLive": "Follow live",
"historySnapshot": "Historical snapshot",
"evaluatedAt": "Evaluated",
"refreshBoard": "Refresh board",
"oldestSample": "Oldest sample",
"liveOverview": "Business migration overview",
"remaining": "Remaining items",
"anomalyInBatch": "Current wave anomalies",
"anomalyOutsideBatch": "Outside wave anomalies",
"coverageUnknown": "Uncertain coverage; acceptance cannot pass",
"coverageUnknownHint": "Resolve missing, stale or misaligned samples and duplicate keys. Healthy samples cover their own monitor items.",
"awaitingPeer": "Awaiting target sample",
"migratedPrevious": "Previous wave received",
"unfinishedPrevious": "Previous wave unfinished",
"sample_time_missing": "Sample time missing",
"sample_skew": "Samples too far apart",
"sample_stale": "Stale samples",
"duplicateKeys": "Duplicate business keys",
"uncoveredScope": "Missing scope sheets",
"refreshFailed": "Refresh failed; showing retained snapshot:",
"lastAcceptance": "Last acceptance",
"diffSearch": "Search business key / verdict / status",
"verdictFilter": "Verdict filter",
"allVerdicts": "All verdicts",
"warningVerdict": "In progress / warning",
"healthyVerdict": "Received / healthy",
"unknownVerdict": "Uncertain",
"loadingBoard": "Updating…",
"rowsMatched": "{{n}} matching items",
"showOutsideAnomalies": "Show outside anomalies",
"scopeCurrent": "Current wave",
"scopePrevious": "Previous wave",
"scopeOutside": "Outside scope",
"fieldChanges": "{{n}} changed fields",
"outsideProtection": "Protect businesses outside this wave",
"outsideProtectionHint": "Report drift outside this wave. Continue checking migrated businesses from previous waves without counting them in current progress.",
"reason_service_not_received": "Source departed; target not healthy",
"reason_target_unhealthy": "Target unhealthy",
"reason_old_service_not_in_baseline": "Business missing from source baseline",
"reason_service_content_changed": "Received business content differs",
"reason_old_service_still_present": "Business still on source",
"reason_target_sample_predates_source": "Target sample predates source departure",
"reason_previous_batch_received": "Previous wave remains healthy",
"reason_previous_batch_not_received": "Previous wave not received",
"reason_unmapped_same_iface": "Drift on unmapped interface",
"reason_out_of_expect:anomaly": "Business drift outside scope",
"reason_out_of_expect:lost": "Business lost outside scope",
title: "Cutover monitor",
hint: "Separate from business compare: baseline drift, expect set, dual-device verdict. Reuses collect tasks and port mappings.",
hintPort:
"Metric focus: port status (interface_brief / admin·phy·prot). Baseline → pick ports → evaluate board.",
hintShort: "Monitor template sets rules; bind collect + dual baselines → expect → evaluate board.",
create: "Create task",
createHint:
"Except portrait baselines, the full cutover flow runs on this page; create auto-spawns HF biz-monitor tasks on schedule.",
createHintShort:
"Pick old/new NEs → monitor template & metrics → interval/window → baselines → port map → create. HF tasks are auto-created.",
pickOldNe: "Old NE",
pickNewNe: "New NE",
pickNe: "Pick NE",
pickNeHint: "Click to pick a device",
hideNePicker: "Hide list",
nePairReady: "Both NEs selected — continue, or click a card to re-pick.",
needNePair: "Enter a name and select old/new NEs",
needProjectName: "Enter a task name",
needMonitorTemplate: "Select a monitor template",
needMetrics: "Select at least one metric",
noPortraitBaseline:
"No portrait task/batches for this NE yet — run portrait collect in business monitor first (baselines can be set later).",
baselineOptionalHint: "Baselines can be set later in detail; pick from portrait batches when available.",
metricInterval: "Per-metric interval (sec)",
metricIntervalOptional: "Per-metric interval (optional)",
metricIntervalHint: "Leave blank to use the global interval; a filled value creates a separate HF task.",
scheduleHint:
"Enter the window in your local time; stored as UTC. Scheduler pauses HF outside the window.",
sectionSchedule: "Collect interval & window",
sectionBaseline: "Baselines & mapping",
sectionDevices: "Old / new NEs",
sectionMetrics: "Collect metrics",
createSteps: "Create steps",
stepDevices: "Devices",
stepTemplate: "Template",
stepSchedule: "Schedule",
stepBaseline: "Baselines",
next: "Next",
back: "Back",
colHf: "HF",
hfReady: "Ready",
hfMissing: "Missing",
showCreateMore: "More: template / mapping / baselines",
hideCreateMore: "Hide more",
setupHintShort: "Portrait tasks feed baselines; HF tasks feed cutover collect. Configure template, metrics, window and dual baselines here.",
createProject: "Create task",
projectName: "Task name",
projectNamePh: "e.g. PE-A / PE-B cutover-1",
sectionName: "1. Task name",
sectionTasks: "2. Bind collect tasks (required)",
sectionTasksHint: "Pick the old-device and new-device business-monitor tasks.",
sectionOptional: "3. Monitor template, mapping & baselines (optional)",
sectionOptionalHint:
"Skip for now if needed; set later under Baseline. Defaults to the system monitor template when omitted.",
sectionPair: "Bound collect tasks",
sectionPairHint: "Collect tasks are fixed at create time; adjust monitor template, mapping and baselines here.",
sectionBaselineHint: "Both old and new baselines are required; old lists expect objects, new feeds dual-side verdict.",
optionalNone: "None",
monitorTemplate: "Monitor template",
monitorTemplateHint: "Chooses which metrics to compare and dual-side success semantics. Defaults to the system monitor template.",
compareTemplate: "Compare template",
compareTemplateBound: "Bound compare template: {{name}}",
portMappingHint:
"With a mapping, iface metrics compare only mapped ports (they must appear after cutover). Same-name ports outside the map are not a migration; old down + new up is an anomaly. No mapping means same-name compare. BGP/PW still use the batch expect set.",
baselineNeedTask: "Select the matching collect task first",
baselinePickHint: "Choose from that task’s collect batches",
oldTask: "Old-device portrait task",
newTask: "New-device portrait task",
portMapping: "Port mapping",
oldBaseline: "Old baseline batch",
newBaseline: "New baseline batch",
needProjectFields: "Enter a name and pick old/new portrait tasks",
needExpectPorts: "Select at least one expected object for this batch",
needBothBaselines: "Save both old and new baseline batches before evaluating",
errOldBaselineRequired: "Old baseline batch is required",
errNewBaselineRequired: "New baseline batch is required",
needBizStateTasks: "No portrait business-monitor tasks yet. Create one first:",
projectCreated: "Task created",
projects: "Tasks",
empty: "No cutover tasks yet.",
emptyProjects: "No cutover tasks yet",
listFilterPh: "Filter name / NE / IP / status",
kpiProjects: "Tasks",
kpiActive: "Active",
detail: "Detail",
cancel: "Close",
colName: "Task",
colPair: "Old → New",
colBaseline: "Baseline",
colCreated: "Created",
colActions: "Actions",
baselineSet: "Set",
baselineUnset: "Unset",
statusActive: "Active",
statusDone: "Done",
statusPaused: "Paused",
statusPending: "Pending",
statusReview: "Review",
tabSetup: "Baseline",
tabBatches: "Batches",
tabBoard: "Board",
saveBaseline: "Save",
baselineSaved: "Baseline saved",
errPairCapacity: "Paired sampling needs at least two collection slots. Check task concurrency, collection threads and the CLI budget.",
errPairBusy: "This metric is still collecting or queued on either side. Wait for the round to finish.",
errPairInvalid: "The collector pair is incomplete. Save configuration again to restore it.",
errPairSameDevice: "Cutover monitoring requires two different devices.",
pickExpectPorts: "Expect",
portFilterPh: "Filter",
emptyBaselinePorts: "No baseline data",
batchLabel: "Batch label",
batchId: "Batch ID",
copyBatchId: "Copy batch ID",
defaultBatchLabel: "Night N",
expectPorts: "Expected ports (old side)",
expectPortsPh: "Comma or newline separated",
createBatch: "New",
batchCreated: "Batch created",
batches: "Batches",
startBatch: "Start",
finishBatch: "Finish",
evaluate: "Evaluate",
evaluated: "Done",
statusUpdated: "Status updated",
board: "Board",
metricPort: "Port status",
progress: "Progress",
anomaly: "Anomaly",
onlyExpect: "Expect only",
onlyExpectHint: "Shows all drift by default; check to filter to this batch's expect set",
pinCurrentBatches: "Current collect batches",
pinOldBatch: "Old current",
pinNewBatch: "New current",
pinLatest: "Latest",
runHistory: "Runs",
runLatest: "Latest",
colRuleHit: "Rule hit",
colCommand: "Collect command",
mappingBound: "Mapping saved and bound to this project",
allMetrics: "All metrics",
colMetric: "Metric",
colKey: "Object",
colPort: "Old port",
openRedChip: "Red {{n}}",
metaPortrait: "Portrait",
metaHf: "HF",
metaBaseline: "Baseline",
baselineOk: "set",
baselineMissing: "missing",
needBaselineFirst: "Set baseline first",
enableHighfreq: "Enable HF",
highfreqReady: "HF ready",
collectNow: "Collect now",
collectTriggered: "Collect triggered",
openBizState: "Biz monitor",
hfNotBound: "Unbound",
projectCollectMetrics: "Metrics",
hfInterval: "Interval(s)",
hfStart: "Start",
hfEnd: "End",
markProjectDone: "Done / stop HF",
confirmProjectDone: "Complete project and pause all HF?",
projectMarkedDone: "Done; HF paused",
deleteProject: "Delete",
confirmDeleteProject: "Delete project? HF will pause and batches removed.",
projectDeleted: "Deleted",
acceptPassedShort: "Pass",
acceptFailedShort: "Fail",
colMapped: "Mapped new",
colDesc: "Description",
colOldPort: "Old business key",
colNewPort: "New business key",
colOldStatus: "Old admin/phy/prot",
colNewStatus: "New admin/phy/prot",
colOld: "Old",
colNew: "New",
colVerdict: "Verdict",
emptyDiffs: "No rows yet — run Evaluate",
emptyRed: "No red tickets",
verdictMigrated: "Migrated",
verdictMigrating: "Migrating",
verdictLost: "Lost",
verdictAnomaly: "Anomaly",
verdictNotInvolved: "Not involved",
verdictUnexpected: "Unexpected new",
verdictOk: "OK",
highfreqHint: "",
boundTasks: "Bound tasks",
oldPortraitTask: "Old portrait",
newPortraitTask: "New portrait",
oldHfTask: "Old HF",
newHfTask: "New HF",
projectCollectHint: "",
hfWindowInactive: "HF window inactive or project done",
baselinePortraitOnly: "",
collectSkipped: "skipped",
collectSkippedBanner: "Skipped: {{metrics}}",
currentMissing: "no current",
currentMissingBanner: "No current: {{metrics}}",
highfreqReadyInactive: "HF bound (window inactive, no collect)",
collectPartial: "Partial fail (ok {{ok}} / fail {{fail}})",
errMetricNeedsBindings: "Metric \"{{metric}}\" needs bindings ({{params}}); not supported for cutover HF — remove it",
errNoProfileForMetric: "Metric \"{{metric}}\" has no collect profile for this vendor",
acceptTitle: "Acceptance",
acceptPassed: "Passed",
acceptFailed: "Failed ({{n}} red)",
newBaselineMissingBanner: "Missing new baseline: {{metrics}}",
acceptCarryHint: "May continue with red",
openRedHint: "Red {{n}}",
redTitle: "Red tickets",
redOpen: "open",
resolveRed: "Resolve",
redResolved: "Resolved",
batchCreatedWithRed: "Batch created ({{n}} red)",
colStatus: "Status",
verdictUnfinished: "Unfinished",
};
export default bizMigration;

Some files were not shown because too many files have changed in this diff Show more