diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index ee83202..cdbc42a 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -166,6 +166,86 @@ const en = { body: "This capability is not available yet.", }, }, + bizState: { + title: "Business state", + create: "Create task", + createHint: "Pick a managed NE (LLDP enabled by default)", + created: "Task created", + createFailed: "Create failed", + empty: "No business-state tasks yet.", + pickNe: "Select managed NE…", + start: "Start schedule", + pause: "Pause", + collectNow: "Collect now", + delete: "Delete", + confirmDelete: "Delete this task and all batches?", + deleted: "Deleted", + started: "Schedule started", + paused: "Paused", + collecting: "Collecting", + profiles: "Monitor items", + enable: "On", + command: "Command template", + params: "Bindings", + discoverVrf: "Discover VRFs", + saveBindings: "Save bindings", + cancel: "Cancel", + bindTitle: "Select VRF bindings", + unbound: "Not bound", + batches: "Batches", + viewBatch: "View", + export: "Export", + noBatches: "No batches yet", + batchDetail: "Batch detail", + colNe: "NE", + colStatus: "Status", + colLast: "Last collect", + colTime: "Time", + colRows: "Rows", + colActions: "Actions", + pickTask: "Select a task on the left", + noNeedBind: "This item needs no parameter bindings", + discoverFailed: "Discover failed", + bindingsSaved: "Bindings saved", + }, + bizCompare: { + title: "Business compare", + mapping: "Port mapping", + newMapping: "New mapping…", + mapName: "Mapping name", + saveMapping: "Save mapping", + validateMapping: "Validate", + mapHint: "before_if,after_if one pair per line", + mappingSaved: "Mapping saved", + createJob: "Create compare job", + 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", + pick: "Select…", + sameAsBefore: "Same as before / select…", + autoHint: "auto: new after-task batches trigger compare", + createCompare: "Create job", + runNow: "Run now", + jobList: "Compare jobs", + runs: "Run history", + result: "Result", + pickRun: "Pick a run or click Run now", + emptyJobs: "No compare jobs yet", + created: "Compare job created", + ran: "Compare finished", + colName: "Name", + colMode: "Mode", + colStatus: "Status", + colKind: "Kind", + colKey: "Key", + colChange: "Changes", + }, portTraffic: { title: "Port traffic", create: "Monitor device", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index faf72d9..25b9592 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -166,6 +166,86 @@ const zh = { body: "该能力尚未开放。", }, }, + bizState: { + title: "业务状态监控", + create: "创建任务", + createHint: "选择托管网元后创建(默认启用 LLDP)", + created: "已创建任务", + createFailed: "创建失败", + empty: "暂无业务监控任务。", + pickNe: "选择托管网元…", + start: "启动周期", + pause: "暂停", + collectNow: "立即采集", + delete: "删除", + confirmDelete: "删除该业务监控任务及所有批次?", + deleted: "已删除", + started: "已启动周期采集", + paused: "已暂停", + collecting: "采集中", + profiles: "监控项", + enable: "启用", + command: "命令模板", + params: "参数关联", + discoverVrf: "发现 VRF", + saveBindings: "保存关联", + cancel: "取消", + bindTitle: "选择 VRF 绑定", + unbound: "未关联", + batches: "采集批次", + viewBatch: "查看", + export: "导出", + noBatches: "尚无批次", + batchDetail: "批次详情", + colNe: "网元", + colStatus: "状态", + colLast: "最近采集", + colTime: "时间", + colRows: "行数", + colActions: "操作", + pickTask: "选择左侧任务查看详情", + noNeedBind: "该监控项无需参数关联", + discoverFailed: "发现失败", + bindingsSaved: "参数关联已保存", + }, + bizCompare: { + title: "业务状态比对", + mapping: "端口映射", + newMapping: "新建映射…", + mapName: "映射名称", + saveMapping: "保存映射", + validateMapping: "校验映射", + mapHint: "before_if,after_if 每行一对", + mappingSaved: "映射已保存", + createJob: "建立比对任务", + jobName: "名称", + template: "模板", + mode: "模式", + modeManual: "手工选批次", + modeAuto: "自动(钉死操作前,操作后取最新)", + beforeTask: "操作前任务", + beforeBatch: "操作前批次", + afterTask: "操作后任务", + afterBatch: "操作后批次", + pick: "选择…", + sameAsBefore: "同操作前 / 选择…", + autoHint: "auto:操作后任务新批次到达会自动跑比对", + createCompare: "创建比对任务", + runNow: "立即比对", + jobList: "比对任务列表", + runs: "历史 Run", + result: "比对结果", + pickRun: "选择 Run 或点「立即比对」", + emptyJobs: "暂无比对任务", + created: "比对任务已创建", + ran: "比对完成", + colName: "名称", + colMode: "模式", + colStatus: "状态", + colKind: "类型", + colKey: "Key", + colChange: "变更", + }, portTraffic: { title: "端口流量监控", create: "添加设备监控", diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index 049b5fe..5628d9c 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -1,4 +1,8 @@ +import { Button, Input } from "@heroui/react"; import { useCallback, useEffect, useState } from "react"; +import { FieldSelect } from "../../components/ui/FieldSelect"; +import { useToast } from "../../hooks/useToast"; +import { useI18n } from "../../i18n"; import { bizCompareCreateJob, bizCompareCreateMapping, @@ -13,11 +17,14 @@ import { bizCompareValidateMapping, bizStateListBatches, bizStateListTasks, + formatErr, } from "../../services/api"; +import { formatSystemTime } from "../../utils/time"; +import { jobChipColor, NmStatusChip } from "./nmChips"; type TaskOpt = { id: string; ne_name: string; ne_ip: string; vendor: string }; type BatchOpt = { id: string; status: string; row_count: number; started_at?: string | null }; -type Template = { id: string; name: string; metric_id: string; key_fields: string[]; iface_fields: string[] }; +type Template = { id: string; name: string; metric_id: string }; type Mapping = { id: string; name: string; rows: { before_if: string; after_if: string }[] }; type Job = { id: string; @@ -32,7 +39,14 @@ type Job = { status: string; }; +function fmtTime(v?: string | null) { + if (!v) return "—"; + return formatSystemTime(v) || v; +} + export function BizComparePage() { + const { t } = useI18n(); + const { showOk, showError } = useToast(); const [tasks, setTasks] = useState([]); const [templates, setTemplates] = useState([]); const [mappings, setMappings] = useState([]); @@ -40,10 +54,8 @@ export function BizComparePage() { const [selectedJobId, setSelectedJobId] = useState(""); const [runs, setRuns] = useState([]); const [runDetail, setRunDetail] = useState(null); - const [err, setErr] = useState(""); const [busy, setBusy] = useState(false); - // create job form const [name, setName] = useState("割接比对"); const [templateId, setTemplateId] = useState(""); const [mappingId, setMappingId] = useState(""); @@ -55,19 +67,18 @@ export function BizComparePage() { const [afterBatchId, setAfterBatchId] = useState(""); const [mode, setMode] = useState<"manual" | "auto">("manual"); - // mapping editor const [mapName, setMapName] = useState("端口映射"); - const [mapText, setMapText] = useState("old-if,new-if\n"); + const [mapText, setMapText] = useState("before_if,after_if\n"); const [validateOut, setValidateOut] = useState(null); const refresh = useCallback(async () => { - const [t, tpl, maps, j] = await Promise.all([ + const [taskRes, tpl, maps, j] = await Promise.all([ bizStateListTasks(), bizCompareListTemplates(), bizCompareListMappings(), bizCompareListJobs(), ]); - setTasks((t.items || []) as TaskOpt[]); + setTasks((taskRes.items || []) as TaskOpt[]); setTemplates((tpl.items || []) as Template[]); setMappings((maps.items || []) as Mapping[]); setJobs((j.items || []) as Job[]); @@ -79,10 +90,11 @@ export function BizComparePage() { void (async () => { try { await refresh(); - } catch (e: any) { - setErr(String(e?.message || e)); + } catch (e) { + showError(formatErr(e)); } })(); + // eslint-disable-next-line react-hooks/exhaustive-deps -- mount once }, [refresh]); useEffect(() => { @@ -111,7 +123,9 @@ export function BizComparePage() { const rows: { before_if: string; after_if: string }[] = []; for (const line of mapText.split(/\r?\n/)) { const s = line.trim(); - if (!s || s.startsWith("#") || s.toLowerCase().startsWith("old")) continue; + if (!s || s.startsWith("#") || s.toLowerCase().startsWith("old") || s.toLowerCase().startsWith("before")) { + continue; + } const parts = s.split(/[,|\t]+/).map((x) => x.trim()); if (parts.length >= 2 && parts[0] && parts[1]) { rows.push({ before_if: parts[0], after_if: parts[1] }); @@ -120,16 +134,20 @@ export function BizComparePage() { return rows; }; - const saveMapping = async () => { + const updateExistingMapping = async () => { setBusy(true); - setErr(""); try { const rows = parseMapRows(); - const m = await bizCompareCreateMapping({ name: mapName, rows }); - setMappingId(String(m.id)); + if (mappingId) { + await bizCompareUpdateMapping(mappingId, { name: mapName, rows }); + } else { + const m = await bizCompareCreateMapping({ name: mapName, rows }); + setMappingId(String(m.id)); + } + showOk(t("bizCompare.mappingSaved")); await refresh(); - } catch (e: any) { - setErr(String(e?.message || e)); + } catch (e) { + showError(formatErr(e)); } finally { setBusy(false); } @@ -137,11 +155,10 @@ export function BizComparePage() { const doValidate = async () => { if (!mappingId || !beforeBatchId || !afterBatchId) { - setErr("请先选择映射与前后批次"); + showError(`${t("bizCompare.validateMapping")}: mapping / batches`); return; } setBusy(true); - setErr(""); try { const v = await bizCompareValidateMapping({ mapping_id: mappingId, @@ -150,8 +167,8 @@ export function BizComparePage() { template_id: templateId, }); setValidateOut(v); - } catch (e: any) { - setErr(String(e?.message || e)); + } catch (e) { + showError(formatErr(e)); } finally { setBusy(false); } @@ -159,7 +176,6 @@ export function BizComparePage() { const createJob = async () => { setBusy(true); - setErr(""); try { const j = await bizCompareCreateJob({ name, @@ -172,11 +188,12 @@ export function BizComparePage() { mode, }); setSelectedJobId(String(j.id)); + showOk(t("bizCompare.created")); await refresh(); const r = await bizCompareListRuns(String(j.id)); setRuns(r.items || []); - } catch (e: any) { - setErr(String(e?.message || e)); + } catch (e) { + showError(formatErr(e)); } finally { setBusy(false); } @@ -197,15 +214,14 @@ export function BizComparePage() { setBeforeBatchId(job.before_batch_id); setAfterBatchId(job.after_batch_id); setMode(job.mode === "auto" ? "auto" : "manual"); + if (job.mapping_id) loadMappingText(job.mapping_id); } }; const runNow = async () => { if (!selectedJobId) return; setBusy(true); - setErr(""); try { - // sync latest batch selection onto job before run await bizCompareUpdateJob(selectedJobId, { before_batch_id: beforeBatchId, after_batch_id: mode === "manual" ? afterBatchId : "", @@ -215,18 +231,23 @@ export function BizComparePage() { }); const run = await bizCompareRunJob(selectedJobId); setRunDetail(run); + showOk(t("bizCompare.ran")); const r = await bizCompareListRuns(selectedJobId); setRuns(r.items || []); - } catch (e: any) { - setErr(String(e?.message || e)); + } catch (e) { + showError(formatErr(e)); } finally { setBusy(false); } }; const loadRun = async (runId: string) => { - const d = await bizCompareGetRun(runId); - setRunDetail(d); + try { + const d = await bizCompareGetRun(runId); + setRunDetail(d); + } catch (e) { + showError(formatErr(e)); + } }; const loadMappingText = (id: string) => { @@ -237,251 +258,278 @@ export function BizComparePage() { setMapText(["before_if,after_if", ...m.rows.map((r) => `${r.before_if},${r.after_if}`)].join("\n")); }; - const updateExistingMapping = async () => { - if (!mappingId) { - await saveMapping(); - return; - } - setBusy(true); - try { - await bizCompareUpdateMapping(mappingId, { name: mapName, rows: parseMapRows() }); - await refresh(); - } catch (e: any) { - setErr(String(e?.message || e)); - } finally { - setBusy(false); - } - }; - - const taskLabel = (t: TaskOpt) => `${t.ne_name || t.ne_ip || t.id} (${t.vendor || "-"})`; + const taskLabel = (row: TaskOpt) => `${row.ne_name || row.ne_ip || row.id} (${row.vendor || "-"})`; return ( -
-
-

业务状态比对

-

Phase2:模板 + 端口映射 + 前后批次 diff(支持 auto 追比)

-
- {err ?
{err}
: null} - -
-
-

端口映射

-
- - setMapName(e.target.value)} placeholder="映射名称" /> - - -
-