mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 13:03:19 +08:00
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:
parent
3b45d6abe3
commit
0d894efe5e
196 changed files with 26947 additions and 28091 deletions
78
web/WEB.md
78
web/WEB.md
|
|
@ -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`
|
||||
|
||||
## 构建
|
||||
|
|
|
|||
|
|
@ -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 → 共享层)。把共用部分下沉到共享层。',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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() {
|
||||
|
|
@ -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() {
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
|
|
@ -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";
|
||||
|
|
@ -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[] };
|
||||
|
|
@ -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;
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -38,7 +38,7 @@ import {
|
|||
jobChipColor,
|
||||
NmStatusChip,
|
||||
sourceChipColor,
|
||||
} from "./nmChips";
|
||||
} from "../../components/NmChips";
|
||||
|
||||
const POLL_MS = 2500;
|
||||
const TARGET_PAGE_SIZE = 10;
|
||||
|
|
@ -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() {
|
||||
|
|
@ -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";
|
||||
|
|
@ -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 =
|
||||
|
|
@ -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);
|
||||
|
|
@ -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() {
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { openNewModuleWindow } from "./moduleWindows";
|
||||
import { openNewModuleWindow } from "../../../utils/moduleWindows";
|
||||
|
||||
export const PORT_TRAFFIC_WALL_MODULE_ID = "port-traffic-wall";
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
|
|
@ -1 +1 @@
|
|||
export { AppModalShell, TopoModalShell, type AppModalShellProps } from "./AppModalShell";
|
||||
export { AppModalShell, TopoModalShell, type AppModalShellProps } from "../../../components/ui/AppModalShell";
|
||||
|
|
@ -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 = {
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 = {
|
||||
|
|
@ -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 };
|
||||
|
||||
|
|
@ -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 };
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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];
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>,
|
||||
|
|
@ -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(() => {
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>[],
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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";
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { TopologyDiscoverNeResult } from "../../types";
|
||||
import type { TopologyDiscoverNeResult } from "../../../types";
|
||||
|
||||
export function newId(): string {
|
||||
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
||||
|
|
@ -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>[];
|
||||
|
|
@ -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 || "")
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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. */
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"];
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ModuleIconKind, ModuleIconTone } from "../config/modules";
|
||||
import type { ModuleIconKind, ModuleIconTone } from "../../../config/modules";
|
||||
|
||||
type Props = {
|
||||
tone: ModuleIconTone;
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
2987
web/src/i18n/en.ts
2987
web/src/i18n/en.ts
File diff suppressed because it is too large
Load diff
80
web/src/i18n/locales/en/audit.ts
Normal file
80
web/src/i18n/locales/en/audit.ts
Normal 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;
|
||||
112
web/src/i18n/locales/en/auth.ts
Normal file
112
web/src/i18n/locales/en/auth.ts
Normal 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;
|
||||
331
web/src/i18n/locales/en/bizCompare.ts
Normal file
331
web/src/i18n/locales/en/bizCompare.ts
Normal 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;
|
||||
302
web/src/i18n/locales/en/bizMigration.ts
Normal file
302
web/src/i18n/locales/en/bizMigration.ts
Normal 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
Loading…
Add table
Add a link
Reference in a new issue