diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index c090aac..8e061a3 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -452,8 +452,10 @@ const en = { 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.", - createProject: "New cutover project", - projectName: "Project name", + create: "Create task", + createHint: "Pick old/new business-monitor tasks; optional port mapping and baseline batches.", + createProject: "Create task", + projectName: "Task name", oldTask: "Old-device collect task", newTask: "New-device collect task", portMapping: "Port mapping", @@ -462,9 +464,31 @@ const en = { needProjectFields: "Enter a name and pick old/new collect tasks", needExpectPorts: "Select at least one expected port for this batch", needBaselineFirst: "Save an old baseline batch first to list ports", - projectCreated: "Project created", - projects: "Projects", - emptyProjects: "No cutover projects yet", + needBizStateTasks: "No 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 baseline / mapping", baselineSaved: "Baseline saved", pickExpectPorts: "Expected ports (from old baseline)", @@ -510,10 +534,10 @@ const en = { enableHighfreq: "Enable high-freq port collect", highfreqHint: "Creates/reuses biz-state tasks with interface_brief only on old/new NEs. Add BGP etc. later on the same tasks in Business monitor.", - highfreqReady: "High-freq port collect tasks ready and bound to this project", + highfreqReady: "High-freq port collect tasks ready and bound to this task", collectNow: "Collect old+new now", collectTriggered: "Collect triggered", - openBizState: "Open business state", + openBizState: "Open business monitor", boundTasks: "Bound collect tasks", acceptTitle: "Batch acceptance summary", acceptPassed: "Batch acceptance passed", @@ -521,7 +545,7 @@ const en = { acceptPassedShort: "Passed", acceptFailedShort: "Failed", acceptCarryHint: "Failure does not block the next batch; red tickets stay until resolved.", - openRedHint: "{{n}} open red ticket(s) on this project (carry allowed)", + openRedHint: "{{n}} open red ticket(s) on this task (carry allowed)", redTitle: "Red tickets", redOpen: "open", resolveRed: "Resolve", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 6b055a5..ac871e3 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -449,19 +449,43 @@ const zh = { title: "割接监控", hint: "与业务对比独立:相对基线漂移 + 本批预期 + 老/新双端判定。复用采集任务与端口映射。", hintPort: "当前监控项:端口状态(interface_brief / admin·phy·prot)。选基线 → 勾选本批端口 → 判定看板。", - createProject: "新建割接项目", - projectName: "项目名称", + create: "创建任务", + createHint: "选择老/新设备的业务监控任务,可选端口映射与基线批次。", + createProject: "创建任务", + projectName: "任务名称", oldTask: "老设备采集任务", newTask: "新设备采集任务", portMapping: "端口映射", oldBaseline: "老设备基线批次", newBaseline: "新设备基线批次(可选)", - needProjectFields: "请填写项目名并选择老/新采集任务", + needProjectFields: "请填写任务名并选择老/新采集任务", needExpectPorts: "请先勾选本批预期迁移的端口", needBaselineFirst: "请先保存老设备基线批次,才能列出端口", - projectCreated: "项目已创建", - projects: "项目", - emptyProjects: "暂无割接项目", + needBizStateTasks: "尚无业务监控任务,请先创建:", + projectCreated: "任务已创建", + projects: "任务", + empty: "暂无割接任务。", + emptyProjects: "暂无割接任务", + listFilterPh: "筛选任务名 / 网元 / IP / 状态", + kpiProjects: "任务数", + kpiActive: "进行中", + detail: "详情", + cancel: "关闭", + colName: "任务", + colPair: "老 → 新", + colBaseline: "基线", + colCreated: "创建时间", + colActions: "操作", + baselineSet: "已设", + baselineUnset: "未设", + statusActive: "进行中", + statusDone: "已完成", + statusPaused: "已暂停", + statusPending: "待开始", + statusReview: "验收中", + tabSetup: "基线配置", + tabBatches: "割接批次", + tabBoard: "判定看板", saveBaseline: "保存基线/映射", baselineSaved: "基线已保存", pickExpectPorts: "本批预期端口(来自老基线)", @@ -507,7 +531,7 @@ const zh = { enableHighfreq: "开启高频端口采集", highfreqHint: "在业务监控下为老/新设备各建(或复用)仅含 interface_brief 的任务并周期采集;后续要加 BGP 等直接去业务监控勾选即可。", - highfreqReady: "高频端口采集任务已就绪(已绑定到本项目)", + highfreqReady: "高频端口采集任务已就绪(已绑定到本任务)", collectNow: "立即采集老+新", collectTriggered: "已触发采集", openBizState: "打开业务监控", @@ -518,7 +542,7 @@ const zh = { acceptPassedShort: "通过", acceptFailedShort: "未通过", acceptCarryHint: "验收不过不拦截:可新建下一批继续;红单保留留痕,可手动关闭。", - openRedHint: "项目仍有 {{n}} 条未关闭红单(带红继续)", + openRedHint: "任务仍有 {{n}} 条未关闭红单(带红继续)", redTitle: "红单留痕", redOpen: "未关闭", resolveRed: "关闭", diff --git a/web/src/pages/network/BizMigrationPage.tsx b/web/src/pages/network/BizMigrationPage.tsx index 2b0a333..a107be5 100644 --- a/web/src/pages/network/BizMigrationPage.tsx +++ b/web/src/pages/network/BizMigrationPage.tsx @@ -1,6 +1,9 @@ -import { Button, Input } from "@heroui/react"; +import { Button, Input, Modal } from "@heroui/react"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import { AppModalShell } from "../../components/ui/AppModalShell"; import { FieldSelect } from "../../components/ui/FieldSelect"; +import { useDebouncedValue } from "../../hooks/useDebouncedValue"; import { useToast } from "../../hooks/useToast"; import { useI18n } from "../../i18n"; import { @@ -24,7 +27,8 @@ import { bizStateListTasks, formatErr, } from "../../services/api"; -import { Link } from "react-router-dom"; +import { formatSystemTime } from "../../utils/time"; +import { jobChipColor, NmStatusChip } from "./nmChips"; type TaskOpt = { id: string; ne_name: string; ne_ip: string; note?: string; interval_sec?: number }; type Project = { @@ -36,6 +40,8 @@ type Project = { new_baseline_batch_id: string; mapping_id: string; status: string; + note?: string; + created_at?: string | null; old_task?: TaskOpt; new_task?: TaskOpt; }; @@ -93,6 +99,8 @@ type BaselinePort = { mapped_to?: string; }; +type DetailTab = "setup" | "batches" | "board"; + const COLOR: Record = { green: "#16a34a", yellow: "#ca8a04", @@ -100,7 +108,7 @@ const COLOR: Record = { gray: "#6b7280", }; - const VERDICT_I18N: Record = { +const VERDICT_I18N: Record = { migrated: "bizMigration.verdictMigrated", migrating: "bizMigration.verdictMigrating", lost: "bizMigration.verdictLost", @@ -111,6 +119,18 @@ const COLOR: Record = { ok: "bizMigration.verdictOk", }; +function fmtTime(v?: string | null) { + if (!v) return "—"; + return formatSystemTime(v) || v; +} + +function taskLabel(x: TaskOpt) { + const base = `${x.ne_name || x.id} (${x.ne_ip || "-"})`; + const note = x.note ? ` · ${x.note}` : ""; + const iv = x.interval_sec ? ` · ${x.interval_sec}s` : ""; + return `${base}${note}${iv}`; +} + export function BizMigrationPage() { const { t } = useI18n(); const { showOk, showError } = useToast(); @@ -118,7 +138,27 @@ export function BizMigrationPage() { const [projects, setProjects] = useState([]); const [tasks, setTasks] = useState([]); const [mappings, setMappings] = useState<{ id: string; name: string }[]>([]); + const [busy, setBusy] = useState(false); + const [listKeyword, setListKeyword] = useState(""); + const debouncedListKw = useDebouncedValue(listKeyword, 250); + + const [createOpen, setCreateOpen] = useState(false); + const [createName, setCreateName] = useState(""); + const [createOldTaskId, setCreateOldTaskId] = useState(""); + const [createNewTaskId, setCreateNewTaskId] = useState(""); + const [createMappingId, setCreateMappingId] = useState(""); + const [createOldBaselineId, setCreateOldBaselineId] = useState(""); + const [createNewBaselineId, setCreateNewBaselineId] = useState(""); + const [createOldBatches, setCreateOldBatches] = useState<{ id: string; started_at?: string | null }[]>([]); + const [createNewBatches, setCreateNewBatches] = useState<{ id: string; started_at?: string | null }[]>([]); + const [projectId, setProjectId] = useState(""); + const [detailTab, setDetailTab] = useState("setup"); + const [mappingId, setMappingId] = useState(""); + const [oldBaselineId, setOldBaselineId] = useState(""); + const [newBaselineId, setNewBaselineId] = useState(""); + const [oldBatches, setOldBatches] = useState<{ id: string; started_at?: string | null }[]>([]); + const [newBatches, setNewBatches] = useState<{ id: string; started_at?: string | null }[]>([]); const [batches, setBatches] = useState([]); const [batchId, setBatchId] = useState(""); const [board, setBoard] = useState<{ @@ -133,17 +173,7 @@ export function BizMigrationPage() { } | null; } | null>(null); const [diffs, setDiffs] = useState([]); - const [busy, setBusy] = useState(false); const [onlyExpect, setOnlyExpect] = useState(true); - - const [newName, setNewName] = useState(""); - const [oldTaskId, setOldTaskId] = useState(""); - const [newTaskId, setNewTaskId] = useState(""); - const [mappingId, setMappingId] = useState(""); - const [oldBaselineId, setOldBaselineId] = useState(""); - const [newBaselineId, setNewBaselineId] = useState(""); - const [oldBatches, setOldBatches] = useState<{ id: string; started_at?: string | null }[]>([]); - const [newBatches, setNewBatches] = useState<{ id: string; started_at?: string | null }[]>([]); const [batchLabel, setBatchLabel] = useState(""); const [baselinePorts, setBaselinePorts] = useState([]); const [selectedPorts, setSelectedPorts] = useState>(new Set()); @@ -163,6 +193,32 @@ export function BizMigrationPage() { [diffs, onlyExpect], ); + const filteredProjects = useMemo(() => { + const kw = debouncedListKw.trim().toLowerCase(); + if (!kw) return projects; + return projects.filter((p) => { + const blob = [ + p.name, + p.status, + p.note || "", + p.old_task?.ne_name || "", + p.old_task?.ne_ip || "", + p.new_task?.ne_name || "", + p.new_task?.ne_ip || "", + p.old_task_id, + p.new_task_id, + ] + .join(" ") + .toLowerCase(); + return blob.includes(kw); + }); + }, [projects, debouncedListKw]); + + const activeCount = useMemo( + () => projects.filter((p) => String(p.status || "").toLowerCase() === "active").length, + [projects], + ); + useEffect(() => { if (batch?.accept_summary && Object.keys(batch.accept_summary).length) { setAcceptInfo(batch.accept_summary); @@ -252,6 +308,11 @@ export function BizMigrationPage() { setBatches([]); setBatchId(""); setBaselinePorts([]); + setRedTickets([]); + setOpenRedCount(0); + setBoard(null); + setDiffs([]); + setAcceptInfo(null); return; } void (async () => { @@ -259,16 +320,13 @@ export function BizMigrationPage() { const res = await bizMigrationListBatches(projectId); const items = (res.items || []) as MigBatch[]; setBatches(items); - if (!items.find((b) => b.id === batchId)) { - setBatchId(items[0]?.id || ""); - } + setBatchId((prev) => (items.find((b) => b.id === prev) ? prev : items[0]?.id || "")); await loadBaselinePorts(projectId); await loadRedTickets(projectId); } catch (e) { showError(formatErr(e)); } })(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectId, showError, loadBaselinePorts, loadRedTickets]); useEffect(() => { @@ -282,11 +340,41 @@ export function BizMigrationPage() { }, [batchId]); useEffect(() => { - if (!oldTaskId) { + if (!createOldTaskId) { + setCreateOldBatches([]); + return; + } + void bizStateListBatches(createOldTaskId, 30).then((r) => { + setCreateOldBatches( + ((r.items || []) as Record[]).map((x) => ({ + id: String(x.id || ""), + started_at: (x.started_at as string) || null, + })), + ); + }); + }, [createOldTaskId]); + + useEffect(() => { + if (!createNewTaskId) { + setCreateNewBatches([]); + return; + } + void bizStateListBatches(createNewTaskId, 30).then((r) => { + setCreateNewBatches( + ((r.items || []) as Record[]).map((x) => ({ + id: String(x.id || ""), + started_at: (x.started_at as string) || null, + })), + ); + }); + }, [createNewTaskId]); + + useEffect(() => { + if (!project?.old_task_id) { setOldBatches([]); return; } - void bizStateListBatches(oldTaskId, 30).then((r) => { + void bizStateListBatches(project.old_task_id, 30).then((r) => { setOldBatches( ((r.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), @@ -294,14 +382,14 @@ export function BizMigrationPage() { })), ); }); - }, [oldTaskId]); + }, [project?.old_task_id]); useEffect(() => { - if (!newTaskId) { + if (!project?.new_task_id) { setNewBatches([]); return; } - void bizStateListBatches(newTaskId, 30).then((r) => { + void bizStateListBatches(project.new_task_id, 30).then((r) => { setNewBatches( ((r.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), @@ -309,7 +397,15 @@ export function BizMigrationPage() { })), ); }); - }, [newTaskId]); + }, [project?.new_task_id]); + + useEffect(() => { + if (project) { + setMappingId(project.mapping_id || ""); + setOldBaselineId(project.old_baseline_batch_id || ""); + setNewBaselineId(project.new_baseline_batch_id || ""); + } + }, [project]); async function loadBoard(bid: string, runId = "") { try { @@ -327,26 +423,60 @@ export function BizMigrationPage() { } } + function resetCreateForm() { + setCreateName(""); + setCreateOldTaskId(""); + setCreateNewTaskId(""); + setCreateMappingId(""); + setCreateOldBaselineId(""); + setCreateNewBaselineId(""); + } + + const openCreate = () => { + closeProject(); + resetCreateForm(); + setCreateOpen(true); + }; + + const closeCreate = () => { + setCreateOpen(false); + resetCreateForm(); + }; + + const openProject = (id: string, tab: DetailTab = "setup") => { + setProjectId(id); + setDetailTab(tab); + setSelectedPorts(new Set()); + setPortFilter(""); + setBatchLabel(""); + setOnlyExpect(true); + }; + + const closeProject = () => { + setProjectId(""); + setDetailTab("setup"); + }; + async function onCreateProject() { - if (!newName.trim() || !oldTaskId || !newTaskId) { + if (!createName.trim() || !createOldTaskId || !createNewTaskId) { showError(t("bizMigration.needProjectFields")); return; } setBusy(true); try { const p = (await bizMigrationCreateProject({ - name: newName.trim(), - old_task_id: oldTaskId, - new_task_id: newTaskId, - mapping_id: mappingId || "", - old_baseline_batch_id: oldBaselineId || "", - new_baseline_batch_id: newBaselineId || "", + name: createName.trim(), + old_task_id: createOldTaskId, + new_task_id: createNewTaskId, + mapping_id: createMappingId || "", + old_baseline_batch_id: createOldBaselineId || "", + new_baseline_batch_id: createNewBaselineId || "", status: "active", })) as Project; await reloadProjects(); - setProjectId(p.id); - setNewName(""); + closeCreate(); showOk(t("bizMigration.projectCreated")); + openProject(p.id, "setup"); } catch (e) { showError(formatErr(e)); } finally { @@ -393,11 +523,16 @@ export function BizMigrationPage() { setBatchLabel(""); setSelectedPorts(new Set()); if (Number((b as { open_red_count?: number }).open_red_count || 0) > 0) { - showOk(t("bizMigration.batchCreatedWithRed", { n: String((b as { open_red_count?: number }).open_red_count) })); + showOk( + t("bizMigration.batchCreatedWithRed", { + n: String((b as { open_red_count?: number }).open_red_count), + }), + ); } else { showOk(t("bizMigration.batchCreated")); } await loadRedTickets(projectId); + setDetailTab("board"); } catch (e) { showError(formatErr(e)); } finally { @@ -427,6 +562,7 @@ export function BizMigrationPage() { if (res.batch?.accept_run_id) { await loadBoard(batchId, res.batch.accept_run_id); } + setDetailTab("board"); } else { await bizMigrationPatchBatch(batchId, { status }); const res = await bizMigrationListBatches(projectId); @@ -458,6 +594,7 @@ export function BizMigrationPage() { const run = await bizMigrationEvaluate(batchId, {}); await loadBoard(batchId, String((run as { id?: string }).id || "")); showOk(t("bizMigration.evaluated")); + setDetailTab("board"); } catch (e) { showError(formatErr(e)); } finally { @@ -469,13 +606,11 @@ export function BizMigrationPage() { if (!projectId) return; setBusy(true); try { - const res = await bizMigrationEnsurePortHighfreq(projectId, { + await bizMigrationEnsurePortHighfreq(projectId, { interval_sec: 60, collect_now: true, }); await reloadProjects(); - const pr = await bizMigrationListProjects(); - setProjects((pr.items || []) as Project[]); showOk(t("bizMigration.highfreqReady")); } catch (e) { showError(formatErr(e)); @@ -497,16 +632,6 @@ export function BizMigrationPage() { } } - useEffect(() => { - if (project) { - setOldTaskId(project.old_task_id); - setNewTaskId(project.new_task_id); - setMappingId(project.mapping_id || ""); - setOldBaselineId(project.old_baseline_batch_id || ""); - setNewBaselineId(project.new_baseline_batch_id || ""); - } - }, [project]); - function togglePort(name: string) { setSelectedPorts((prev) => { const next = new Set(prev); @@ -521,418 +646,638 @@ export function BizMigrationPage() { return key ? t(key) : v; } + function statusLabel(status: string) { + const s = String(status || "").toLowerCase(); + if (s === "active") return t("bizMigration.statusActive"); + if (s === "done" || s === "completed") return t("bizMigration.statusDone"); + if (s === "paused") return t("bizMigration.statusPaused"); + if (s === "pending") return t("bizMigration.statusPending"); + if (s === "review") return t("bizMigration.statusReview"); + return status || "—"; + } + + const taskOptions = ( + <> + + {tasks.map((x) => ( + + ))} + + ); + + const batchOptions = (items: { id: string; started_at?: string | null }[]) => ( + <> + + {items.map((x) => ( + + ))} + + ); + return ( -
-
-

{t("bizMigration.title")}

-

{t("bizMigration.hintPort")}

+
+
+

{t("bizMigration.title")}

+
+ +
+
+

{t("bizMigration.hintPort")}

+ +
+
+
+
{t("bizMigration.kpiProjects")}
+
{projects.length}
+
+
+
{t("bizMigration.kpiActive")}
+
{activeCount}
+
+
+ +
+ setListKeyword(e.target.value)} + /> +
+ +
+ + + + + + + + + + + + + {filteredProjects.map((row) => ( + + + + + + + + + ))} + {!filteredProjects.length ? ( + + + + ) : null} + +
{t("bizMigration.colName")}{t("bizMigration.colPair")}{t("bizMigration.colStatus")}{t("bizMigration.colBaseline")}{t("bizMigration.colCreated")}{t("bizMigration.colActions")}
+
{row.name}
+ {row.note ?
{row.note}
: null} +
+
+ {row.old_task?.ne_name || row.old_task_id.slice(0, 8)} +
+
+ → {row.new_task?.ne_name || row.new_task_id.slice(0, 8)} + {row.old_task?.ne_ip || row.new_task?.ne_ip + ? ` · ${row.old_task?.ne_ip || "—"} / ${row.new_task?.ne_ip || "—"}` + : ""} +
+
+ {statusLabel(row.status)} + + {row.old_baseline_batch_id ? ( + {t("bizMigration.baselineSet")} + ) : ( + {t("bizMigration.baselineUnset")} + )} + {fmtTime(row.created_at)} +
+ + + +
+
+
{t("bizMigration.empty")}
+
+
-
- {t("bizMigration.createProject")} -
- - setOldTaskId(e.target.value)} - fullWidth - > - - {tasks.map((x) => ( - - ))} - - setNewTaskId(e.target.value)} - fullWidth - > - - {tasks.map((x) => ( - - ))} - - setMappingId(e.target.value)} - fullWidth - > - - {mappings.map((m) => ( - - ))} - - setOldBaselineId(e.target.value)} - fullWidth - > - - {oldBatches.map((x) => ( - - ))} - - setNewBaselineId(e.target.value)} - fullWidth - > - - {newBatches.map((x) => ( - - ))} - -
- -
+ {/* Create task */} + + + {t("bizMigration.create")} + + + +

