mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 19:50:53 +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
76
web/WEB.md
76
web/WEB.md
|
|
@ -7,26 +7,68 @@
|
||||||
- TanStack Query(服务端状态)
|
- TanStack Query(服务端状态)
|
||||||
- 自研 i18n(`src/i18n/`,无额外依赖)
|
- 自研 i18n(`src/i18n/`,无额外依赖)
|
||||||
|
|
||||||
## 目录结构
|
## 代码结构
|
||||||
|
|
||||||
|
**按领域(feature)组织,依赖方向 `features → 共享层`,反向禁止**(ESLint `no-restricted-imports` 强制)。
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
config/modules.ts # 模块注册表(工作台卡片、路由、标题 i18n)
|
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
|
constants/queryKeys.ts # React Query key
|
||||||
components/ # 可复用 UI(无业务 API 调用)
|
styles/ # 见「样式规范」
|
||||||
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 # 纯展示辅助
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
现有领域:`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 |
|
| 路径 | 页面 | moduleId |
|
||||||
|
|
@ -61,7 +103,7 @@ src/
|
||||||
|
|
||||||
1. 在 `MODULES` 增加一项(`moduleId`、`path`、`section`、i18n key、`iconTone`)
|
1. 在 `MODULES` 增加一项(`moduleId`、`path`、`section`、i18n key、`iconTone`)
|
||||||
2. 在 `App.tsx` 增加 `<Route path="..." element={...} />`
|
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`
|
4. 工作台卡片由 `modulesInSection` 自动生成,点击走 `openOrFocusModule`
|
||||||
|
|
||||||
## 跨标签页导航
|
## 跨标签页导航
|
||||||
|
|
@ -79,7 +121,7 @@ src/
|
||||||
|
|
||||||
## 数据请求
|
## 数据请求
|
||||||
|
|
||||||
- API 仅写在 `services/api.ts`
|
- API 仅写在 `services/api/<领域>.ts`
|
||||||
- Query key 集中在 `constants/queryKeys.ts`
|
- Query key 集中在 `constants/queryKeys.ts`
|
||||||
- 失效缓存用 prefix key(如 `queryKeys.umeSyncStatusAll`)
|
- 失效缓存用 prefix key(如 `queryKeys.umeSyncStatusAll`)
|
||||||
- 顶栏连接状态:`App` 轮询 `GET /v1/integrations/status`(5s),展示 **netx api** 与 **DSH alarm hub**
|
- 顶栏连接状态:`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`
|
- 不使用 CSS Modules;类名 BEM 风格:`app-brand__logo`、`wb-card`
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,21 @@ export default defineConfig([
|
||||||
globals: globals.browser,
|
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 { lazy, Suspense } from "react";
|
||||||
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||||
import { AppLayout } from "./layout/AppLayout";
|
import { AppLayout } from "./layout/AppLayout";
|
||||||
import { WorkbenchPage } from "./pages/WorkbenchPage";
|
import { WorkbenchPage } from "./features/workbench/WorkbenchPage";
|
||||||
import { LoginPage } from "./pages/LoginPage";
|
import { LoginPage } from "./features/auth/LoginPage";
|
||||||
import { ForceChangePasswordPage } from "./pages/ForceChangePasswordPage";
|
import { ForceChangePasswordPage } from "./features/auth/ForceChangePasswordPage";
|
||||||
import { useAuth } from "./auth/AuthContext";
|
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(() =>
|
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 NePage = lazy(() => import("./features/managed-ne/NePage").then((m) => ({ default: m.NePage })));
|
||||||
const UmePage = lazy(() => import("./pages/UmePage").then((m) => ({ default: m.UmePage })));
|
const UmePage = lazy(() => import("./features/ume/UmePage").then((m) => ({ default: m.UmePage })));
|
||||||
const WebcrtPage = lazy(() => import("./pages/WebcrtPage").then((m) => ({ default: m.WebcrtPage })));
|
const WebcrtPage = lazy(() => import("./features/webcrt/WebcrtPage").then((m) => ({ default: m.WebcrtPage })));
|
||||||
const TopologyPage = lazy(() => import("./pages/TopologyPage").then((m) => ({ default: m.TopologyPage })));
|
const TopologyPage = lazy(() => import("./features/topology/TopologyPage").then((m) => ({ default: m.TopologyPage })));
|
||||||
const NetworkLayout = lazy(() =>
|
const NetworkLayout = lazy(() =>
|
||||||
import("./pages/network/NetworkLayout").then((m) => ({ default: m.NetworkLayout })),
|
import("./features/network/NetworkLayout").then((m) => ({ default: m.NetworkLayout })),
|
||||||
);
|
);
|
||||||
const NetworkDevicesPage = lazy(() =>
|
const NetworkDevicesPage = lazy(() =>
|
||||||
import("./pages/network/NetworkDevicesPage").then((m) => ({ default: m.NetworkDevicesPage })),
|
import("./features/network/NetworkDevicesPage").then((m) => ({ default: m.NetworkDevicesPage })),
|
||||||
);
|
);
|
||||||
const NetworkAlarmsPage = lazy(() =>
|
const NetworkAlarmsPage = lazy(() =>
|
||||||
import("./pages/network/NetworkAlarmsPage").then((m) => ({ default: m.NetworkAlarmsPage })),
|
import("./features/network/NetworkAlarmsPage").then((m) => ({ default: m.NetworkAlarmsPage })),
|
||||||
);
|
);
|
||||||
const NetworkConfigsPage = lazy(() =>
|
const NetworkConfigsPage = lazy(() =>
|
||||||
import("./pages/network/NetworkConfigsPage").then((m) => ({ default: m.NetworkConfigsPage })),
|
import("./features/network/NetworkConfigsPage").then((m) => ({ default: m.NetworkConfigsPage })),
|
||||||
);
|
);
|
||||||
const LldpLinksPage = lazy(() =>
|
const LldpLinksPage = lazy(() =>
|
||||||
import("./pages/network/LldpLinksPage").then((m) => ({ default: m.LldpLinksPage })),
|
import("./features/network/LldpLinksPage").then((m) => ({ default: m.LldpLinksPage })),
|
||||||
);
|
);
|
||||||
const TopologyClassifyPage = lazy(() =>
|
const TopologyClassifyPage = lazy(() =>
|
||||||
import("./pages/network/TopologyClassifyPage").then((m) => ({ default: m.TopologyClassifyPage })),
|
import("./features/network/TopologyClassifyPage").then((m) => ({ default: m.TopologyClassifyPage })),
|
||||||
);
|
);
|
||||||
const PortTrafficPage = lazy(() =>
|
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(() =>
|
const PortTrafficBoardListPage = lazy(() =>
|
||||||
import("./pages/network/PortTrafficBoardListPage").then((m) => ({
|
import("./features/port-traffic/PortTrafficBoardListPage").then((m) => ({
|
||||||
default: m.PortTrafficBoardListPage,
|
default: m.PortTrafficBoardListPage,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
const PortTrafficWallPage = lazy(() =>
|
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(() =>
|
const BizStatePage = lazy(() =>
|
||||||
import("./pages/network/BizStatePage").then((m) => ({ default: m.BizStatePage })),
|
import("./features/cutover/BizStatePage").then((m) => ({ default: m.BizStatePage })),
|
||||||
);
|
);
|
||||||
const BizComparePage = lazy(() =>
|
const BizComparePage = lazy(() =>
|
||||||
import("./pages/network/BizComparePage").then((m) => ({ default: m.BizComparePage })),
|
import("./features/cutover/BizComparePage").then((m) => ({ default: m.BizComparePage })),
|
||||||
);
|
);
|
||||||
const BizMigrationPage = lazy(() =>
|
const BizMigrationPage = lazy(() =>
|
||||||
import("./pages/network/BizMigrationPage").then((m) => ({ default: m.BizMigrationPage })),
|
import("./features/cutover/BizMigrationPage").then((m) => ({ default: m.BizMigrationPage })),
|
||||||
);
|
);
|
||||||
const BizMonitorTemplatesPage = lazy(() =>
|
const BizMonitorTemplatesPage = lazy(() =>
|
||||||
import("./pages/network/BizMonitorTemplatesPage").then((m) => ({
|
import("./features/cutover/BizMonitorTemplatesPage").then((m) => ({
|
||||||
default: m.BizMonitorTemplatesPage,
|
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(() =>
|
const AuditLayout = lazy(() =>
|
||||||
import("./pages/audit/AuditLayout").then((m) => ({ default: m.AuditLayout })),
|
import("./features/audit/AuditLayout").then((m) => ({ default: m.AuditLayout })),
|
||||||
);
|
);
|
||||||
const TaskOverviewPage = lazy(() =>
|
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(() =>
|
const ApiTokensPage = lazy(() =>
|
||||||
import("./pages/ApiTokensPage").then((m) => ({ default: m.ApiTokensPage })),
|
import("./features/api-tokens/ApiTokensPage").then((m) => ({ default: m.ApiTokensPage })),
|
||||||
);
|
);
|
||||||
const SessionsPage = lazy(() =>
|
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. */
|
/** 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 { useMemo, useState, type FormEvent } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import { HelpHint } from "../components/HelpHint";
|
import { HelpHint } from "../../components/HelpHint";
|
||||||
import { AppModalShell } from "../components/ui/AppModalShell";
|
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||||
import { FieldSelect } from "../components/ui/FieldSelect";
|
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useToast } from "../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import { apiDelete, apiGet, apiPatch, apiPost } from "../services/api";
|
import { apiDelete, apiGet, apiPatch, apiPost } from "../../services/api";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
|
|
||||||
type TokenRow = {
|
type TokenRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -2,9 +2,9 @@ import { Button, Input } from "@heroui/react";
|
||||||
import { Fragment, useMemo, useState } from "react";
|
import { Fragment, useMemo, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { apiGet } from "../services/api";
|
import { apiGet } from "../../services/api";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
|
|
||||||
type AuditItem = {
|
type AuditItem = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
useState,
|
useState,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { apiGet, apiPost, clearAuthToken } from "../services/api";
|
import { apiGet, apiPost, clearAuthToken } from "../../services/api";
|
||||||
|
|
||||||
export type AuthUser = {
|
export type AuthUser = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Alert, Button, Card, Form, Input, Label, TextField } from "@heroui/react";
|
import { Alert, Button, Card, Form, Input, Label, TextField } from "@heroui/react";
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "./AuthContext";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { apiPost } from "../services/api";
|
import { apiPost } from "../../services/api";
|
||||||
import { LoginShell } from "./LoginShell";
|
import { LoginShell } from "./LoginShell";
|
||||||
|
|
||||||
export function ForceChangePasswordPage() {
|
export function ForceChangePasswordPage() {
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Alert, Button, Card, Form, Input, Label, TextField } from "@heroui/react";
|
import { Alert, Button, Card, Form, Input, Label, TextField } from "@heroui/react";
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { Navigate, useSearchParams } from "react-router-dom";
|
import { Navigate, useSearchParams } from "react-router-dom";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "./AuthContext";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { LoginShell } from "./LoginShell";
|
import { LoginShell } from "./LoginShell";
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
|
|
@ -2,8 +2,8 @@ import { Button, Input, Modal } from "@heroui/react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { ListPager } from "../components/ListPager";
|
import { ListPager } from "../../components/ListPager";
|
||||||
import { AppModalShell } from "../components/ui/AppModalShell";
|
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||||
import {
|
import {
|
||||||
createNeCollection,
|
createNeCollection,
|
||||||
deleteCollectionJob,
|
deleteCollectionJob,
|
||||||
|
|
@ -20,16 +20,16 @@ import {
|
||||||
updateCollectionPolicy,
|
updateCollectionPolicy,
|
||||||
collectionJobDownloadUrl,
|
collectionJobDownloadUrl,
|
||||||
collectionRunDownloadUrl,
|
collectionRunDownloadUrl,
|
||||||
} from "../services/api";
|
} from "../../services/api";
|
||||||
import { queryKeys } from "../constants/queryKeys";
|
import { queryKeys } from "../../constants/queryKeys";
|
||||||
import { useDebouncedValue } from "../hooks/useDebouncedValue";
|
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useToast } from "../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import type { CollectionJobItem, CollectionRunItem, CollectionTargetRef, EligibleNeItem } from "../types";
|
import type { CollectionJobItem, CollectionRunItem, CollectionTargetRef, EligibleNeItem } from "../../types";
|
||||||
import { downloadCsv, fetchAllPages } from "../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { jobChipColor, NmStatusChip, sourceChipColor } from "./network/nmChips";
|
import { jobChipColor, NmStatusChip, sourceChipColor } from "../../components/NmChips";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
|
|
||||||
const POLL_MS = 2000;
|
const POLL_MS = 2000;
|
||||||
const ELIGIBLE_PAGE_SIZE = 10;
|
const ELIGIBLE_PAGE_SIZE = 10;
|
||||||
|
|
@ -2,13 +2,13 @@ import { Button, Input, Modal } from "@heroui/react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { ListPager } from "../components/ListPager";
|
import { ListPager } from "../../components/ListPager";
|
||||||
import { AppModalShell } from "../components/ui/AppModalShell";
|
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||||
import { FieldSelect } from "../components/ui/FieldSelect";
|
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||||
import { queryKeys } from "../constants/queryKeys";
|
import { queryKeys } from "../../constants/queryKeys";
|
||||||
import { useDebouncedValue } from "../hooks/useDebouncedValue";
|
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
|
||||||
import { useToast } from "../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
createConfigSyncCycle,
|
createConfigSyncCycle,
|
||||||
fetchCliTargets,
|
fetchCliTargets,
|
||||||
|
|
@ -20,12 +20,12 @@ import {
|
||||||
resumeConfigSyncCycle,
|
resumeConfigSyncCycle,
|
||||||
stopConfigSyncCycle,
|
stopConfigSyncCycle,
|
||||||
updateConfigSyncPolicy,
|
updateConfigSyncPolicy,
|
||||||
} from "../services/api";
|
} from "../../services/api";
|
||||||
import type { CliTargetItem, ConfigSyncCycle, ConfigSyncTargetRef, ConfigSyncTask } from "../types";
|
import type { CliTargetItem, ConfigSyncCycle, ConfigSyncTargetRef, ConfigSyncTask } from "../../types";
|
||||||
import { downloadCsv, fetchAllPages } from "../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
import { jobChipColor, NmStatusChip, sourceChipColor } from "./network/nmChips";
|
import { jobChipColor, NmStatusChip, sourceChipColor } from "../../components/NmChips";
|
||||||
|
|
||||||
const POLL_MS = 2500;
|
const POLL_MS = 2500;
|
||||||
const TARGET_PAGE_SIZE = 10;
|
const TARGET_PAGE_SIZE = 10;
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Button, Input, Modal } from "@heroui/react";
|
import { Button, Input, Modal } from "@heroui/react";
|
||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
|
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
|
||||||
import { parseTemplateImport, parseIfaceNormText, toOrGroups, fromOrGroups, validateTemplateDraft } from "./compareTemplateModel";
|
import { parseTemplateImport, parseIfaceNormText, toOrGroups, fromOrGroups, validateTemplateDraft } from "./lib/compareTemplateModel";
|
||||||
import type { MetricSheet, FieldRule, RowFilter } from "./compareTemplateModel";
|
import type { MetricSheet, FieldRule, RowFilter } from "./lib/compareTemplateModel";
|
||||||
import { ListPager } from "../../components/ListPager";
|
import { ListPager } from "../../components/ListPager";
|
||||||
import { AppModalShell } from "../../components/ui/AppModalShell";
|
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||||
import { FieldSelect } from "../../components/ui/FieldSelect";
|
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||||
|
|
@ -35,8 +35,8 @@ import {
|
||||||
formatErr,
|
formatErr,
|
||||||
} from "../../services/api";
|
} from "../../services/api";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./cutoverDataCache";
|
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./lib/cutoverDataCache";
|
||||||
import { jobChipColor, NmStatusChip } from "./nmChips";
|
import { jobChipColor, NmStatusChip } from "../../components/NmChips";
|
||||||
|
|
||||||
type PageTab = "templates" | "jobs";
|
type PageTab = "templates" | "jobs";
|
||||||
type JobDetailTab = "config" | "runs" | "result";
|
type JobDetailTab = "config" | "runs" | "result";
|
||||||
|
|
@ -40,8 +40,8 @@ import type { CliTargetItem } from "../../types";
|
||||||
import { writeClipboardText } from "../../utils/clipboard";
|
import { writeClipboardText } from "../../utils/clipboard";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { formatSystemTime, localDatetimeInputToUtcIso, utcIsoToLocalDatetimeInput } from "../../utils/time";
|
import { formatSystemTime, localDatetimeInputToUtcIso, utcIsoToLocalDatetimeInput } from "../../utils/time";
|
||||||
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./cutoverDataCache";
|
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./lib/cutoverDataCache";
|
||||||
import { jobChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
|
import { jobChipColor, NmStatusChip, sourceChipColor } from "../../components/NmChips";
|
||||||
|
|
||||||
type NeSourceFilter = "all" | "managed" | "ume";
|
type NeSourceFilter = "all" | "managed" | "ume";
|
||||||
type CreatePickSide = "old" | "new";
|
type CreatePickSide = "old" | "new";
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
bizMonitorUpdateTemplate,
|
bizMonitorUpdateTemplate,
|
||||||
formatErr,
|
formatErr,
|
||||||
} from "../../services/api";
|
} from "../../services/api";
|
||||||
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./cutoverDataCache";
|
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./lib/cutoverDataCache";
|
||||||
|
|
||||||
type MetricField = { name: string; display_name?: string };
|
type MetricField = { name: string; display_name?: string };
|
||||||
type MetricSchema = { metric_id: string; fields: MetricField[] };
|
type MetricSchema = { metric_id: string; fields: MetricField[] };
|
||||||
|
|
@ -43,8 +43,8 @@ import type { CliTargetItem } from "../../types";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { writeClipboardText } from "../../utils/clipboard";
|
import { writeClipboardText } from "../../utils/clipboard";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./cutoverDataCache";
|
import { cutoverCachedGet, cutoverCachedGetSWR, invalidateCutoverCache } from "./lib/cutoverDataCache";
|
||||||
import { jobChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
|
import { jobChipColor, NmStatusChip, sourceChipColor } from "../../components/NmChips";
|
||||||
|
|
||||||
type TaskRow = {
|
type TaskRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -6,8 +6,8 @@ export function prefetchCutoverPages(): void {
|
||||||
if (started) return;
|
if (started) return;
|
||||||
started = true;
|
started = true;
|
||||||
// Fire-and-forget dynamic imports (same modules as App.tsx lazy()).
|
// Fire-and-forget dynamic imports (same modules as App.tsx lazy()).
|
||||||
void import("./BizStatePage");
|
void import("../BizStatePage");
|
||||||
void import("./BizComparePage");
|
void import("../BizComparePage");
|
||||||
void import("./BizMigrationPage");
|
void import("../BizMigrationPage");
|
||||||
void import("./BizMonitorTemplatesPage");
|
void import("../BizMonitorTemplatesPage");
|
||||||
}
|
}
|
||||||
|
|
@ -18,28 +18,28 @@ import {
|
||||||
downloadManagedNeImportTemplate,
|
downloadManagedNeImportTemplate,
|
||||||
syncUmeManagedNe,
|
syncUmeManagedNe,
|
||||||
type ManagedNeStats,
|
type ManagedNeStats,
|
||||||
} from "../services/api";
|
} from "../../services/api";
|
||||||
import { HelpHint } from "../components/HelpHint";
|
import { HelpHint } from "../../components/HelpHint";
|
||||||
import { HopProxyFields, emptyHopProxyFields, type HopProxyFieldsState } from "../components/HopProxyFields";
|
import { HopProxyFields, emptyHopProxyFields, type HopProxyFieldsState } from "./components/HopProxyFields";
|
||||||
import { AppModalShell } from "../components/ui/AppModalShell";
|
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||||
import { FieldSelect } from "../components/ui/FieldSelect";
|
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||||
import { queryKeys } from "../constants/queryKeys";
|
import { queryKeys } from "../../constants/queryKeys";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useToast } from "../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import type { ManagedNeItem } from "../types";
|
import type { ManagedNeItem } from "../../types";
|
||||||
import { pageCount } from "../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
import { openOrFocusModule } from "../utils/moduleWindows";
|
import { openOrFocusModule } from "../../utils/moduleWindows";
|
||||||
import { ManagedNeFormDialog } from "./managedNe/ManagedNeFormDialog";
|
import { ManagedNeFormDialog } from "./components/ManagedNeFormDialog";
|
||||||
import { ManagedNeConnectDetailDialog } from "./managedNe/ManagedNeConnectDetailDialog";
|
import { ManagedNeConnectDetailDialog } from "./components/ManagedNeConnectDetailDialog";
|
||||||
import { connectStatusClass } from "./managedNe/connectStatus";
|
import { connectStatusClass } from "./lib/connectStatus";
|
||||||
import {
|
import {
|
||||||
deviceTypeForVendor,
|
deviceTypeForVendor,
|
||||||
emptyManagedNeForm,
|
emptyManagedNeForm,
|
||||||
managedSourceKey,
|
managedSourceKey,
|
||||||
type ManagedNeFormState,
|
type ManagedNeFormState,
|
||||||
} from "./managedNe/formState";
|
} from "./lib/formState";
|
||||||
|
|
||||||
/** Hide UME→managed sync/delete controls until needed again. APIs remain available. */
|
/** Hide UME→managed sync/delete controls until needed again. APIs remain available. */
|
||||||
const SHOW_UME_MANAGED_SYNC = false;
|
const SHOW_UME_MANAGED_SYNC = false;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Input, Label, TextField } from "@heroui/react";
|
import { Input, Label, TextField } from "@heroui/react";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { FieldSelect } from "./ui/FieldSelect";
|
import { FieldSelect } from "../../../components/ui/FieldSelect";
|
||||||
import {
|
import {
|
||||||
HOP_VENDORS,
|
HOP_VENDORS,
|
||||||
defaultHopTemplate,
|
defaultHopTemplate,
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
patchHopVendorChange,
|
patchHopVendorChange,
|
||||||
type HopTargetAuthMode,
|
type HopTargetAuthMode,
|
||||||
type HopVendor,
|
type HopVendor,
|
||||||
} from "../utils/hopProxy";
|
} from "../lib/hopProxy";
|
||||||
|
|
||||||
export type HopProxyFieldsState = {
|
export type HopProxyFieldsState = {
|
||||||
hop_vendor: HopVendor;
|
hop_vendor: HopVendor;
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Alert, Button, Chip, Modal } from "@heroui/react";
|
import { Alert, Button, Chip, Modal } from "@heroui/react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { connectTestManagedNe } from "../../services/api";
|
import { connectTestManagedNe } from "../../../services/api";
|
||||||
import { AppModalShell } from "../../components/ui/AppModalShell";
|
import { AppModalShell } from "../../../components/ui/AppModalShell";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../../utils/time";
|
||||||
import type { ManagedNeItem } from "../../types";
|
import type { ManagedNeItem } from "../../../types";
|
||||||
import { connectPillLevel } from "./connectStatus";
|
import { connectPillLevel } from "../lib/connectStatus";
|
||||||
|
|
||||||
export type ManagedNeConnectDetailDialogProps = {
|
export type ManagedNeConnectDetailDialogProps = {
|
||||||
row: ManagedNeItem | null;
|
row: ManagedNeItem | null;
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { Alert, Button, Checkbox, Input, Label, Modal, TextField } from "@heroui/react";
|
import { Alert, Button, Checkbox, Input, Label, Modal, TextField } from "@heroui/react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { createManagedNe, fetchManagedNeMeta, updateManagedNe } from "../../services/api";
|
import { createManagedNe, fetchManagedNeMeta, updateManagedNe } from "../../../services/api";
|
||||||
import { HopProxyFields } from "../../components/HopProxyFields";
|
import { HopProxyFields } from "./HopProxyFields";
|
||||||
import { AppModalShell } from "../../components/ui/AppModalShell";
|
import { AppModalShell } from "../../../components/ui/AppModalShell";
|
||||||
import { FieldSelect } from "../../components/ui/FieldSelect";
|
import { FieldSelect } from "../../../components/ui/FieldSelect";
|
||||||
import { queryKeys } from "../../constants/queryKeys";
|
import { queryKeys } from "../../../constants/queryKeys";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { patchHopVendorChange } from "../../utils/hopProxy";
|
import { patchHopVendorChange } from "../lib/hopProxy";
|
||||||
import type { ManagedNeItem } from "../../types";
|
import type { ManagedNeItem } from "../../../types";
|
||||||
import {
|
import {
|
||||||
applyHopTemplate,
|
applyHopTemplate,
|
||||||
buildManagedNeSaveBody,
|
buildManagedNeSaveBody,
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
formFromManagedNe,
|
formFromManagedNe,
|
||||||
allowsOpenExecPolicy,
|
allowsOpenExecPolicy,
|
||||||
type ManagedNeFormState,
|
type ManagedNeFormState,
|
||||||
} from "./formState";
|
} from "../lib/formState";
|
||||||
|
|
||||||
export type ManagedNeFormDialogProps = {
|
export type ManagedNeFormDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -2,8 +2,8 @@ import {
|
||||||
defaultHopTemplate,
|
defaultHopTemplate,
|
||||||
isAutoHopTemplate,
|
isAutoHopTemplate,
|
||||||
type HopVendor,
|
type HopVendor,
|
||||||
} from "../../utils/hopProxy";
|
} from "./hopProxy";
|
||||||
import type { ManagedNeItem } from "../../types";
|
import type { ManagedNeItem } from "../../../types";
|
||||||
|
|
||||||
export type ManagedNeFormState = {
|
export type ManagedNeFormState = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -38,7 +38,7 @@ import {
|
||||||
jobChipColor,
|
jobChipColor,
|
||||||
NmStatusChip,
|
NmStatusChip,
|
||||||
sourceChipColor,
|
sourceChipColor,
|
||||||
} from "./nmChips";
|
} from "../../components/NmChips";
|
||||||
|
|
||||||
const POLL_MS = 2500;
|
const POLL_MS = 2500;
|
||||||
const TARGET_PAGE_SIZE = 10;
|
const TARGET_PAGE_SIZE = 10;
|
||||||
|
|
@ -13,7 +13,7 @@ import type { UmeAlarmItem } from "../../types";
|
||||||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
import { NmStatusChip, severityChipColor } from "./nmChips";
|
import { NmStatusChip, severityChipColor } from "../../components/NmChips";
|
||||||
|
|
||||||
/** Current UME alarms query view for Network Management. */
|
/** Current UME alarms query view for Network Management. */
|
||||||
export function NetworkAlarmsPage() {
|
export function NetworkAlarmsPage() {
|
||||||
|
|
@ -20,7 +20,7 @@ import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { openNewModuleWindow } from "../../utils/moduleWindows";
|
import { openNewModuleWindow } from "../../utils/moduleWindows";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
import { NmStatusChip, sourceChipColor } from "./nmChips";
|
import { NmStatusChip, sourceChipColor } from "../../components/NmChips";
|
||||||
|
|
||||||
function fmtBytes(n: number): string {
|
function fmtBytes(n: number): string {
|
||||||
if (!n) return "0 B";
|
if (!n) return "0 B";
|
||||||
|
|
@ -12,7 +12,7 @@ import type { CliTargetItem } from "../../types";
|
||||||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { openNewModuleWindow } from "../../utils/moduleWindows";
|
import { openNewModuleWindow } from "../../utils/moduleWindows";
|
||||||
import { connectChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
|
import { connectChipColor, NmStatusChip, sourceChipColor } from "../../components/NmChips";
|
||||||
|
|
||||||
function openWebcrt(row: CliTargetItem) {
|
function openWebcrt(row: CliTargetItem) {
|
||||||
const path =
|
const path =
|
||||||
|
|
@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
||||||
import { NETWORK_NAV, type NetworkNavGroupId } from "../../config/networkNav";
|
import { NETWORK_NAV, type NetworkNavGroupId } from "../../config/networkNav";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { prefetchCutoverPages } from "./cutoverPrefetch";
|
import { prefetchCutoverPages } from "../cutover/lib/cutoverPrefetch";
|
||||||
|
|
||||||
function groupContainsPath(groupId: NetworkNavGroupId, pathname: string): boolean {
|
function groupContainsPath(groupId: NetworkNavGroupId, pathname: string): boolean {
|
||||||
const group = NETWORK_NAV.find((g) => g.id === groupId);
|
const group = NETWORK_NAV.find((g) => g.id === groupId);
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
import { queryKeys } from "../../constants/queryKeys";
|
import { queryKeys } from "../../constants/queryKeys";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useToast } from "../../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import { openPortTrafficBoardWindow } from "../../utils/portTrafficBoardWindow";
|
import { openPortTrafficBoardWindow } from "./lib/portTrafficBoardWindow";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
|
|
||||||
export function PortTrafficBoardListPage() {
|
export function PortTrafficBoardListPage() {
|
||||||
|
|
@ -36,7 +36,7 @@ import type {
|
||||||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
import { jobChipColor, NmStatusChip } from "./nmChips";
|
import { jobChipColor, NmStatusChip } from "../../components/NmChips";
|
||||||
|
|
||||||
const POLL_MS = 5000;
|
const POLL_MS = 5000;
|
||||||
const TARGET_PAGE_SIZE = 10;
|
const TARGET_PAGE_SIZE = 10;
|
||||||
|
|
@ -16,8 +16,8 @@ import { queryKeys } from "../../constants/queryKeys";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useToast } from "../../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import type { PortTrafficBoardPanel, PortTrafficBoardPanelIn } from "../../types";
|
import type { PortTrafficBoardPanel, PortTrafficBoardPanelIn } from "../../types";
|
||||||
import { PortTrafficBoardPanelCell } from "./PortTrafficBoardPanelCell";
|
import { PortTrafficBoardPanelCell } from "./components/PortTrafficBoardPanelCell";
|
||||||
import type { WallYMode } from "./PortTrafficWall";
|
import type { WallYMode } from "./components/PortTrafficWall";
|
||||||
|
|
||||||
type BaselineMode = "off" | "shift" | "day" | "week" | "custom";
|
type BaselineMode = "off" | "shift" | "day" | "week" | "custom";
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Button } from "@heroui/react";
|
import { Button } from "@heroui/react";
|
||||||
import { useQuery, keepPreviousData } from "@tanstack/react-query";
|
import { useQuery, keepPreviousData } from "@tanstack/react-query";
|
||||||
import { fetchPortTrafficCompare } from "../../services/api";
|
import { fetchPortTrafficCompare } from "../../../services/api";
|
||||||
import { queryKeys } from "../../constants/queryKeys";
|
import { queryKeys } from "../../../constants/queryKeys";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import type { PortTrafficBoardPanel } from "../../types";
|
import type { PortTrafficBoardPanel } from "../../../types";
|
||||||
import { PortTrafficWall, type WallYMode } from "./PortTrafficWall";
|
import { PortTrafficWall, type WallYMode } from "./PortTrafficWall";
|
||||||
|
|
||||||
const POLL_MS = 5000;
|
const POLL_MS = 5000;
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import uPlot from "uplot";
|
import uPlot from "uplot";
|
||||||
import "uplot/dist/uPlot.min.css";
|
import "uplot/dist/uPlot.min.css";
|
||||||
import type { PortTrafficSamplePoint, PortTrafficTarget } from "../../types";
|
import type { PortTrafficSamplePoint, PortTrafficTarget } from "../../../types";
|
||||||
import { formatSystemTime, parseApiTime } from "../../utils/time";
|
import { formatSystemTime, parseApiTime } from "../../../utils/time";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
|
|
||||||
export type WallYMode = "auto" | "current" | "util";
|
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";
|
export const PORT_TRAFFIC_WALL_MODULE_ID = "port-traffic-wall";
|
||||||
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Button } from "@heroui/react";
|
import { Button } from "@heroui/react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { HelpHint } from "../components/HelpHint";
|
import { HelpHint } from "../../components/HelpHint";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useToast } from "../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import { apiDelete, apiGet, apiPost } from "../services/api";
|
import { apiDelete, apiGet, apiPost } from "../../services/api";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
|
|
||||||
type SessionRow = {
|
type SessionRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -10,16 +10,16 @@ import {
|
||||||
SHOW_ALARM_OVERLAY_KEY,
|
SHOW_ALARM_OVERLAY_KEY,
|
||||||
SHOW_LEVEL_COLORS_KEY,
|
SHOW_LEVEL_COLORS_KEY,
|
||||||
WORLD_MAP_ENABLED,
|
WORLD_MAP_ENABLED,
|
||||||
} from "./topology/constants";
|
} from "./lib/constants";
|
||||||
import type { CtxMenu, PaletteItem, PaletteSource } from "./topology/pageTypes";
|
import type { CtxMenu, PaletteItem, PaletteSource } from "./lib/pageTypes";
|
||||||
import { clampSidebarWidth, loadSidebarWidth, saveSidebarWidth } from "./topology/sidebarLayout";
|
import { clampSidebarWidth, loadSidebarWidth, saveSidebarWidth } from "./lib/sidebarLayout";
|
||||||
import {
|
import {
|
||||||
canvasBgLuminance,
|
canvasBgLuminance,
|
||||||
canvasFloatChromeVars,
|
canvasFloatChromeVars,
|
||||||
findViewInRegion,
|
findViewInRegion,
|
||||||
isWorldFlatViewName,
|
isWorldFlatViewName,
|
||||||
regionDisplayName,
|
regionDisplayName,
|
||||||
} from "./topology/treeUtils";
|
} from "./lib/treeUtils";
|
||||||
import {
|
import {
|
||||||
loadAutoLayoutAfterDiscover,
|
loadAutoLayoutAfterDiscover,
|
||||||
loadBoolFlag,
|
loadBoolFlag,
|
||||||
|
|
@ -29,7 +29,7 @@ import {
|
||||||
persistBoolFlag,
|
persistBoolFlag,
|
||||||
type LabelColors,
|
type LabelColors,
|
||||||
type VendorColors,
|
type VendorColors,
|
||||||
} from "./topology/displayPrefs";
|
} from "./lib/displayPrefs";
|
||||||
import {
|
import {
|
||||||
BUILTIN_EDGE_DEFAULTS,
|
BUILTIN_EDGE_DEFAULTS,
|
||||||
type EdgeDefaultStyle,
|
type EdgeDefaultStyle,
|
||||||
|
|
@ -40,24 +40,24 @@ import {
|
||||||
persistEdgeDefaults,
|
persistEdgeDefaults,
|
||||||
resolveEdgeStyle,
|
resolveEdgeStyle,
|
||||||
withEdgeVisual,
|
withEdgeVisual,
|
||||||
} from "./topology/edgeStyle";
|
} from "./lib/edgeStyle";
|
||||||
import { applyViewGraph, flowToPositions } from "./topology/graphFlow";
|
import { applyViewGraph, flowToPositions } from "./lib/graphFlow";
|
||||||
import { isLocalPendingEdgeId, newLocalEdgeId } from "./topology/idUtils";
|
import { isLocalPendingEdgeId, newLocalEdgeId } from "./lib/idUtils";
|
||||||
import { fuzzyIncludes, isPlaceholderSource, nodeMatchesQuery } from "./topology/searchUtils";
|
import { fuzzyIncludes, isPlaceholderSource, nodeMatchesQuery } from "./lib/searchUtils";
|
||||||
import { TopologyDiscoverPanel } from "./topology/TopologyDiscoverPanel";
|
import { TopologyDiscoverPanel } from "./components/TopologyDiscoverPanel";
|
||||||
import { TopologyHexBrowser } from "./topology/TopologyHexBrowser";
|
import { TopologyHexBrowser } from "./components/TopologyHexBrowser";
|
||||||
import { TopologyToolbar } from "./topology/TopologyToolbar";
|
import { TopologyToolbar } from "./components/TopologyToolbar";
|
||||||
import { TopologyViewTools } from "./topology/TopologyViewTools";
|
import { TopologyViewTools } from "./components/TopologyViewTools";
|
||||||
import { TopologySidebar } from "./topology/TopologySidebar";
|
import { TopologySidebar } from "./components/TopologySidebar";
|
||||||
import { TopologyModals } from "./topology/TopologyModals";
|
import { TopologyModals } from "./components/TopologyModals";
|
||||||
import { TopologyCtxMenu } from "./topology/TopologyCtxMenu";
|
import { TopologyCtxMenu } from "./components/TopologyCtxMenu";
|
||||||
import { useTopologyHistory } from "./topology/hooks/useTopologyHistory";
|
import { useTopologyHistory } from "./hooks/useTopologyHistory";
|
||||||
import { useTopologyDiscover } from "./topology/hooks/useTopologyDiscover";
|
import { useTopologyDiscover } from "./hooks/useTopologyDiscover";
|
||||||
import { useTopologyTreeNav } from "./topology/hooks/useTopologyTreeNav";
|
import { useTopologyTreeNav } from "./hooks/useTopologyTreeNav";
|
||||||
import { useTopologyCanvas } from "./topology/hooks/useTopologyCanvas";
|
import { useTopologyCanvas } from "./hooks/useTopologyCanvas";
|
||||||
import { useTopologyCreateNe } from "./topology/hooks/useTopologyCreateNe";
|
import { useTopologyCreateNe } from "./hooks/useTopologyCreateNe";
|
||||||
import { useTopologyConnectTest } from "./topology/hooks/useTopologyConnectTest";
|
import { useTopologyConnectTest } from "./hooks/useTopologyConnectTest";
|
||||||
import { useTopologyAlarmOverlay } from "./topology/hooks/useTopologyAlarmOverlay";
|
import { useTopologyAlarmOverlay } from "./hooks/useTopologyAlarmOverlay";
|
||||||
import {
|
import {
|
||||||
lazy,
|
lazy,
|
||||||
Suspense,
|
Suspense,
|
||||||
|
|
@ -91,33 +91,33 @@ import {
|
||||||
patchTopologyPositions,
|
patchTopologyPositions,
|
||||||
removeTopologyViewNodes,
|
removeTopologyViewNodes,
|
||||||
applyUmeTopologyToFabric,
|
applyUmeTopologyToFabric,
|
||||||
} from "../services/api";
|
} from "../../services/api";
|
||||||
import { queryKeys } from "../constants/queryKeys";
|
import { queryKeys } from "../../constants/queryKeys";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useToast } from "../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import { formatErr } from "../services/api";
|
import { formatErr } from "../../services/api";
|
||||||
import { openOrFocusModule } from "../utils/moduleWindows";
|
import { openOrFocusModule } from "../../utils/moduleWindows";
|
||||||
import type {
|
import type {
|
||||||
FabricNodeSearchHit,
|
FabricNodeSearchHit,
|
||||||
TopologyViewItem,
|
TopologyViewItem,
|
||||||
} from "../types";
|
} from "../../types";
|
||||||
import { alignNodes } from "./topology/layoutGraph";
|
import { alignNodes } from "./lib/layoutGraph";
|
||||||
import {
|
import {
|
||||||
formatPortPairLabel,
|
formatPortPairLabel,
|
||||||
isAggregateEdgeId,
|
isAggregateEdgeId,
|
||||||
physicalIdsForDisplayEdge,
|
physicalIdsForDisplayEdge,
|
||||||
} from "./topology/linkDisplay";
|
} from "./lib/linkDisplay";
|
||||||
import { behaviorForMode, toolModeFromKey, type ToolMode } from "./topology/toolMode";
|
import { behaviorForMode, toolModeFromKey, type ToolMode } from "./lib/toolMode";
|
||||||
import type { NeNodeData } from "./topology/TopologyReactFlowView";
|
import type { NeNodeData } from "./components/TopologyReactFlowView";
|
||||||
import {
|
import {
|
||||||
buildExportGraph,
|
buildExportGraph,
|
||||||
exportTopologySvg,
|
exportTopologySvg,
|
||||||
exportTopologyXml,
|
exportTopologyXml,
|
||||||
} from "./topology/exportTopology";
|
} from "./lib/exportTopology";
|
||||||
|
|
||||||
/** xyflow + canvas nodes — only loaded when a map canvas is open. */
|
/** 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() {
|
export function TopologyPage() {
|
||||||
const { t, locale } = useI18n();
|
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 { createPortal } from "react-dom";
|
||||||
import { Button } from "@heroui/react";
|
import { Button } from "@heroui/react";
|
||||||
import type { Edge, Node } from "@xyflow/react";
|
import type { Edge, Node } from "@xyflow/react";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import type { CtxMenu } from "./pageTypes";
|
import type { CtxMenu } from "../lib/pageTypes";
|
||||||
import type { EdgeStyleData } from "./edgeStyle";
|
import type { EdgeStyleData } from "../lib/edgeStyle";
|
||||||
import { formatPortPairLabel } from "./linkDisplay";
|
import { formatPortPairLabel } from "../lib/linkDisplay";
|
||||||
import { isPlaceholderSource } from "./searchUtils";
|
import { isPlaceholderSource } from "../lib/searchUtils";
|
||||||
import type { NeNodeData } from "./TopologyReactFlowView";
|
import type { NeNodeData } from "./TopologyReactFlowView";
|
||||||
|
|
||||||
export type ResolvedEdgeStyle = {
|
export type ResolvedEdgeStyle = {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Button } from "@heroui/react";
|
import { Button } from "@heroui/react";
|
||||||
import type { TopologyDiscoverOut } from "../../types";
|
import type { TopologyDiscoverOut } from "../../../types";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { openOrFocusModule } from "../../utils/moduleWindows";
|
import { openOrFocusModule } from "../../../utils/moduleWindows";
|
||||||
|
|
||||||
export type DiscoverProgress = {
|
export type DiscoverProgress = {
|
||||||
index: number;
|
index: number;
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { Button } from "@heroui/react";
|
import { Button } from "@heroui/react";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../types";
|
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../../types";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import {
|
import {
|
||||||
displayViewName,
|
displayViewName,
|
||||||
folderNeCount,
|
folderNeCount,
|
||||||
formatNeCount,
|
formatNeCount,
|
||||||
isUmeWorldContainer,
|
isUmeWorldContainer,
|
||||||
regionDisplayName,
|
regionDisplayName,
|
||||||
} from "./treeUtils";
|
} from "../lib/treeUtils";
|
||||||
import { LayerGlyph, RegionGlyph } from "./topologyIcons";
|
import { LayerGlyph, RegionGlyph } from "./topologyIcons";
|
||||||
|
|
||||||
export type UmeWorldHexModules = {
|
export type UmeWorldHexModules = {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { DragEvent } from "react";
|
import type { DragEvent } from "react";
|
||||||
import type { TopologyOutsidePeer } from "../../types";
|
import type { TopologyOutsidePeer } from "../../../types";
|
||||||
import type { PaletteItem, PaletteSource } from "./pageTypes";
|
import type { PaletteItem, PaletteSource } from "../lib/pageTypes";
|
||||||
import { AddNePaletteDialog } from "./modals/AddNePaletteDialog";
|
import { AddNePaletteDialog } from "./modals/AddNePaletteDialog";
|
||||||
import { CreateNeModeDialog } from "./modals/CreateNeModeDialog";
|
import { CreateNeModeDialog } from "./modals/CreateNeModeDialog";
|
||||||
import { NewRootDialog } from "./modals/NewRootDialog";
|
import { NewRootDialog } from "./modals/NewRootDialog";
|
||||||
|
|
@ -9,10 +9,10 @@ import {
|
||||||
type PlaceholderCreateDialogState,
|
type PlaceholderCreateDialogState,
|
||||||
} from "./modals/PlaceholderCreateDialog";
|
} from "./modals/PlaceholderCreateDialog";
|
||||||
import { OutsidePeersDialog } from "./modals/OutsidePeersDialog";
|
import { OutsidePeersDialog } from "./modals/OutsidePeersDialog";
|
||||||
import { ManagedNeFormDialog } from "../managedNe/ManagedNeFormDialog";
|
import { ManagedNeFormDialog } from "../../managed-ne/components/ManagedNeFormDialog";
|
||||||
import { ManagedNeConnectDetailDialog } from "../managedNe/ManagedNeConnectDetailDialog";
|
import { ManagedNeConnectDetailDialog } from "../../managed-ne/components/ManagedNeConnectDetailDialog";
|
||||||
import type { ManagedNeItem } from "../../types";
|
import type { ManagedNeItem } from "../../../types";
|
||||||
import type { ManagedNeFormState } from "../managedNe/formState";
|
import type { ManagedNeFormState } from "../../managed-ne/lib/formState";
|
||||||
|
|
||||||
export type { PlaceholderCreateDialogState as CreateNeDialogState };
|
export type { PlaceholderCreateDialogState as CreateNeDialogState };
|
||||||
|
|
||||||
|
|
@ -24,13 +24,13 @@ import {
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { WorldScatterLayer, type WorldScatterPoint } from "./WorldScatterLayer";
|
import { WorldScatterLayer, type WorldScatterPoint } from "./WorldScatterLayer";
|
||||||
import { ParallelEdge } from "./ParallelEdge";
|
import { ParallelEdge } from "./ParallelEdge";
|
||||||
import { isAggregateEdgeId } from "./linkDisplay";
|
import { isAggregateEdgeId } from "../lib/linkDisplay";
|
||||||
import type { ToolMode, ToolModeBehavior } from "./toolMode";
|
import type { ToolMode, ToolModeBehavior } from "../lib/toolMode";
|
||||||
import type { TopologyViewRole, TopologyWorldTransform } from "../../types";
|
import type { TopologyViewRole, TopologyWorldTransform } from "../../../types";
|
||||||
import { isPlaceholderSource } from "./searchUtils";
|
import { isPlaceholderSource } from "../lib/searchUtils";
|
||||||
import { nodeIconTone } from "./vendorTone";
|
import { nodeIconTone } from "../lib/vendorTone";
|
||||||
import { levelToneKey } from "./levelTone";
|
import { levelToneKey } from "../lib/levelTone";
|
||||||
import type { NodeAlarmSummary } from "./alarmOverlay";
|
import type { NodeAlarmSummary } from "../lib/alarmOverlay";
|
||||||
|
|
||||||
export { isPlaceholderSource };
|
export { isPlaceholderSource };
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type { MouseEvent as ReactMouseEvent, RefObject } from "react";
|
import type { MouseEvent as ReactMouseEvent, RefObject } from "react";
|
||||||
import { Button, Input } from "@heroui/react";
|
import { Button, Input } from "@heroui/react";
|
||||||
import type { FabricNodeSearchHit, TopologyTreeFolderItem } from "../../types";
|
import type { FabricNodeSearchHit, TopologyTreeFolderItem } from "../../../types";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { WORLD_MAP_ENABLED } from "./constants";
|
import { WORLD_MAP_ENABLED } from "../lib/constants";
|
||||||
import { isWorldFlatViewName } from "./treeUtils";
|
import { isWorldFlatViewName } from "../lib/treeUtils";
|
||||||
import { PlusIcon, SidebarFoldIcon } from "./topologyIcons";
|
import { PlusIcon, SidebarFoldIcon } from "./topologyIcons";
|
||||||
import {
|
import {
|
||||||
TopologyTreeNavFolder,
|
TopologyTreeNavFolder,
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Button, Dropdown } from "@heroui/react";
|
import { Button, Dropdown } from "@heroui/react";
|
||||||
import { Fragment, type Key, type RefObject, useMemo } from "react";
|
import { Fragment, type Key, type RefObject, useMemo } from "react";
|
||||||
import type { Edge, Node, ReactFlowInstance } from "@xyflow/react";
|
import type { Edge, Node, ReactFlowInstance } from "@xyflow/react";
|
||||||
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../types";
|
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../../types";
|
||||||
import { HelpHint } from "../../components/HelpHint";
|
import { HelpHint } from "../../../components/HelpHint";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { displayViewName, isRegionCanvasFolder, regionDisplayName } from "./treeUtils";
|
import { displayViewName, isRegionCanvasFolder, regionDisplayName } from "../lib/treeUtils";
|
||||||
import type { NeNodeData } from "./TopologyReactFlowView";
|
import type { NeNodeData } from "./TopologyReactFlowView";
|
||||||
import type { ToolMode } from "./toolMode";
|
import type { ToolMode } from "../lib/toolMode";
|
||||||
|
|
||||||
export type TopologyToolbarProps = {
|
export type TopologyToolbarProps = {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../types";
|
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../../types";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { WORLD_MAP_ENABLED } from "./constants";
|
import { WORLD_MAP_ENABLED } from "../lib/constants";
|
||||||
import {
|
import {
|
||||||
displayViewName,
|
displayViewName,
|
||||||
folderNeCount,
|
folderNeCount,
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
isWorldDrillFolder,
|
isWorldDrillFolder,
|
||||||
isWorldFlatViewName,
|
isWorldFlatViewName,
|
||||||
regionDisplayName,
|
regionDisplayName,
|
||||||
} from "./treeUtils";
|
} from "../lib/treeUtils";
|
||||||
import {
|
import {
|
||||||
ChevronIcon,
|
ChevronIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
|
|
@ -3,10 +3,10 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||||
import type { RefObject } from "react";
|
import type { RefObject } from "react";
|
||||||
import type { Node } from "@xyflow/react";
|
import type { Node } from "@xyflow/react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { queryKeys } from "../../constants/queryKeys";
|
import { queryKeys } from "../../../constants/queryKeys";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { useToast } from "../../hooks/useToast";
|
import { useToast } from "../../../hooks/useToast";
|
||||||
import { updateLldpCollectPolicy } from "../../services/api";
|
import { updateLldpCollectPolicy } from "../../../services/api";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CANVAS_BG,
|
DEFAULT_CANVAS_BG,
|
||||||
DISCOVER_AUTO_ADD_KEY,
|
DISCOVER_AUTO_ADD_KEY,
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
SHOW_CONNECT_STATUS_KEY,
|
SHOW_CONNECT_STATUS_KEY,
|
||||||
SHOW_ALARM_OVERLAY_KEY,
|
SHOW_ALARM_OVERLAY_KEY,
|
||||||
SHOW_LEVEL_COLORS_KEY,
|
SHOW_LEVEL_COLORS_KEY,
|
||||||
} from "./constants";
|
} from "../lib/constants";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LABEL_COLORS,
|
DEFAULT_LABEL_COLORS,
|
||||||
DEFAULT_VENDOR_COLORS,
|
DEFAULT_VENDOR_COLORS,
|
||||||
|
|
@ -33,9 +33,9 @@ import {
|
||||||
persistVendorColors,
|
persistVendorColors,
|
||||||
type LabelColors,
|
type LabelColors,
|
||||||
type VendorColors,
|
type VendorColors,
|
||||||
} from "./displayPrefs";
|
} from "../lib/displayPrefs";
|
||||||
import type { EdgeDefaultStyle, EdgeDefaults, EdgeLineStyle, EdgeSourceKind } from "./edgeStyle";
|
import type { EdgeDefaultStyle, EdgeDefaults, EdgeLineStyle, EdgeSourceKind } from "../lib/edgeStyle";
|
||||||
import type { alignNodes } from "./layoutGraph";
|
import type { alignNodes } from "../lib/layoutGraph";
|
||||||
import type { NeNodeData } from "./TopologyReactFlowView";
|
import type { NeNodeData } from "./TopologyReactFlowView";
|
||||||
|
|
||||||
export type AlignKind = Parameters<typeof alignNodes>[2];
|
export type AlignKind = Parameters<typeof alignNodes>[2];
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { DragEvent } from "react";
|
import type { DragEvent } from "react";
|
||||||
import { Button, Chip, Input, Modal } from "@heroui/react";
|
import { Button, Chip, Input, Modal } from "@heroui/react";
|
||||||
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
|
import { TopoModalShell } from "../TopoModalShell";
|
||||||
import { useI18n } from "../../../i18n";
|
import { useI18n } from "../../../../i18n";
|
||||||
import type { PaletteItem, PaletteSource } from "../pageTypes";
|
import type { PaletteItem, PaletteSource } from "../../lib/pageTypes";
|
||||||
|
|
||||||
export type AddNePaletteDialogProps = {
|
export type AddNePaletteDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button, Modal } from "@heroui/react";
|
import { Button, Modal } from "@heroui/react";
|
||||||
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
|
import { TopoModalShell } from "../TopoModalShell";
|
||||||
import { useI18n } from "../../../i18n";
|
import { useI18n } from "../../../../i18n";
|
||||||
|
|
||||||
export type CreateNeModeDialogProps = {
|
export type CreateNeModeDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button, Input, Label, Modal, TextField } from "@heroui/react";
|
import { Button, Input, Label, Modal, TextField } from "@heroui/react";
|
||||||
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
|
import { TopoModalShell } from "../TopoModalShell";
|
||||||
import { useI18n } from "../../../i18n";
|
import { useI18n } from "../../../../i18n";
|
||||||
|
|
||||||
export type NewRootDialogProps = {
|
export type NewRootDialogProps = {
|
||||||
dialog: { name: string } | null;
|
dialog: { name: string } | null;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Button, Chip, Input, Modal } from "@heroui/react";
|
import { Button, Chip, Input, Modal } from "@heroui/react";
|
||||||
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
|
import { TopoModalShell } from "../TopoModalShell";
|
||||||
import type { TopologyOutsidePeer } from "../../../types";
|
import type { TopologyOutsidePeer } from "../../../../types";
|
||||||
import { useI18n } from "../../../i18n";
|
import { useI18n } from "../../../../i18n";
|
||||||
|
|
||||||
export type OutsidePeersDialogProps = {
|
export type OutsidePeersDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button, Input, Label, Modal, TextField } from "@heroui/react";
|
import { Button, Input, Label, Modal, TextField } from "@heroui/react";
|
||||||
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
|
import { TopoModalShell } from "../TopoModalShell";
|
||||||
import { useI18n } from "../../../i18n";
|
import { useI18n } from "../../../../i18n";
|
||||||
|
|
||||||
export type PlaceholderCreateDialogState = {
|
export type PlaceholderCreateDialogState = {
|
||||||
flowX: number;
|
flowX: number;
|
||||||
|
|
@ -12,8 +12,8 @@ import {
|
||||||
normalizeHostKey,
|
normalizeHostKey,
|
||||||
worseSeverity,
|
worseSeverity,
|
||||||
type NodeAlarmSummary,
|
type NodeAlarmSummary,
|
||||||
} from "../alarmOverlay";
|
} from "../lib/alarmOverlay";
|
||||||
import type { NeNodeData } from "../TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
|
|
||||||
function accumulate(
|
function accumulate(
|
||||||
map: Map<string, NodeAlarmSummary>,
|
map: Map<string, NodeAlarmSummary>,
|
||||||
|
|
@ -9,25 +9,25 @@ import {
|
||||||
WORLD_LOCATE_HALF,
|
WORLD_LOCATE_HALF,
|
||||||
WORLD_LOCATE_ZOOM,
|
WORLD_LOCATE_ZOOM,
|
||||||
WORLD_MAP_ENABLED,
|
WORLD_MAP_ENABLED,
|
||||||
} from "../constants";
|
} from "../lib/constants";
|
||||||
import type { EdgeDefaults, EdgeStyleData } from "../edgeStyle";
|
import type { EdgeDefaults, EdgeStyleData } from "../lib/edgeStyle";
|
||||||
import { withEdgeVisual } from "../edgeStyle";
|
import { withEdgeVisual } from "../lib/edgeStyle";
|
||||||
import { graphFingerprint, graphToFlow, mergeOverlayIntoNodes, overlayFingerprint } from "../graphFlow";
|
import { graphFingerprint, graphToFlow, mergeOverlayIntoNodes, overlayFingerprint } from "../lib/graphFlow";
|
||||||
import {
|
import {
|
||||||
buildLinkDisplayEdges,
|
buildLinkDisplayEdges,
|
||||||
type LinkMember,
|
type LinkMember,
|
||||||
} from "../linkDisplay";
|
} from "../lib/linkDisplay";
|
||||||
import type { NeNodeData } from "../TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
import { TOPO_HANDLE_X, TOPO_HANDLE_Y } from "../topoGeometry";
|
import { TOPO_HANDLE_X, TOPO_HANDLE_Y } from "../lib/topoGeometry";
|
||||||
import type { ToolMode } from "../toolMode";
|
import type { ToolMode } from "../lib/toolMode";
|
||||||
import {
|
import {
|
||||||
displayViewName,
|
displayViewName,
|
||||||
isWorldFlatViewName,
|
isWorldFlatViewName,
|
||||||
worldDisplayBounds,
|
worldDisplayBounds,
|
||||||
worldVisualLodFromZoom,
|
worldVisualLodFromZoom,
|
||||||
} from "../treeUtils";
|
} from "../lib/treeUtils";
|
||||||
import { mergeFlatWorldGraph } from "../worldGraph";
|
import { mergeFlatWorldGraph } from "../lib/worldGraph";
|
||||||
import type { HistorySnap } from "../pageTypes";
|
import type { HistorySnap } from "../lib/pageTypes";
|
||||||
|
|
||||||
export type UseTopologyCanvasOptions = {
|
export type UseTopologyCanvasOptions = {
|
||||||
mapId: string;
|
mapId: string;
|
||||||
|
|
@ -300,7 +300,7 @@ export function useTopologyCanvas(opts: UseTopologyCanvasOptions) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canvasMode) return;
|
if (!canvasMode) return;
|
||||||
void import("../TopologyReactFlowView");
|
void import("../components/TopologyReactFlowView");
|
||||||
}, [canvasMode]);
|
}, [canvasMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -6,7 +6,7 @@ import { queryKeys } from "../../../constants/queryKeys";
|
||||||
import { useI18n } from "../../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { useToast } from "../../../hooks/useToast";
|
import { useToast } from "../../../hooks/useToast";
|
||||||
import type { ManagedNeItem } from "../../../types";
|
import type { ManagedNeItem } from "../../../types";
|
||||||
import type { NeNodeData } from "../TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
closeCtxMenu: () => void;
|
closeCtxMenu: () => void;
|
||||||
|
|
@ -18,12 +18,12 @@ import { queryKeys } from "../../../constants/queryKeys";
|
||||||
import { useI18n } from "../../../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { useToast } from "../../../hooks/useToast";
|
import { useToast } from "../../../hooks/useToast";
|
||||||
import type { ManagedNeItem } from "../../../types";
|
import type { ManagedNeItem } from "../../../types";
|
||||||
import { applyViewGraph } from "../graphFlow";
|
import { applyViewGraph } from "../lib/graphFlow";
|
||||||
import type { EdgeDefaults } from "../edgeStyle";
|
import type { EdgeDefaults } from "../lib/edgeStyle";
|
||||||
import type { PlaceholderCreateDialogState } from "../modals/PlaceholderCreateDialog";
|
import type { PlaceholderCreateDialogState } from "../components/modals/PlaceholderCreateDialog";
|
||||||
import type { ManagedNeFormState } from "../../managedNe/formState";
|
import type { ManagedNeFormState } from "../../managed-ne/lib/formState";
|
||||||
import { isWorldFlatViewName } from "../treeUtils";
|
import { isWorldFlatViewName } from "../lib/treeUtils";
|
||||||
import type { NeNodeData } from "../TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
|
|
||||||
type FocusNode = (fabricNodeId: string, fit: boolean) => void;
|
type FocusNode = (fabricNodeId: string, fit: boolean) => void;
|
||||||
|
|
||||||
|
|
@ -21,12 +21,12 @@ import type {
|
||||||
TopologyDiscoverOut,
|
TopologyDiscoverOut,
|
||||||
TopologyViewGraph,
|
TopologyViewGraph,
|
||||||
} from "../../../types";
|
} from "../../../types";
|
||||||
import type { DiscoverProgress, DiscoverSummary } from "../TopologyDiscoverPanel";
|
import type { DiscoverProgress, DiscoverSummary } from "../components/TopologyDiscoverPanel";
|
||||||
import type { EdgeDefaults, EdgeStyleData } from "../edgeStyle";
|
import type { EdgeDefaults, EdgeStyleData } from "../lib/edgeStyle";
|
||||||
import { flowToPositions, graphToFlow } from "../graphFlow";
|
import { flowToPositions, graphToFlow } from "../lib/graphFlow";
|
||||||
import { discoverResultKind, isLocalPendingEdgeId } from "../idUtils";
|
import { discoverResultKind, isLocalPendingEdgeId } from "../lib/idUtils";
|
||||||
import { layoutGraph } from "../layoutGraph";
|
import { layoutGraph } from "../lib/layoutGraph";
|
||||||
import type { NeNodeData } from "../TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
|
|
||||||
export type UseTopologyDiscoverOptions = {
|
export type UseTopologyDiscoverOptions = {
|
||||||
mapId: string;
|
mapId: string;
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import type { Edge, Node } from "@xyflow/react";
|
import type { Edge, Node } from "@xyflow/react";
|
||||||
import { UNDO_MAX } from "../constants";
|
import { UNDO_MAX } from "../lib/constants";
|
||||||
import type { HistorySnap } from "../pageTypes";
|
import type { HistorySnap } from "../lib/pageTypes";
|
||||||
import type { NeNodeData } from "../TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
|
|
||||||
export function useTopologyHistory(
|
export function useTopologyHistory(
|
||||||
nodes: Node<NeNodeData>[],
|
nodes: Node<NeNodeData>[],
|
||||||
|
|
@ -14,8 +14,8 @@ import {
|
||||||
updateTopologyMap,
|
updateTopologyMap,
|
||||||
} from "../../../services/api";
|
} from "../../../services/api";
|
||||||
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../../types";
|
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../../types";
|
||||||
import { LAST_LEAF_KEY, TREE_EXPAND_KEY, WORLD_MAP_ENABLED } from "../constants";
|
import { LAST_LEAF_KEY, TREE_EXPAND_KEY, WORLD_MAP_ENABLED } from "../lib/constants";
|
||||||
import type { CtxMenu } from "../pageTypes";
|
import type { CtxMenu } from "../lib/pageTypes";
|
||||||
import {
|
import {
|
||||||
findFolderInTree,
|
findFolderInTree,
|
||||||
findViewInRegion,
|
findViewInRegion,
|
||||||
|
|
@ -26,8 +26,8 @@ import {
|
||||||
isWorldDrillFolder,
|
isWorldDrillFolder,
|
||||||
isWorldFlatViewName,
|
isWorldFlatViewName,
|
||||||
regionDisplayName,
|
regionDisplayName,
|
||||||
} from "../treeUtils";
|
} from "../lib/treeUtils";
|
||||||
import type { NeNodeData } from "../TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
|
|
||||||
export type UseTopologyTreeNavOptions = {
|
export type UseTopologyTreeNavOptions = {
|
||||||
liveSync: boolean;
|
liveSync: boolean;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { getStraightPath } from "@xyflow/react";
|
import { getStraightPath } from "@xyflow/react";
|
||||||
import type { Edge, Node } from "@xyflow/react";
|
import type { Edge, Node } from "@xyflow/react";
|
||||||
import type { TopologyViewGraph, TopologyViewItem, TopologyViewNodeItem } from "../../types";
|
import type { TopologyViewGraph, TopologyViewItem, TopologyViewNodeItem } from "../../../types";
|
||||||
import type { NeNodeData } from "./TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
import { isAggregateEdgeId, type LinkEdgeData } from "./linkDisplay";
|
import { isAggregateEdgeId, type LinkEdgeData } from "./linkDisplay";
|
||||||
import {
|
import {
|
||||||
TOPO_HANDLE_X,
|
TOPO_HANDLE_X,
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Edge, Node, Position } from "@xyflow/react";
|
import type { Edge, Node, Position } from "@xyflow/react";
|
||||||
import type { TopologyViewEdgeItem, TopologyViewGraph, TopologyViewNodeItem } from "../../types";
|
import type { TopologyViewEdgeItem, TopologyViewGraph, TopologyViewNodeItem } from "../../../types";
|
||||||
import type { NeNodeData } from "./TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
import { formatPortPairLabel } from "./linkDisplay";
|
import { formatPortPairLabel } from "./linkDisplay";
|
||||||
import type { EdgeDefaults, EdgeStyleData } from "./edgeStyle";
|
import type { EdgeDefaults, EdgeStyleData } from "./edgeStyle";
|
||||||
import { withEdgeVisual } from "./edgeStyle";
|
import { withEdgeVisual } from "./edgeStyle";
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { TopologyDiscoverNeResult } from "../../types";
|
import type { TopologyDiscoverNeResult } from "../../../types";
|
||||||
|
|
||||||
export function newId(): string {
|
export function newId(): string {
|
||||||
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Edge, Node } from "@xyflow/react";
|
import type { Edge, Node } from "@xyflow/react";
|
||||||
import type { NeNodeData } from "./TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
|
|
||||||
export type HistorySnap = {
|
export type HistorySnap = {
|
||||||
nodes: Node<NeNodeData>[];
|
nodes: Node<NeNodeData>[];
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Node } from "@xyflow/react";
|
import type { Node } from "@xyflow/react";
|
||||||
import type { NeNodeData } from "./TopologyReactFlowView";
|
import type { NeNodeData } from "../components/TopologyReactFlowView";
|
||||||
|
|
||||||
export function normalizeSearchText(s: string): string {
|
export function normalizeSearchText(s: string): string {
|
||||||
return String(s || "")
|
return String(s || "")
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { CSSProperties } from "react";
|
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";
|
import { WORLD_MAP_ENABLED } from "./constants";
|
||||||
|
|
||||||
export { WORLD_MAP_ENABLED };
|
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";
|
import { VENDOR_TONE_KEYS, type VendorColors, type VendorToneKey } from "./displayPrefs";
|
||||||
|
|
||||||
export type { VendorToneKey, VendorColors };
|
export type { VendorToneKey, VendorColors };
|
||||||
|
|
@ -2,7 +2,7 @@ import type {
|
||||||
TopologyViewEdgeItem,
|
TopologyViewEdgeItem,
|
||||||
TopologyViewGraph,
|
TopologyViewGraph,
|
||||||
TopologyViewNodeItem,
|
TopologyViewNodeItem,
|
||||||
} from "../../types";
|
} from "../../../types";
|
||||||
import { WORLD_FLAT_ACCUM_CAP } from "./constants";
|
import { WORLD_FLAT_ACCUM_CAP } from "./constants";
|
||||||
|
|
||||||
/** Merge newly fetched flat LOD into the existing cache so visited areas stay. */
|
/** 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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import { FieldSelect } from "../components/ui/FieldSelect";
|
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useToast } from "../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import { apiGet, apiPatch, apiPost } from "../services/api";
|
import { apiGet, apiPatch, apiPost } from "../../services/api";
|
||||||
|
|
||||||
type UserRow = {
|
type UserRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -13,10 +13,10 @@ import {
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { Button, Input, Modal } from "@heroui/react";
|
import { Button, Input, Modal } from "@heroui/react";
|
||||||
import type { WebTerminalHandle } from "../components/WebTerminal";
|
import type { WebTerminalHandle } from "./components/WebTerminal";
|
||||||
import { AppModalShell } from "../components/ui/AppModalShell";
|
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useToast } from "../hooks/useToast";
|
import { useToast } from "../../hooks/useToast";
|
||||||
import {
|
import {
|
||||||
ApiRequestError,
|
ApiRequestError,
|
||||||
closeWebcrtSession,
|
closeWebcrtSession,
|
||||||
|
|
@ -35,10 +35,10 @@ import {
|
||||||
webcrtSftpRename,
|
webcrtSftpRename,
|
||||||
webcrtSftpUpload,
|
webcrtSftpUpload,
|
||||||
type WebcrtSftpItem,
|
type WebcrtSftpItem,
|
||||||
} from "../services/api";
|
} from "../../services/api";
|
||||||
import { pageCount } from "../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { writeClipboardText } from "../utils/clipboard";
|
import { writeClipboardText } from "../../utils/clipboard";
|
||||||
import type { CliTargetItem, ManagedNeItem } from "../types";
|
import type { CliTargetItem, ManagedNeItem } from "../../types";
|
||||||
import {
|
import {
|
||||||
defaultKeywordHighlightConfig,
|
defaultKeywordHighlightConfig,
|
||||||
loadKeywordHighlightConfig,
|
loadKeywordHighlightConfig,
|
||||||
|
|
@ -46,11 +46,11 @@ import {
|
||||||
saveKeywordHighlightConfig,
|
saveKeywordHighlightConfig,
|
||||||
type KeywordHighlightConfig,
|
type KeywordHighlightConfig,
|
||||||
type KeywordRule,
|
type KeywordRule,
|
||||||
} from "../utils/webcrtKeywordHighlight";
|
} from "./lib/webcrtKeywordHighlight";
|
||||||
|
|
||||||
/** xterm is large — only pull it when a session pane actually mounts. */
|
/** xterm is large — only pull it when a session pane actually mounts. */
|
||||||
const WebTerminal = lazy(() =>
|
const WebTerminal = lazy(() =>
|
||||||
import("../components/WebTerminal").then((m) => ({ default: m.WebTerminal })),
|
import("./components/WebTerminal").then((m) => ({ default: m.WebTerminal })),
|
||||||
);
|
);
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
const SESSION_OPTS_KEY = "netx.webcrt.sessionOptions";
|
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 { FitAddon } from "@xterm/addon-fit";
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../../i18n";
|
||||||
import { webcrtWsUrl } from "../services/api";
|
import { webcrtWsUrl } from "../../../services/api";
|
||||||
import {
|
import {
|
||||||
applyKeywordHighlight,
|
applyKeywordHighlight,
|
||||||
type KeywordHighlightConfig,
|
type KeywordHighlightConfig,
|
||||||
} from "../utils/webcrtKeywordHighlight";
|
} from "../lib/webcrtKeywordHighlight";
|
||||||
import { readClipboardText, writeClipboardText } from "../utils/clipboard";
|
import { readClipboardText, writeClipboardText } from "../../../utils/clipboard";
|
||||||
|
|
||||||
export type WebTerminalHandle = {
|
export type WebTerminalHandle = {
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { useMemo, type ReactNode } from "react";
|
import { useMemo, type ReactNode } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Button } from "@heroui/react";
|
import { Button } from "@heroui/react";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { WorkbenchCardIcon } from "../components/WorkbenchCardIcon";
|
import { WorkbenchCardIcon } from "./components/WorkbenchCardIcon";
|
||||||
import { modulesInSection, type ModuleDefinition, type WorkbenchSection } from "../config/modules";
|
import { modulesInSection, type ModuleDefinition, type WorkbenchSection } from "../../config/modules";
|
||||||
import { openOrFocusModule } from "../utils/moduleWindows";
|
import { openOrFocusModule } from "../../utils/moduleWindows";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import { fetchFabricSummary, fetchManagedNeStats, fetchOpsTasks, fetchRuntimeMetrics, fetchTopologyTree, type OpsTaskItem } from "../services/api";
|
import { fetchFabricSummary, fetchManagedNeStats, fetchOpsTasks, fetchRuntimeMetrics, fetchTopologyTree, type OpsTaskItem } from "../../services/api";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
import { isUmeWorldContainer, isWorldDrillFolder } from "./topology/treeUtils";
|
import { isUmeWorldContainer, isWorldDrillFolder } from "../topology/lib/treeUtils";
|
||||||
import type { TopologyTreeFolderItem } from "../types";
|
import type { TopologyTreeFolderItem } from "../../types";
|
||||||
|
|
||||||
const SECTIONS: WorkbenchSection[] = ["monitoring", "operations", "system"];
|
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 = {
|
type Props = {
|
||||||
tone: ModuleIconTone;
|
tone: ModuleIconTone;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
import en from "./en";
|
import en from "./locales/en";
|
||||||
import zh from "./zh";
|
import zh from "./locales/zh";
|
||||||
|
|
||||||
export type Locale = "zh" | "en";
|
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