{t("bizMigration.createHint")}

+
+ + + + + + +
+ {!tasks.length ? ( +

+ {t("bizMigration.needBizStateTasks")}{" "} + {t("bizMigration.openBizState")} +

+ ) : null} +
+ + + + +
-
-
- {t("bizMigration.projects")} - {projects.length === 0 && ( - {t("bizMigration.emptyProjects")} - )} - {projects.map((p) => ( - - ))} -
- -
- {project && ( -
-
- {project.name} -
- - - - - {t("bizMigration.openBizState")} - -
-
- {(project.old_task || project.new_task) && ( -
- {t("bizMigration.boundTasks")}:{" "} - {project.old_task?.ne_name || project.old_task_id.slice(0, 8)} - {project.old_task?.note ? ` (${project.old_task.note})` : ""} / - {project.new_task?.ne_name || project.new_task_id.slice(0, 8)} - {project.new_task?.note ? ` (${project.new_task.note})` : ""} - {project.old_task?.interval_sec - ? ` · ${project.old_task.interval_sec}s` - : ""} -
- )} - {openRedCount > 0 && ( -
- {t("bizMigration.openRedHint", { n: String(openRedCount) })} -
- )} -

{t("bizMigration.highfreqHint")}

- -
-
- {t("bizMigration.pickExpectPorts")} ({selectedPorts.size}) -
- + + + {project?.name || t("bizMigration.detail")} + {project ? ( + + {statusLabel(project.status)} + + ) : null} + + + + + {project ? ( + <> +
+
- -
- - - setBatchId(e.target.value)} + + + {t("bizMigration.openBizState")} + + {openRedCount > 0 ? ( + + {t("bizMigration.openRedHint", { n: String(openRedCount) })} + + ) : null} +
+

+ {t("bizMigration.boundTasks")}:{" "} + {project.old_task?.ne_name || project.old_task_id.slice(0, 8)} + {project.old_task?.note ? ` (${project.old_task.note})` : ""} /{" "} + {project.new_task?.ne_name || project.new_task_id.slice(0, 8)} + {project.new_task?.note ? ` (${project.new_task.note})` : ""} + {project.old_task?.interval_sec ? ` · ${project.old_task.interval_sec}s` : ""} +

+

+ {t("bizMigration.highfreqHint")} +

+ +
+ + + +
+ + {detailTab === "setup" ? ( +
+
+ + + +
+
+ -
+ ) : null} + + {detailTab === "batches" ? ( +
+
+
+ {t("bizMigration.pickExpectPorts")} ({selectedPorts.size}) +
+
+ +
+ {!project.old_baseline_batch_id ? ( +
+ {t("bizMigration.needBaselineFirst")} +
+ ) : null} +
+ + + + + + + + + + + {filteredBaselinePorts.map((p) => ( + togglePort(p.interface)} + style={{ cursor: "pointer" }} + > + + + + + + + ))} + {!filteredBaselinePorts.length ? ( + + + + ) : null} + +
+ {t("bizMigration.colPort")}admin/phy/prot{t("bizMigration.colMapped")}{t("bizMigration.colDesc")}
+ togglePort(p.interface)} + /> + {p.interface} + {p.admin || "-"}/{p.phy || "-"}/{p.prot || "-"} + {p.mapped_to || "—"} + {p.description || ""} +
+
{t("bizMigration.emptyBaselinePorts")}
+
+
+
+ +
+ void setBatchStatus("review")} - > - {t("bizMigration.finishBatch")} + placeholder={t("bizMigration.defaultBatchLabel")} + style={{ minWidth: 160 }} + /> + - + + + + ) : null} +
+
+ ) : null} + + {detailTab === "board" ? ( +
+
+ setBatchId(e.target.value)} + aria-label={t("bizMigration.batches")} + > + {batches.map((b) => ( + + ))} + + - - )} -
-
- )} - - {acceptInfo && ( -
- {t("bizMigration.acceptTitle")} -
-
- {acceptInfo.passed ? t("bizMigration.acceptPassedShort") : t("bizMigration.acceptFailedShort")} -
-
- {t("bizMigration.progress")}: {acceptInfo.progress_ok ?? 0}/{acceptInfo.progress_total ?? 0} -
-
- {t("bizMigration.anomaly")}: {acceptInfo.anomaly ?? 0} -
-
-
{t("bizMigration.acceptCarryHint")}
-
- )} - - {redTickets.length > 0 && ( -
- - {t("bizMigration.redTitle")} ({openRedCount} {t("bizMigration.redOpen")}) - -
- - - - - - - - - - - - - {redTickets.map((r) => ( - - - - - - - - - - ))} - -
{t("bizMigration.colOldPort")}{t("bizMigration.colNewPort")}{t("bizMigration.colOldStatus")}{t("bizMigration.colNewStatus")}{t("bizMigration.colVerdict")}{t("bizMigration.colStatus")} -
{r.key_str}{r.new_key_str || "—"}{r.old_status || "—"}{r.new_status || "—"}{verdictLabel(r.verdict)}{r.status} - {r.status !== "resolved" && ( - - )} -
-
-
- )} - - {board?.run && ( -
- - {t("bizMigration.board")} · {t("bizMigration.metricPort")} - -
-
- {t("bizMigration.progress")}:{" "} - - {board.run.summary?.progress?.ok ?? 0}/{board.run.summary?.progress?.total ?? 0} - -
-
- {t("bizMigration.anomaly")}: {board.run.summary?.anomaly ?? 0} -
- {sheetCards[0] && ( -
- {sheetCards[0].title || sheetCards[0].metric_id} +
- )} - -
-
- - - - - - - - - - - - {visibleDiffs.map((d) => ( - - - - - - - - ))} - {visibleDiffs.length === 0 && ( - - - - )} - -
{t("bizMigration.colOldPort")}{t("bizMigration.colNewPort")}{t("bizMigration.colOldStatus")}{t("bizMigration.colNewStatus")}{t("bizMigration.colVerdict")}
{d.key_str || "—"}{d.new_key_str || "—"}{d.old_status || "—"}{d.new_status || "—"} - {verdictLabel(d.verdict)} -
- {t("bizMigration.emptyDiffs")} -
-
-
- )} -
-
-
+ {acceptInfo ? ( +
+ {t("bizMigration.acceptTitle")} +
+
+ {acceptInfo.passed + ? t("bizMigration.acceptPassedShort") + : t("bizMigration.acceptFailedShort")} +
+
+ {t("bizMigration.progress")}: {acceptInfo.progress_ok ?? 0}/ + {acceptInfo.progress_total ?? 0} +
+
+ {t("bizMigration.anomaly")}: {acceptInfo.anomaly ?? 0} +
+
+
+ {t("bizMigration.acceptCarryHint")} +
+
+ ) : null} + + {redTickets.length > 0 ? ( +
+ + {t("bizMigration.redTitle")} ({openRedCount} {t("bizMigration.redOpen")}) + +
+ + + + + + + + + + + + + {redTickets.map((r) => ( + + + + + + + + + + ))} + +
{t("bizMigration.colOldPort")}{t("bizMigration.colNewPort")}{t("bizMigration.colOldStatus")}{t("bizMigration.colNewStatus")}{t("bizMigration.colVerdict")}{t("bizMigration.colStatus")} +
{r.key_str}{r.new_key_str || "—"}{r.old_status || "—"}{r.new_status || "—"}{verdictLabel(r.verdict)}{r.status} + {r.status !== "resolved" ? ( + + ) : null} +
+
+
+ ) : null} + + {board?.run ? ( +
+ + {t("bizMigration.board")} · {t("bizMigration.metricPort")} + +
+
+ {t("bizMigration.progress")}:{" "} + + {board.run.summary?.progress?.ok ?? 0}/{board.run.summary?.progress?.total ?? 0} + +
+
+ {t("bizMigration.anomaly")}: {board.run.summary?.anomaly ?? 0} +
+ {sheetCards[0] ? ( +
+ {sheetCards[0].title || sheetCards[0].metric_id} +
+ ) : null} +
+
+ + + + + + + + + + + + {visibleDiffs.map((d) => ( + + + + + + + + ))} + {!visibleDiffs.length ? ( + + + + ) : null} + +
{t("bizMigration.colOldPort")}{t("bizMigration.colNewPort")}{t("bizMigration.colOldStatus")}{t("bizMigration.colNewStatus")}{t("bizMigration.colVerdict")}
{d.key_str || "—"}{d.new_key_str || "—"}{d.old_status || "—"}{d.new_status || "—"} + {verdictLabel(d.verdict)} +
+
{t("bizMigration.emptyDiffs")}
+
+
+
+ ) : ( +
{t("bizMigration.emptyDiffs")}
+ )} +
+ ) : null} + + ) : null} + + + + + + ); }