mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 05:30:46 +08:00
Add cutover migration monitor and day-based batch retention.
Protect baselines and compare/migration-referenced batches from purge or delete, and let operators mark baselines and clean high-freq data manually. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c7539e379d
commit
8631f58113
23 changed files with 3837 additions and 91 deletions
|
|
@ -49,6 +49,9 @@ const BizStatePage = lazy(() =>
|
|||
const BizComparePage = lazy(() =>
|
||||
import("./pages/network/BizComparePage").then((m) => ({ default: m.BizComparePage })),
|
||||
);
|
||||
const BizMigrationPage = lazy(() =>
|
||||
import("./pages/network/BizMigrationPage").then((m) => ({ default: m.BizMigrationPage })),
|
||||
);
|
||||
const UsersPage = lazy(() => import("./pages/UsersPage").then((m) => ({ default: m.UsersPage })));
|
||||
const AuditLayout = lazy(() =>
|
||||
import("./pages/audit/AuditLayout").then((m) => ({ default: m.AuditLayout })),
|
||||
|
|
@ -128,6 +131,7 @@ function ProtectedApp() {
|
|||
<Route path="tasks/port-traffic" element={<PortTrafficPage />} />
|
||||
<Route path="tasks/biz-state" element={<BizStatePage />} />
|
||||
<Route path="tasks/biz-compare" element={<BizComparePage />} />
|
||||
<Route path="tasks/biz-migration" element={<BizMigrationPage />} />
|
||||
</Route>
|
||||
<Route path="/collect" element={<Navigate to="/network/tasks/collect" replace />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
|
|
|
|||
|
|
@ -86,6 +86,12 @@ export const NETWORK_NAV: readonly NetworkNavGroup[] = [
|
|||
labelKey: "network.nav.bizCompare",
|
||||
group: "tasks",
|
||||
},
|
||||
{
|
||||
id: "biz-migration",
|
||||
path: "/network/tasks/biz-migration",
|
||||
labelKey: "network.nav.bizMigration",
|
||||
group: "tasks",
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ const en = {
|
|||
portTrafficWall: "Traffic wall",
|
||||
bizState: "Business state",
|
||||
bizCompare: "Business compare",
|
||||
bizMigration: "Cutover monitor",
|
||||
},
|
||||
collapseNav: "Collapse sidebar",
|
||||
expandNav: "Expand sidebar",
|
||||
|
|
@ -199,7 +200,25 @@ const en = {
|
|||
intervalUnitDays: "days",
|
||||
intervalUnitHours: "hours",
|
||||
intervalUnitSeconds: "seconds",
|
||||
retention: "Keep batches",
|
||||
retention: "Retention days",
|
||||
retentionHint: "Batches older than this are purged; baselines and compare/cutover refs are kept.",
|
||||
dailyKeepEnabled: "Daily keep policy",
|
||||
dailyKeepCount: "Keep per day",
|
||||
dailyKeepHint: "When on: keep only the newest N batches per past day; extras purge next day (off by default).",
|
||||
markBaseline: "Mark baseline",
|
||||
unmarkBaseline: "Unmark baseline",
|
||||
baseline: "Baseline",
|
||||
protected: "Protected",
|
||||
deleteBatch: "Delete",
|
||||
bulkDelete: "Bulk delete",
|
||||
confirmDeleteBatch: "Delete this batch? Protected batches cannot be deleted.",
|
||||
confirmBulkDelete: "Delete {{count}} selected batches? Protected ones are skipped.",
|
||||
batchDeleted: "Batch deleted",
|
||||
bulkDeleted: "Deleted {{deleted}}, skipped {{skipped}}",
|
||||
purgeNow: "Purge now",
|
||||
purgeOk: "Purge done: dropped {{dropped}}",
|
||||
selectAll: "Select deletable",
|
||||
colProtect: "Protect",
|
||||
saveSchedule: "Save schedule",
|
||||
scheduleSaved: "Schedule saved",
|
||||
colInterval: "Interval",
|
||||
|
|
@ -428,6 +447,89 @@ const en = {
|
|||
colKey: "Identity",
|
||||
colChange: "Changes",
|
||||
},
|
||||
bizMigration: {
|
||||
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.",
|
||||
createProject: "New cutover project",
|
||||
projectName: "Project name",
|
||||
oldTask: "Old-device collect task",
|
||||
newTask: "New-device collect task",
|
||||
portMapping: "Port mapping",
|
||||
oldBaseline: "Old baseline batch",
|
||||
newBaseline: "New baseline batch (optional)",
|
||||
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",
|
||||
saveBaseline: "Save baseline / mapping",
|
||||
baselineSaved: "Baseline saved",
|
||||
pickExpectPorts: "Expected ports (from old baseline)",
|
||||
portFilterPh: "Filter interface / description",
|
||||
emptyBaselinePorts: "No interface_brief rows in baseline (enable that profile on the task)",
|
||||
batchLabel: "Batch label",
|
||||
defaultBatchLabel: "Night N",
|
||||
expectPorts: "Expected ports (old side)",
|
||||
expectPortsPh: "Comma or newline separated",
|
||||
createBatch: "New batch",
|
||||
batchCreated: "Batch created",
|
||||
batches: "Batches",
|
||||
startBatch: "Start batch",
|
||||
finishBatch: "Finish batch",
|
||||
evaluate: "Evaluate now",
|
||||
evaluated: "Evaluation done",
|
||||
statusUpdated: "Status updated",
|
||||
board: "Progress board",
|
||||
metricPort: "Port status",
|
||||
progress: "Progress",
|
||||
anomaly: "Anomaly",
|
||||
onlyExpect: "Expected ports only",
|
||||
colMetric: "Metric",
|
||||
colKey: "Object",
|
||||
colPort: "Old port",
|
||||
colMapped: "Mapped new",
|
||||
colDesc: "Description",
|
||||
colOldPort: "Old port",
|
||||
colNewPort: "New port",
|
||||
colOldStatus: "Old admin/phy/prot",
|
||||
colNewStatus: "New admin/phy/prot",
|
||||
colOld: "Old",
|
||||
colNew: "New",
|
||||
colVerdict: "Verdict",
|
||||
emptyDiffs: "No rows yet — run Evaluate",
|
||||
verdictMigrated: "Migrated",
|
||||
verdictMigrating: "Migrating",
|
||||
verdictLost: "Lost",
|
||||
verdictAnomaly: "Anomaly",
|
||||
verdictNotInvolved: "Not involved",
|
||||
verdictUnexpected: "Unexpected new",
|
||||
verdictOk: "OK",
|
||||
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 state.",
|
||||
highfreqReady: "High-freq port collect tasks ready and bound to this project",
|
||||
collectNow: "Collect old+new now",
|
||||
collectTriggered: "Collect triggered",
|
||||
openBizState: "Open business state",
|
||||
boundTasks: "Bound collect tasks",
|
||||
acceptTitle: "Batch acceptance summary",
|
||||
acceptPassed: "Batch acceptance passed",
|
||||
acceptFailed: "Batch acceptance failed ({{n}} red tickets; you may continue)",
|
||||
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)",
|
||||
redTitle: "Red tickets",
|
||||
redOpen: "open",
|
||||
resolveRed: "Resolve",
|
||||
redResolved: "Red ticket resolved",
|
||||
batchCreatedWithRed: "Batch created ({{n}} open red tickets; carry allowed)",
|
||||
colStatus: "Status",
|
||||
verdictUnfinished: "Unfinished",
|
||||
},
|
||||
portTraffic: {
|
||||
title: "Port traffic",
|
||||
create: "Monitor device",
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ const zh = {
|
|||
portTrafficWall: "流量大屏",
|
||||
bizState: "业务状态监控",
|
||||
bizCompare: "业务状态比对",
|
||||
bizMigration: "割接监控",
|
||||
},
|
||||
collapseNav: "折叠侧栏",
|
||||
expandNav: "展开侧栏",
|
||||
|
|
@ -199,7 +200,25 @@ const zh = {
|
|||
intervalUnitDays: "天",
|
||||
intervalUnitHours: "小时",
|
||||
intervalUnitSeconds: "秒",
|
||||
retention: "保留批次数",
|
||||
retention: "保留天数",
|
||||
retentionHint: "超过天数的批次会自动清理;基线与比对/割接引用的批次不会删除。",
|
||||
dailyKeepEnabled: "每日保留策略",
|
||||
dailyKeepCount: "每天保留次数",
|
||||
dailyKeepHint: "开启后:过去每一天只保留最近 N 次,多余在次日清理(默认关闭)。",
|
||||
markBaseline: "标为基线",
|
||||
unmarkBaseline: "撤销基线",
|
||||
baseline: "基线",
|
||||
protected: "已保护",
|
||||
deleteBatch: "删除",
|
||||
bulkDelete: "批量删除",
|
||||
confirmDeleteBatch: "删除该采集批次?受保护的批次无法删除。",
|
||||
confirmBulkDelete: "删除选中的 {{count}} 个批次?受保护的会自动跳过。",
|
||||
batchDeleted: "已删除批次",
|
||||
bulkDeleted: "已删除 {{deleted}} 个,跳过 {{skipped}} 个",
|
||||
purgeNow: "立即清理",
|
||||
purgeOk: "清理完成:删除 {{dropped}} 个批次",
|
||||
selectAll: "全选可删",
|
||||
colProtect: "保护",
|
||||
saveSchedule: "保存周期",
|
||||
scheduleSaved: "周期配置已保存",
|
||||
colInterval: "周期",
|
||||
|
|
@ -426,6 +445,88 @@ const zh = {
|
|||
colKey: "身份",
|
||||
colChange: "变更",
|
||||
},
|
||||
bizMigration: {
|
||||
title: "割接监控",
|
||||
hint: "与业务状态比对独立:相对基线漂移 + 本批预期 + 老/新双端判定。复用采集任务与端口映射。",
|
||||
hintPort: "当前监控项:端口状态(interface_brief / admin·phy·prot)。选基线 → 勾选本批端口 → 判定看板。",
|
||||
createProject: "新建割接项目",
|
||||
projectName: "项目名称",
|
||||
oldTask: "老设备采集任务",
|
||||
newTask: "新设备采集任务",
|
||||
portMapping: "端口映射",
|
||||
oldBaseline: "老设备基线批次",
|
||||
newBaseline: "新设备基线批次(可选)",
|
||||
needProjectFields: "请填写项目名并选择老/新采集任务",
|
||||
needExpectPorts: "请先勾选本批预期迁移的端口",
|
||||
needBaselineFirst: "请先保存老设备基线批次,才能列出端口",
|
||||
projectCreated: "项目已创建",
|
||||
projects: "项目",
|
||||
emptyProjects: "暂无割接项目",
|
||||
saveBaseline: "保存基线/映射",
|
||||
baselineSaved: "基线已保存",
|
||||
pickExpectPorts: "本批预期端口(来自老基线)",
|
||||
portFilterPh: "筛选接口名 / 描述",
|
||||
emptyBaselinePorts: "基线中无端口状态数据(确认任务已采 interface_brief)",
|
||||
batchLabel: "批次标签",
|
||||
defaultBatchLabel: "第N晚",
|
||||
expectPorts: "本批预期端口(老侧)",
|
||||
expectPortsPh: "逗号或换行分隔",
|
||||
createBatch: "新建批次",
|
||||
batchCreated: "批次已创建",
|
||||
batches: "批次",
|
||||
startBatch: "本批开始",
|
||||
finishBatch: "本批完成",
|
||||
evaluate: "立即判定",
|
||||
evaluated: "判定完成",
|
||||
statusUpdated: "状态已更新",
|
||||
board: "进度看板",
|
||||
metricPort: "端口状态",
|
||||
progress: "进度",
|
||||
anomaly: "异常",
|
||||
onlyExpect: "仅看预期端口",
|
||||
colMetric: "监控项",
|
||||
colKey: "对象",
|
||||
colPort: "老侧端口",
|
||||
colMapped: "映射新口",
|
||||
colDesc: "描述",
|
||||
colOldPort: "老端口",
|
||||
colNewPort: "新端口",
|
||||
colOldStatus: "老状态 admin/phy/prot",
|
||||
colNewStatus: "新状态 admin/phy/prot",
|
||||
colOld: "老侧",
|
||||
colNew: "新侧",
|
||||
colVerdict: "判定",
|
||||
emptyDiffs: "暂无明细,请先「立即判定」",
|
||||
verdictMigrated: "已迁移",
|
||||
verdictMigrating: "迁移中",
|
||||
verdictLost: "丢失",
|
||||
verdictAnomaly: "异常",
|
||||
verdictNotInvolved: "未涉及",
|
||||
verdictUnexpected: "意外新增",
|
||||
verdictOk: "正常",
|
||||
enableHighfreq: "开启高频端口采集",
|
||||
highfreqHint:
|
||||
"在业务监控下为老/新设备各建(或复用)仅含 interface_brief 的任务并周期采集;后续要加 BGP 等直接去业务监控勾选即可。",
|
||||
highfreqReady: "高频端口采集任务已就绪(已绑定到本项目)",
|
||||
collectNow: "立即采集老+新",
|
||||
collectTriggered: "已触发采集",
|
||||
openBizState: "打开业务监控",
|
||||
boundTasks: "当前绑定采集任务",
|
||||
acceptTitle: "本批验收小结",
|
||||
acceptPassed: "本批验收通过",
|
||||
acceptFailed: "本批验收未通过(红单 {{n}},可带红开下一批)",
|
||||
acceptPassedShort: "通过",
|
||||
acceptFailedShort: "未通过",
|
||||
acceptCarryHint: "验收不过不拦截:可新建下一批继续;红单保留留痕,可手动关闭。",
|
||||
openRedHint: "项目仍有 {{n}} 条未关闭红单(带红继续)",
|
||||
redTitle: "红单留痕",
|
||||
redOpen: "未关闭",
|
||||
resolveRed: "关闭",
|
||||
redResolved: "红单已关闭",
|
||||
batchCreatedWithRed: "批次已创建(当前有 {{n}} 条未关闭红单,允许带红继续)",
|
||||
colStatus: "状态",
|
||||
verdictUnfinished: "未完成",
|
||||
},
|
||||
portTraffic: {
|
||||
title: "端口流量监控",
|
||||
create: "添加设备监控",
|
||||
|
|
|
|||
938
web/src/pages/network/BizMigrationPage.tsx
Normal file
938
web/src/pages/network/BizMigrationPage.tsx
Normal file
|
|
@ -0,0 +1,938 @@
|
|||
import { Button, Input } from "@heroui/react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||
import { useToast } from "../../hooks/useToast";
|
||||
import { useI18n } from "../../i18n";
|
||||
import {
|
||||
bizCompareListMappings,
|
||||
bizMigrationCollectNow,
|
||||
bizMigrationCreateBatch,
|
||||
bizMigrationCreateProject,
|
||||
bizMigrationEnsurePortHighfreq,
|
||||
bizMigrationEvaluate,
|
||||
bizMigrationFinishBatch,
|
||||
bizMigrationGetBoard,
|
||||
bizMigrationListBaselinePorts,
|
||||
bizMigrationListBatches,
|
||||
bizMigrationListDiffs,
|
||||
bizMigrationListProjects,
|
||||
bizMigrationListRedTickets,
|
||||
bizMigrationPatchBatch,
|
||||
bizMigrationPatchProject,
|
||||
bizMigrationResolveRedTicket,
|
||||
bizStateListBatches,
|
||||
bizStateListTasks,
|
||||
formatErr,
|
||||
} from "../../services/api";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type TaskOpt = { id: string; ne_name: string; ne_ip: string; note?: string; interval_sec?: number };
|
||||
type Project = {
|
||||
id: string;
|
||||
name: string;
|
||||
old_task_id: string;
|
||||
new_task_id: string;
|
||||
old_baseline_batch_id: string;
|
||||
new_baseline_batch_id: string;
|
||||
mapping_id: string;
|
||||
status: string;
|
||||
old_task?: TaskOpt;
|
||||
new_task?: TaskOpt;
|
||||
};
|
||||
type MigBatch = {
|
||||
id: string;
|
||||
batch_label: string;
|
||||
status: string;
|
||||
expect_set?: { ports?: string[] };
|
||||
accept_status?: string;
|
||||
accept_run_id?: string;
|
||||
accept_summary?: {
|
||||
passed?: boolean;
|
||||
progress_ok?: number;
|
||||
progress_total?: number;
|
||||
anomaly?: number;
|
||||
verdict_counts?: Record<string, number>;
|
||||
};
|
||||
};
|
||||
type RedTicket = {
|
||||
id: string;
|
||||
batch_id: string;
|
||||
key_str: string;
|
||||
new_key_str?: string;
|
||||
verdict: string;
|
||||
old_status?: string;
|
||||
new_status?: string;
|
||||
status: string;
|
||||
};
|
||||
type SheetCard = {
|
||||
metric_id: string;
|
||||
title?: string;
|
||||
progress_ok: number;
|
||||
progress_total: number;
|
||||
anomaly: number;
|
||||
};
|
||||
type DiffRow = {
|
||||
id: string;
|
||||
metric_id: string;
|
||||
verdict: string;
|
||||
color: string;
|
||||
old_kind: string;
|
||||
new_kind: string;
|
||||
in_expect: boolean;
|
||||
key_str?: string;
|
||||
new_key_str?: string;
|
||||
old_status?: string;
|
||||
new_status?: string;
|
||||
};
|
||||
type BaselinePort = {
|
||||
interface: string;
|
||||
admin?: string;
|
||||
phy?: string;
|
||||
prot?: string;
|
||||
description?: string;
|
||||
mapped_to?: string;
|
||||
};
|
||||
|
||||
const COLOR: Record<string, string> = {
|
||||
green: "#16a34a",
|
||||
yellow: "#ca8a04",
|
||||
red: "#dc2626",
|
||||
gray: "#6b7280",
|
||||
};
|
||||
|
||||
const VERDICT_I18N: Record<string, string> = {
|
||||
migrated: "bizMigration.verdictMigrated",
|
||||
migrating: "bizMigration.verdictMigrating",
|
||||
lost: "bizMigration.verdictLost",
|
||||
anomaly: "bizMigration.verdictAnomaly",
|
||||
not_involved: "bizMigration.verdictNotInvolved",
|
||||
unexpected_new: "bizMigration.verdictUnexpected",
|
||||
unfinished: "bizMigration.verdictUnfinished",
|
||||
ok: "bizMigration.verdictOk",
|
||||
};
|
||||
|
||||
export function BizMigrationPage() {
|
||||
const { t } = useI18n();
|
||||
const { showOk, showError } = useToast();
|
||||
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [tasks, setTasks] = useState<TaskOpt[]>([]);
|
||||
const [mappings, setMappings] = useState<{ id: string; name: string }[]>([]);
|
||||
const [projectId, setProjectId] = useState("");
|
||||
const [batches, setBatches] = useState<MigBatch[]>([]);
|
||||
const [batchId, setBatchId] = useState("");
|
||||
const [board, setBoard] = useState<{
|
||||
run?: {
|
||||
id: string;
|
||||
summary?: {
|
||||
sheet_cards?: SheetCard[];
|
||||
progress?: { ok: number; total: number };
|
||||
anomaly?: number;
|
||||
expect_ports?: string[];
|
||||
};
|
||||
} | null;
|
||||
} | null>(null);
|
||||
const [diffs, setDiffs] = useState<DiffRow[]>([]);
|
||||
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<BaselinePort[]>([]);
|
||||
const [selectedPorts, setSelectedPorts] = useState<Set<string>>(new Set());
|
||||
const [portFilter, setPortFilter] = useState("");
|
||||
const [redTickets, setRedTickets] = useState<RedTicket[]>([]);
|
||||
const [openRedCount, setOpenRedCount] = useState(0);
|
||||
const [acceptInfo, setAcceptInfo] = useState<MigBatch["accept_summary"] | null>(null);
|
||||
|
||||
const project = useMemo(
|
||||
() => projects.find((p) => p.id === projectId) || null,
|
||||
[projects, projectId],
|
||||
);
|
||||
const batch = useMemo(() => batches.find((b) => b.id === batchId) || null, [batches, batchId]);
|
||||
const sheetCards = board?.run?.summary?.sheet_cards || [];
|
||||
const visibleDiffs = useMemo(
|
||||
() => (onlyExpect ? diffs.filter((d) => d.in_expect) : diffs),
|
||||
[diffs, onlyExpect],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (batch?.accept_summary && Object.keys(batch.accept_summary).length) {
|
||||
setAcceptInfo(batch.accept_summary);
|
||||
} else if (batch && batch.accept_status === "none") {
|
||||
setAcceptInfo(null);
|
||||
}
|
||||
}, [batch]);
|
||||
|
||||
const filteredBaselinePorts = useMemo(() => {
|
||||
const kw = portFilter.trim().toLowerCase();
|
||||
if (!kw) return baselinePorts;
|
||||
return baselinePorts.filter(
|
||||
(p) =>
|
||||
p.interface.toLowerCase().includes(kw) ||
|
||||
String(p.description || "")
|
||||
.toLowerCase()
|
||||
.includes(kw),
|
||||
);
|
||||
}, [baselinePorts, portFilter]);
|
||||
|
||||
const reloadProjects = useCallback(async () => {
|
||||
const res = await bizMigrationListProjects();
|
||||
setProjects((res.items || []) as Project[]);
|
||||
}, []);
|
||||
|
||||
const loadBaselinePorts = useCallback(async (pid: string) => {
|
||||
if (!pid) {
|
||||
setBaselinePorts([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await bizMigrationListBaselinePorts(pid);
|
||||
setBaselinePorts(res.ports || []);
|
||||
} catch {
|
||||
setBaselinePorts([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadRedTickets = useCallback(async (pid: string) => {
|
||||
if (!pid) {
|
||||
setRedTickets([]);
|
||||
setOpenRedCount(0);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await bizMigrationListRedTickets(pid);
|
||||
setRedTickets((res.items || []) as RedTicket[]);
|
||||
setOpenRedCount(Number(res.open_count || 0));
|
||||
} catch {
|
||||
setRedTickets([]);
|
||||
setOpenRedCount(0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const [pr, tk, mp] = await Promise.all([
|
||||
bizMigrationListProjects(),
|
||||
bizStateListTasks(),
|
||||
bizCompareListMappings(),
|
||||
]);
|
||||
setProjects((pr.items || []) as Project[]);
|
||||
setTasks(
|
||||
((tk.items || []) as Record<string, unknown>[]).map((x) => ({
|
||||
id: String(x.id || ""),
|
||||
ne_name: String(x.ne_name || ""),
|
||||
ne_ip: String(x.ne_ip || ""),
|
||||
note: String(x.note || ""),
|
||||
interval_sec: Number(x.interval_sec || 0) || undefined,
|
||||
})),
|
||||
);
|
||||
setMappings(
|
||||
((mp.items || []) as Record<string, unknown>[]).map((x) => ({
|
||||
id: String(x.id || ""),
|
||||
name: String(x.name || x.id || ""),
|
||||
})),
|
||||
);
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
}
|
||||
})();
|
||||
}, [showError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId) {
|
||||
setBatches([]);
|
||||
setBatchId("");
|
||||
setBaselinePorts([]);
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await bizMigrationListBatches(projectId);
|
||||
const items = (res.items || []) as MigBatch[];
|
||||
setBatches(items);
|
||||
if (!items.find((b) => b.id === batchId)) {
|
||||
setBatchId(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(() => {
|
||||
if (!batchId) {
|
||||
setBoard(null);
|
||||
setDiffs([]);
|
||||
return;
|
||||
}
|
||||
void loadBoard(batchId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [batchId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!oldTaskId) {
|
||||
setOldBatches([]);
|
||||
return;
|
||||
}
|
||||
void bizStateListBatches(oldTaskId, 30).then((r) => {
|
||||
setOldBatches(
|
||||
((r.items || []) as Record<string, unknown>[]).map((x) => ({
|
||||
id: String(x.id || ""),
|
||||
started_at: (x.started_at as string) || null,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}, [oldTaskId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!newTaskId) {
|
||||
setNewBatches([]);
|
||||
return;
|
||||
}
|
||||
void bizStateListBatches(newTaskId, 30).then((r) => {
|
||||
setNewBatches(
|
||||
((r.items || []) as Record<string, unknown>[]).map((x) => ({
|
||||
id: String(x.id || ""),
|
||||
started_at: (x.started_at as string) || null,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}, [newTaskId]);
|
||||
|
||||
async function loadBoard(bid: string, runId = "") {
|
||||
try {
|
||||
const b = await bizMigrationGetBoard(bid, runId);
|
||||
setBoard(b as typeof board);
|
||||
const rid = String((b as { run?: { id?: string } })?.run?.id || "");
|
||||
if (rid) {
|
||||
const d = await bizMigrationListDiffs({ runId: rid, limit: 500 });
|
||||
setDiffs((d.items || []) as DiffRow[]);
|
||||
} else {
|
||||
setDiffs([]);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateProject() {
|
||||
if (!newName.trim() || !oldTaskId || !newTaskId) {
|
||||
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 || "",
|
||||
status: "active",
|
||||
})) as Project;
|
||||
await reloadProjects();
|
||||
setProjectId(p.id);
|
||||
setNewName("");
|
||||
showOk(t("bizMigration.projectCreated"));
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSaveBaseline() {
|
||||
if (!projectId) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await bizMigrationPatchProject(projectId, {
|
||||
old_baseline_batch_id: oldBaselineId || project?.old_baseline_batch_id || "",
|
||||
new_baseline_batch_id: newBaselineId || project?.new_baseline_batch_id || "",
|
||||
mapping_id: mappingId || project?.mapping_id || "",
|
||||
});
|
||||
await reloadProjects();
|
||||
await loadBaselinePorts(projectId);
|
||||
showOk(t("bizMigration.baselineSaved"));
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateBatch() {
|
||||
if (!projectId) return;
|
||||
const ports = [...selectedPorts];
|
||||
if (!ports.length) {
|
||||
showError(t("bizMigration.needExpectPorts"));
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
const b = (await bizMigrationCreateBatch(projectId, {
|
||||
batch_label: batchLabel.trim() || t("bizMigration.defaultBatchLabel"),
|
||||
expect_set: { ports },
|
||||
status: "pending",
|
||||
})) as MigBatch;
|
||||
const res = await bizMigrationListBatches(projectId);
|
||||
setBatches((res.items || []) as MigBatch[]);
|
||||
setBatchId(b.id);
|
||||
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) }));
|
||||
} else {
|
||||
showOk(t("bizMigration.batchCreated"));
|
||||
}
|
||||
await loadRedTickets(projectId);
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function setBatchStatus(status: string) {
|
||||
if (!batchId) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
if (status === "review" || status === "done") {
|
||||
const res = (await bizMigrationFinishBatch(batchId, status === "done")) as {
|
||||
batch?: MigBatch;
|
||||
accept_summary?: MigBatch["accept_summary"];
|
||||
open_red_count?: number;
|
||||
};
|
||||
const list = await bizMigrationListBatches(projectId);
|
||||
setBatches((list.items || []) as MigBatch[]);
|
||||
setAcceptInfo(res.accept_summary || res.batch?.accept_summary || null);
|
||||
await loadRedTickets(projectId);
|
||||
if (res.accept_summary?.passed) {
|
||||
showOk(t("bizMigration.acceptPassed"));
|
||||
} else {
|
||||
showOk(t("bizMigration.acceptFailed", { n: String(res.open_red_count ?? openRedCount) }));
|
||||
}
|
||||
if (res.batch?.accept_run_id) {
|
||||
await loadBoard(batchId, res.batch.accept_run_id);
|
||||
}
|
||||
} else {
|
||||
await bizMigrationPatchBatch(batchId, { status });
|
||||
const res = await bizMigrationListBatches(projectId);
|
||||
setBatches((res.items || []) as MigBatch[]);
|
||||
showOk(t("bizMigration.statusUpdated"));
|
||||
if (status === "active") await loadRedTickets(projectId);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onResolveTicket(id: string) {
|
||||
try {
|
||||
await bizMigrationResolveRedTicket(id);
|
||||
await loadRedTickets(projectId);
|
||||
showOk(t("bizMigration.redResolved"));
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function onEvaluate() {
|
||||
if (!batchId) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const run = await bizMigrationEvaluate(batchId, {});
|
||||
await loadBoard(batchId, String((run as { id?: string }).id || ""));
|
||||
showOk(t("bizMigration.evaluated"));
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onEnsureHighfreq() {
|
||||
if (!projectId) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const res = 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));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onCollectNow() {
|
||||
if (!projectId) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await bizMigrationCollectNow(projectId);
|
||||
showOk(t("bizMigration.collectTriggered"));
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (next.has(name)) next.delete(name);
|
||||
else next.add(name);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function verdictLabel(v: string) {
|
||||
const key = VERDICT_I18N[v];
|
||||
return key ? t(key) : v;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="nm-page" style={{ display: "grid", gap: 16 }}>
|
||||
<div>
|
||||
<h1 style={{ margin: 0, fontSize: "1.25rem" }}>{t("bizMigration.title")}</h1>
|
||||
<p style={{ margin: "4px 0 0", opacity: 0.75, fontSize: 13 }}>{t("bizMigration.hintPort")}</p>
|
||||
</div>
|
||||
|
||||
<section className="nm-card" style={{ display: "grid", gap: 10, padding: 12 }}>
|
||||
<strong>{t("bizMigration.createProject")}</strong>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8 }}>
|
||||
<Input
|
||||
label={t("bizMigration.projectName")}
|
||||
value={newName}
|
||||
onValueChange={setNewName}
|
||||
size="sm"
|
||||
/>
|
||||
<FieldSelect
|
||||
label={t("bizMigration.oldTask")}
|
||||
value={oldTaskId}
|
||||
onChange={(e) => setOldTaskId(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">—</option>
|
||||
{tasks.map((x) => (
|
||||
<option key={x.id} value={x.id}>
|
||||
{x.ne_name || x.id} ({x.ne_ip || "-"})
|
||||
{x.note ? ` · ${x.note}` : ""}
|
||||
{x.interval_sec ? ` · ${x.interval_sec}s` : ""}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
<FieldSelect
|
||||
label={t("bizMigration.newTask")}
|
||||
value={newTaskId}
|
||||
onChange={(e) => setNewTaskId(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">—</option>
|
||||
{tasks.map((x) => (
|
||||
<option key={x.id} value={x.id}>
|
||||
{x.ne_name || x.id} ({x.ne_ip || "-"})
|
||||
{x.note ? ` · ${x.note}` : ""}
|
||||
{x.interval_sec ? ` · ${x.interval_sec}s` : ""}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
<FieldSelect
|
||||
label={t("bizMigration.portMapping")}
|
||||
value={mappingId}
|
||||
onChange={(e) => setMappingId(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">—</option>
|
||||
{mappings.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
<FieldSelect
|
||||
label={t("bizMigration.oldBaseline")}
|
||||
value={oldBaselineId}
|
||||
onChange={(e) => setOldBaselineId(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">—</option>
|
||||
{oldBatches.map((x) => (
|
||||
<option key={x.id} value={x.id}>
|
||||
{String(x.started_at || x.id).slice(0, 32)}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
<FieldSelect
|
||||
label={t("bizMigration.newBaseline")}
|
||||
value={newBaselineId}
|
||||
onChange={(e) => setNewBaselineId(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">—</option>
|
||||
{newBatches.map((x) => (
|
||||
<option key={x.id} value={x.id}>
|
||||
{String(x.started_at || x.id).slice(0, 32)}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
</div>
|
||||
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void onCreateProject()}>
|
||||
{t("bizMigration.createProject")}
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
<section
|
||||
style={{ display: "grid", gridTemplateColumns: "220px 1fr", gap: 12, alignItems: "start" }}
|
||||
>
|
||||
<div className="nm-card" style={{ padding: 10, display: "grid", gap: 6 }}>
|
||||
<strong>{t("bizMigration.projects")}</strong>
|
||||
{projects.length === 0 && (
|
||||
<span style={{ fontSize: 12, opacity: 0.7 }}>{t("bizMigration.emptyProjects")}</span>
|
||||
)}
|
||||
{projects.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => setProjectId(p.id)}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "6px 8px",
|
||||
borderRadius: 6,
|
||||
border: projectId === p.id ? "1px solid #2563eb" : "1px solid transparent",
|
||||
background: projectId === p.id ? "rgba(37,99,235,.08)" : "transparent",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>{p.name}</div>
|
||||
<div style={{ fontSize: 11, opacity: 0.7 }}>
|
||||
{p.old_task?.ne_name || p.old_task_id.slice(0, 6)} →{" "}
|
||||
{p.new_task?.ne_name || p.new_task_id.slice(0, 6)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gap: 12 }}>
|
||||
{project && (
|
||||
<div className="nm-card" style={{ padding: 12, display: "grid", gap: 10 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap" }}>
|
||||
<strong>{project.name}</strong>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
isDisabled={busy}
|
||||
onPress={() => void onEnsureHighfreq()}
|
||||
>
|
||||
{t("bizMigration.enableHighfreq")}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void onCollectNow()}>
|
||||
{t("bizMigration.collectNow")}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void onSaveBaseline()}>
|
||||
{t("bizMigration.saveBaseline")}
|
||||
</Button>
|
||||
<Link
|
||||
to="/network/tasks/biz-state"
|
||||
style={{ fontSize: 12, alignSelf: "center" }}
|
||||
>
|
||||
{t("bizMigration.openBizState")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{(project.old_task || project.new_task) && (
|
||||
<div style={{ fontSize: 12, opacity: 0.8 }}>
|
||||
{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`
|
||||
: ""}
|
||||
</div>
|
||||
)}
|
||||
{openRedCount > 0 && (
|
||||
<div style={{ fontSize: 12, color: COLOR.red }}>
|
||||
{t("bizMigration.openRedHint", { n: String(openRedCount) })}
|
||||
</div>
|
||||
)}
|
||||
<p style={{ margin: 0, fontSize: 12, opacity: 0.7 }}>{t("bizMigration.highfreqHint")}</p>
|
||||
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, marginBottom: 6 }}>
|
||||
{t("bizMigration.pickExpectPorts")} ({selectedPorts.size})
|
||||
</div>
|
||||
<Input
|
||||
size="sm"
|
||||
value={portFilter}
|
||||
onValueChange={setPortFilter}
|
||||
placeholder={t("bizMigration.portFilterPh")}
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
{!project.old_baseline_batch_id && (
|
||||
<div style={{ fontSize: 12, color: COLOR.yellow, marginBottom: 6 }}>
|
||||
{t("bizMigration.needBaselineFirst")}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
maxHeight: 220,
|
||||
overflow: "auto",
|
||||
border: "1px solid rgba(0,0,0,.08)",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<table className="nm-table" style={{ width: "100%", fontSize: 12 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 36 }} />
|
||||
<th>{t("bizMigration.colPort")}</th>
|
||||
<th>admin/phy/prot</th>
|
||||
<th>{t("bizMigration.colMapped")}</th>
|
||||
<th>{t("bizMigration.colDesc")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredBaselinePorts.map((p) => (
|
||||
<tr key={p.interface} onClick={() => togglePort(p.interface)} style={{ cursor: "pointer" }}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPorts.has(p.interface)}
|
||||
onChange={() => togglePort(p.interface)}
|
||||
/>
|
||||
</td>
|
||||
<td>{p.interface}</td>
|
||||
<td>
|
||||
{p.admin || "-"}/{p.phy || "-"}/{p.prot || "-"}
|
||||
</td>
|
||||
<td>{p.mapped_to || "—"}</td>
|
||||
<td style={{ maxWidth: 180, overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{p.description || ""}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredBaselinePorts.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} style={{ opacity: 0.6 }}>
|
||||
{t("bizMigration.emptyBaselinePorts")}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "end" }}>
|
||||
<Input
|
||||
label={t("bizMigration.batchLabel")}
|
||||
value={batchLabel}
|
||||
onValueChange={setBatchLabel}
|
||||
size="sm"
|
||||
placeholder={t("bizMigration.defaultBatchLabel")}
|
||||
style={{ minWidth: 160 }}
|
||||
/>
|
||||
<Button size="sm" isDisabled={busy} onPress={() => void onCreateBatch()}>
|
||||
{t("bizMigration.createBatch")}
|
||||
</Button>
|
||||
<FieldSelect
|
||||
label={t("bizMigration.batches")}
|
||||
value={batchId}
|
||||
onChange={(e) => setBatchId(e.target.value)}
|
||||
>
|
||||
{batches.map((b) => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{b.batch_label} ({b.status})
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
{batch && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={busy}
|
||||
onPress={() => void setBatchStatus("active")}
|
||||
>
|
||||
{t("bizMigration.startBatch")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={busy}
|
||||
onPress={() => void setBatchStatus("review")}
|
||||
>
|
||||
{t("bizMigration.finishBatch")}
|
||||
</Button>
|
||||
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void onEvaluate()}>
|
||||
{t("bizMigration.evaluate")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{acceptInfo && (
|
||||
<div className="nm-card" style={{ padding: 12, display: "grid", gap: 8 }}>
|
||||
<strong>{t("bizMigration.acceptTitle")}</strong>
|
||||
<div style={{ display: "flex", gap: 20, flexWrap: "wrap", fontVariantNumeric: "tabular-nums" }}>
|
||||
<div style={{ color: acceptInfo.passed ? COLOR.green : COLOR.red, fontWeight: 600 }}>
|
||||
{acceptInfo.passed ? t("bizMigration.acceptPassedShort") : t("bizMigration.acceptFailedShort")}
|
||||
</div>
|
||||
<div>
|
||||
{t("bizMigration.progress")}: {acceptInfo.progress_ok ?? 0}/{acceptInfo.progress_total ?? 0}
|
||||
</div>
|
||||
<div style={{ color: COLOR.red }}>
|
||||
{t("bizMigration.anomaly")}: {acceptInfo.anomaly ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.75 }}>{t("bizMigration.acceptCarryHint")}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{redTickets.length > 0 && (
|
||||
<div className="nm-card" style={{ padding: 12, display: "grid", gap: 8 }}>
|
||||
<strong>
|
||||
{t("bizMigration.redTitle")} ({openRedCount} {t("bizMigration.redOpen")})
|
||||
</strong>
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table className="nm-table" style={{ width: "100%", fontSize: 12 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("bizMigration.colOldPort")}</th>
|
||||
<th>{t("bizMigration.colNewPort")}</th>
|
||||
<th>{t("bizMigration.colOldStatus")}</th>
|
||||
<th>{t("bizMigration.colNewStatus")}</th>
|
||||
<th>{t("bizMigration.colVerdict")}</th>
|
||||
<th>{t("bizMigration.colStatus")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{redTickets.map((r) => (
|
||||
<tr key={r.id}>
|
||||
<td>{r.key_str}</td>
|
||||
<td>{r.new_key_str || "—"}</td>
|
||||
<td>{r.old_status || "—"}</td>
|
||||
<td>{r.new_status || "—"}</td>
|
||||
<td style={{ color: COLOR.red, fontWeight: 600 }}>{verdictLabel(r.verdict)}</td>
|
||||
<td>{r.status}</td>
|
||||
<td>
|
||||
{r.status !== "resolved" && (
|
||||
<Button size="sm" variant="secondary" onPress={() => void onResolveTicket(r.id)}>
|
||||
{t("bizMigration.resolveRed")}
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{board?.run && (
|
||||
<div className="nm-card" style={{ padding: 12, display: "grid", gap: 10 }}>
|
||||
<strong>
|
||||
{t("bizMigration.board")} · {t("bizMigration.metricPort")}
|
||||
</strong>
|
||||
<div style={{ display: "flex", gap: 24, fontVariantNumeric: "tabular-nums", flexWrap: "wrap" }}>
|
||||
<div>
|
||||
{t("bizMigration.progress")}:{" "}
|
||||
<b>
|
||||
{board.run.summary?.progress?.ok ?? 0}/{board.run.summary?.progress?.total ?? 0}
|
||||
</b>
|
||||
</div>
|
||||
<div style={{ color: COLOR.red }}>
|
||||
{t("bizMigration.anomaly")}: <b>{board.run.summary?.anomaly ?? 0}</b>
|
||||
</div>
|
||||
{sheetCards[0] && (
|
||||
<div style={{ fontSize: 12, opacity: 0.75 }}>
|
||||
{sheetCards[0].title || sheetCards[0].metric_id}
|
||||
</div>
|
||||
)}
|
||||
<label style={{ fontSize: 12, display: "flex", gap: 6, alignItems: "center" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={onlyExpect}
|
||||
onChange={(e) => setOnlyExpect(e.target.checked)}
|
||||
/>
|
||||
{t("bizMigration.onlyExpect")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table className="nm-table" style={{ width: "100%", fontSize: 12 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("bizMigration.colOldPort")}</th>
|
||||
<th>{t("bizMigration.colNewPort")}</th>
|
||||
<th>{t("bizMigration.colOldStatus")}</th>
|
||||
<th>{t("bizMigration.colNewStatus")}</th>
|
||||
<th>{t("bizMigration.colVerdict")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleDiffs.map((d) => (
|
||||
<tr key={d.id}>
|
||||
<td>{d.key_str || "—"}</td>
|
||||
<td>{d.new_key_str || "—"}</td>
|
||||
<td>{d.old_status || "—"}</td>
|
||||
<td>{d.new_status || "—"}</td>
|
||||
<td style={{ color: COLOR[d.color] || COLOR.gray, fontWeight: 600 }}>
|
||||
{verdictLabel(d.verdict)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{visibleDiffs.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} style={{ opacity: 0.6 }}>
|
||||
{t("bizMigration.emptyDiffs")}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,8 +7,10 @@ import { useDebouncedValue } from "../../hooks/useDebouncedValue";
|
|||
import { useToast } from "../../hooks/useToast";
|
||||
import { useI18n } from "../../i18n";
|
||||
import {
|
||||
bizStateBulkDeleteBatches,
|
||||
bizStateCollectNow,
|
||||
bizStateCreateTask,
|
||||
bizStateDeleteBatch,
|
||||
bizStateDeleteTask,
|
||||
bizStateDiscover,
|
||||
bizStateDownloadExport,
|
||||
|
|
@ -18,6 +20,8 @@ import {
|
|||
bizStateListProfiles,
|
||||
bizStateListTasks,
|
||||
bizStatePatchTask,
|
||||
bizStatePurgeTask,
|
||||
bizStateSetBatchBaseline,
|
||||
bizStateSetBindings,
|
||||
fetchCliTargets,
|
||||
formatErr,
|
||||
|
|
@ -33,6 +37,7 @@ type TaskRow = {
|
|||
ne_name: string;
|
||||
ne_ip: string;
|
||||
vendor: string;
|
||||
note?: string;
|
||||
status: string;
|
||||
collect_running: boolean;
|
||||
last_error: string;
|
||||
|
|
@ -57,6 +62,9 @@ type BatchRow = {
|
|||
row_count: number;
|
||||
command_count: number;
|
||||
started_at?: string | null;
|
||||
is_baseline?: boolean;
|
||||
protected?: boolean;
|
||||
protect_reasons?: string[];
|
||||
};
|
||||
|
||||
type Candidate = { value: string; label: string; rd?: string };
|
||||
|
|
@ -269,7 +277,10 @@ export function BizStatePage() {
|
|||
const [taskTab, setTaskTab] = useState<TaskTab>("profiles");
|
||||
const [intervalValue, setIntervalValue] = useState(1);
|
||||
const [intervalUnit, setIntervalUnit] = useState<"days" | "hours" | "seconds">("hours");
|
||||
const [retentionBatches, setRetentionBatches] = useState(30);
|
||||
const [retentionDays, setRetentionDays] = useState(30);
|
||||
const [dailyKeepEnabled, setDailyKeepEnabled] = useState(false);
|
||||
const [dailyKeepCount, setDailyKeepCount] = useState(10);
|
||||
const [selectedBatchIds, setSelectedBatchIds] = useState<string[]>([]);
|
||||
|
||||
// VRF bind (inside task modal)
|
||||
const [bindItemId, setBindItemId] = useState("");
|
||||
|
|
@ -333,7 +344,7 @@ export function BizStatePage() {
|
|||
if (!kw) return tasks;
|
||||
return tasks.filter((row) => {
|
||||
const blob =
|
||||
`${row.ne_name} ${row.ne_ip} ${row.vendor} ${row.status} ${row.source || ""} ${row.last_error}`.toLowerCase();
|
||||
`${row.ne_name} ${row.ne_ip} ${row.vendor} ${row.note || ""} ${row.status} ${row.source || ""} ${row.last_error}`.toLowerCase();
|
||||
return blob.includes(kw);
|
||||
});
|
||||
}, [tasks, debouncedListKw]);
|
||||
|
|
@ -382,9 +393,12 @@ export function BizStatePage() {
|
|||
const ui = secToIntervalUi(Number(task.interval_sec || 3600));
|
||||
setIntervalValue(ui.value);
|
||||
setIntervalUnit(ui.unit);
|
||||
setRetentionBatches(Math.max(1, Number(task.retention_batches || 30)));
|
||||
const b = await bizStateListBatches(id);
|
||||
setRetentionDays(Math.max(1, Number(task.retention_days || 30)));
|
||||
setDailyKeepEnabled(Boolean(task.daily_keep_enabled));
|
||||
setDailyKeepCount(Math.max(1, Number(task.daily_keep_count || 10)));
|
||||
const b = await bizStateListBatches(id, 200);
|
||||
setBatches((b.items || []) as BatchRow[]);
|
||||
setSelectedBatchIds([]);
|
||||
const p = await bizStateListProfiles({
|
||||
vendor: task.vendor || "",
|
||||
device_type: task.device_type || "",
|
||||
|
|
@ -457,7 +471,9 @@ export function BizStatePage() {
|
|||
try {
|
||||
await bizStatePatchTask(taskId, {
|
||||
interval_sec: intervalUiToSec(intervalValue, intervalUnit),
|
||||
retention_batches: Math.max(1, Number(retentionBatches) || 30),
|
||||
retention_days: Math.max(1, Number(retentionDays) || 30),
|
||||
daily_keep_enabled: dailyKeepEnabled,
|
||||
daily_keep_count: Math.max(1, Number(dailyKeepCount) || 10),
|
||||
});
|
||||
showOk(t("bizState.scheduleSaved"));
|
||||
await loadTask(taskId);
|
||||
|
|
@ -469,6 +485,82 @@ export function BizStatePage() {
|
|||
}
|
||||
};
|
||||
|
||||
const toggleBatchSelect = (id: string, protectedBatch: boolean) => {
|
||||
if (protectedBatch) return;
|
||||
setSelectedBatchIds((prev) =>
|
||||
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
const selectDeletableBatches = () => {
|
||||
setSelectedBatchIds(batches.filter((b) => !b.protected).map((b) => b.id));
|
||||
};
|
||||
|
||||
const markBaseline = async (batchId: string, marked: boolean) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await bizStateSetBatchBaseline(batchId, marked);
|
||||
if (taskId) await loadTask(taskId);
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeBatch = async (batchId: string) => {
|
||||
if (!window.confirm(t("bizState.confirmDeleteBatch"))) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await bizStateDeleteBatch(batchId);
|
||||
showOk(t("bizState.batchDeleted"));
|
||||
if (taskId) await loadTask(taskId);
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const bulkRemoveBatches = async () => {
|
||||
if (!selectedBatchIds.length) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
t("bizState.confirmBulkDelete").replace("{{count}}", String(selectedBatchIds.length)),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
const res = await bizStateBulkDeleteBatches(selectedBatchIds);
|
||||
showOk(
|
||||
t("bizState.bulkDeleted")
|
||||
.replace("{{deleted}}", String(res.deleted_count || 0))
|
||||
.replace("{{skipped}}", String((res.skipped || []).length)),
|
||||
);
|
||||
if (taskId) await loadTask(taskId);
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const purgeNow = async () => {
|
||||
if (!taskId) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const res = await bizStatePurgeTask(taskId);
|
||||
showOk(t("bizState.purgeOk").replace("{{dropped}}", String(res.dropped || 0)));
|
||||
await loadTask(taskId);
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const collectNow = async () => {
|
||||
if (!taskId) return;
|
||||
setBusy(true);
|
||||
|
|
@ -670,6 +762,7 @@ export function BizStatePage() {
|
|||
<div className="pt-list-task-name">{row.ne_name || row.ne_ip || "—"}</div>
|
||||
<div className="muted">
|
||||
{row.vendor || "—"} · {row.ne_ip || "—"}
|
||||
{row.note ? ` · ${row.note}` : ""}
|
||||
</div>
|
||||
{row.last_error ? <div className="form-error">{row.last_error}</div> : null}
|
||||
</td>
|
||||
|
|
@ -939,16 +1032,41 @@ export function BizStatePage() {
|
|||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={200}
|
||||
value={String(retentionBatches)}
|
||||
onChange={(e) => setRetentionBatches(Math.max(1, Number(e.target.value) || 1))}
|
||||
max={3650}
|
||||
value={String(retentionDays)}
|
||||
onChange={(e) => setRetentionDays(Math.max(1, Number(e.target.value) || 1))}
|
||||
/>
|
||||
</label>
|
||||
<label className="config-sync-policy-field" style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dailyKeepEnabled}
|
||||
disabled={busy}
|
||||
onChange={(e) => setDailyKeepEnabled(e.target.checked)}
|
||||
/>
|
||||
<span>{t("bizState.dailyKeepEnabled")}</span>
|
||||
</label>
|
||||
{dailyKeepEnabled ? (
|
||||
<label className="config-sync-policy-field">
|
||||
<span>{t("bizState.dailyKeepCount")}</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={1000}
|
||||
value={String(dailyKeepCount)}
|
||||
onChange={(e) => setDailyKeepCount(Math.max(1, Number(e.target.value) || 1))}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void saveSchedule()}>
|
||||
{t("bizState.saveSchedule")}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" isDisabled={busy} onPress={() => void purgeNow()}>
|
||||
{t("bizState.purgeNow")}
|
||||
</Button>
|
||||
<span className="muted">
|
||||
{detail.vendor || "—"} · {detail.ne_ip || "—"} · {t("bizState.scheduleHint")}
|
||||
{detail.vendor || "—"} · {detail.ne_ip || "—"} · {t("bizState.retentionHint")}
|
||||
{dailyKeepEnabled ? ` · ${t("bizState.dailyKeepHint")}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -1086,41 +1204,104 @@ export function BizStatePage() {
|
|||
</>
|
||||
) : (
|
||||
<div className="pt-list-table-wrap">
|
||||
<div className="btn-row" style={{ marginBottom: 8, gap: 8, flexWrap: "wrap" }}>
|
||||
<Button size="sm" variant="ghost" isDisabled={busy || !batches.length} onPress={selectDeletableBatches}>
|
||||
{t("bizState.selectAll")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
isDisabled={busy || !selectedBatchIds.length}
|
||||
onPress={() => void bulkRemoveBatches()}
|
||||
>
|
||||
{t("bizState.bulkDelete")}
|
||||
{selectedBatchIds.length ? ` (${selectedBatchIds.length})` : ""}
|
||||
</Button>
|
||||
</div>
|
||||
<table className="data-table pt-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 36 }} />
|
||||
<th>{t("bizState.colTime")}</th>
|
||||
<th>{t("bizState.colStatus")}</th>
|
||||
<th>{t("bizState.colProtect")}</th>
|
||||
<th>{t("bizState.colRows")}</th>
|
||||
<th>{t("bizState.colActions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{batches.map((b) => (
|
||||
<tr key={b.id}>
|
||||
<td className="pt-list-time">{fmtTime(b.started_at)}</td>
|
||||
<td>
|
||||
<NmStatusChip color={jobChipColor(b.status)}>{b.status}</NmStatusChip>
|
||||
</td>
|
||||
<td className="pt-list-num">
|
||||
{b.row_count}
|
||||
<span className="muted"> / {b.command_count} cmd</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="pt-list-actions">
|
||||
<Button size="sm" variant="primary" onPress={() => void openBatch(b.id)}>
|
||||
{t("bizState.viewBatch")}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onPress={() => void bizStateDownloadExport(b.id)}>
|
||||
{t("bizState.export")}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{batches.map((b) => {
|
||||
const locked = Boolean(b.protected);
|
||||
return (
|
||||
<tr key={b.id}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedBatchIds.includes(b.id)}
|
||||
disabled={locked || busy}
|
||||
onChange={() => toggleBatchSelect(b.id, locked)}
|
||||
/>
|
||||
</td>
|
||||
<td className="pt-list-time">{fmtTime(b.started_at)}</td>
|
||||
<td>
|
||||
<NmStatusChip color={jobChipColor(b.status)}>{b.status}</NmStatusChip>
|
||||
</td>
|
||||
<td>
|
||||
{b.is_baseline ? (
|
||||
<NmStatusChip color="accent">{t("bizState.baseline")}</NmStatusChip>
|
||||
) : locked ? (
|
||||
<span className="muted">{t("bizState.protected")}</span>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td className="pt-list-num">
|
||||
{b.row_count}
|
||||
<span className="muted"> / {b.command_count} cmd</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="pt-list-actions">
|
||||
<Button size="sm" variant="primary" onPress={() => void openBatch(b.id)}>
|
||||
{t("bizState.viewBatch")}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onPress={() => void bizStateDownloadExport(b.id)}>
|
||||
{t("bizState.export")}
|
||||
</Button>
|
||||
{b.is_baseline ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={busy}
|
||||
onPress={() => void markBaseline(b.id, false)}
|
||||
>
|
||||
{t("bizState.unmarkBaseline")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={busy}
|
||||
onPress={() => void markBaseline(b.id, true)}
|
||||
>
|
||||
{t("bizState.markBaseline")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
isDisabled={busy || locked}
|
||||
onPress={() => void removeBatch(b.id)}
|
||||
>
|
||||
{t("bizState.deleteBatch")}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{!batches.length ? (
|
||||
<tr>
|
||||
<td colSpan={4}>
|
||||
<td colSpan={6}>
|
||||
<div className="pt-list-empty">{t("bizState.noBatches")}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -1813,6 +1813,29 @@ export const bizStateListBatches = (taskId: string, limit = 50) =>
|
|||
`/v1/biz-state/tasks/${encodeURIComponent(taskId)}/batches?limit=${limit}`,
|
||||
);
|
||||
|
||||
export const bizStateSetBatchBaseline = (batchId: string, marked: boolean) =>
|
||||
apiPost<Record<string, unknown>>(
|
||||
`/v1/biz-state/batches/${encodeURIComponent(batchId)}/baseline`,
|
||||
{ marked },
|
||||
);
|
||||
|
||||
export const bizStateDeleteBatch = (batchId: string) =>
|
||||
apiDelete<{ ok: boolean }>(`/v1/biz-state/batches/${encodeURIComponent(batchId)}`);
|
||||
|
||||
export const bizStateBulkDeleteBatches = (batchIds: string[]) =>
|
||||
apiPost<{
|
||||
ok: boolean;
|
||||
deleted: string[];
|
||||
skipped: { batch_id: string; reasons: string[] }[];
|
||||
deleted_count: number;
|
||||
}>("/v1/biz-state/batches/bulk-delete", { batch_ids: batchIds });
|
||||
|
||||
export const bizStatePurgeTask = (taskId: string) =>
|
||||
apiPost<Record<string, unknown>>(
|
||||
`/v1/biz-state/tasks/${encodeURIComponent(taskId)}/purge`,
|
||||
{},
|
||||
);
|
||||
|
||||
export const bizStateGetBatch = (batchId: string) =>
|
||||
apiGet<Record<string, unknown>>(`/v1/biz-state/batches/${encodeURIComponent(batchId)}`);
|
||||
|
||||
|
|
@ -1959,3 +1982,117 @@ export const bizCompareDownloadRun = async (runId: string): Promise<void> => {
|
|||
URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
|
||||
// --- biz-migration (cutover monitor, separate from compare) ---
|
||||
|
||||
export const bizMigrationListProjects = () =>
|
||||
apiGet<{ items: Record<string, unknown>[] }>("/v1/biz-migration/projects");
|
||||
|
||||
export const bizMigrationCreateProject = (body: Record<string, unknown>) =>
|
||||
apiPost<Record<string, unknown>>("/v1/biz-migration/projects", body);
|
||||
|
||||
export const bizMigrationPatchProject = (projectId: string, body: Record<string, unknown>) =>
|
||||
apiPatch<Record<string, unknown>>(
|
||||
`/v1/biz-migration/projects/${encodeURIComponent(projectId)}`,
|
||||
body,
|
||||
);
|
||||
|
||||
export const bizMigrationListBatches = (projectId: string) =>
|
||||
apiGet<{ items: Record<string, unknown>[] }>(
|
||||
`/v1/biz-migration/projects/${encodeURIComponent(projectId)}/batches`,
|
||||
);
|
||||
|
||||
export const bizMigrationListBaselinePorts = (projectId: string) =>
|
||||
apiGet<{
|
||||
batch_id: string;
|
||||
metric_id: string;
|
||||
ports: {
|
||||
interface: string;
|
||||
admin?: string;
|
||||
phy?: string;
|
||||
prot?: string;
|
||||
description?: string;
|
||||
mapped_to?: string;
|
||||
}[];
|
||||
}>(`/v1/biz-migration/projects/${encodeURIComponent(projectId)}/baseline-ports`);
|
||||
|
||||
export const bizMigrationEnsurePortHighfreq = (
|
||||
projectId: string,
|
||||
body: { interval_sec?: number; retention_days?: number; collect_now?: boolean } = {},
|
||||
) =>
|
||||
apiPost<Record<string, unknown>>(
|
||||
`/v1/biz-migration/projects/${encodeURIComponent(projectId)}/ensure-port-highfreq`,
|
||||
body,
|
||||
);
|
||||
|
||||
export const bizMigrationCollectNow = (projectId: string) =>
|
||||
apiPost<Record<string, unknown>>(
|
||||
`/v1/biz-migration/projects/${encodeURIComponent(projectId)}/collect-now`,
|
||||
{},
|
||||
);
|
||||
|
||||
export const bizMigrationFinishBatch = (batchId: string, markDone = false) =>
|
||||
apiPost<Record<string, unknown>>(
|
||||
`/v1/biz-migration/batches/${encodeURIComponent(batchId)}/finish?mark_done=${markDone ? "true" : "false"}`,
|
||||
{},
|
||||
);
|
||||
|
||||
export const bizMigrationListRedTickets = (projectId: string, status = "") => {
|
||||
const q = status ? `?status=${encodeURIComponent(status)}` : "";
|
||||
return apiGet<{ open_count: number; items: Record<string, unknown>[] }>(
|
||||
`/v1/biz-migration/projects/${encodeURIComponent(projectId)}/red-tickets${q}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const bizMigrationResolveRedTicket = (ticketId: string, note = "") =>
|
||||
apiPost<Record<string, unknown>>(
|
||||
`/v1/biz-migration/red-tickets/${encodeURIComponent(ticketId)}/resolve`,
|
||||
{ note },
|
||||
);
|
||||
|
||||
export const bizMigrationCreateBatch = (projectId: string, body: Record<string, unknown>) =>
|
||||
apiPost<Record<string, unknown>>(
|
||||
`/v1/biz-migration/projects/${encodeURIComponent(projectId)}/batches`,
|
||||
body,
|
||||
);
|
||||
|
||||
export const bizMigrationPatchBatch = (batchId: string, body: Record<string, unknown>) =>
|
||||
apiPatch<Record<string, unknown>>(
|
||||
`/v1/biz-migration/batches/${encodeURIComponent(batchId)}`,
|
||||
body,
|
||||
);
|
||||
|
||||
export const bizMigrationEvaluate = (batchId: string, body: Record<string, unknown> = {}) =>
|
||||
apiPost<Record<string, unknown>>(
|
||||
`/v1/biz-migration/batches/${encodeURIComponent(batchId)}/evaluate`,
|
||||
body,
|
||||
);
|
||||
|
||||
export const bizMigrationGetBoard = (batchId: string, runId = "") => {
|
||||
const q = runId ? `?run_id=${encodeURIComponent(runId)}` : "";
|
||||
return apiGet<Record<string, unknown>>(
|
||||
`/v1/biz-migration/batches/${encodeURIComponent(batchId)}/board${q}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const bizMigrationListDiffs = (params: {
|
||||
runId: string;
|
||||
metricId?: string;
|
||||
verdict?: string;
|
||||
color?: string;
|
||||
kw?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const p = new URLSearchParams();
|
||||
if (params.metricId) p.set("metric_id", params.metricId);
|
||||
if (params.verdict) p.set("verdict", params.verdict);
|
||||
if (params.color) p.set("color", params.color);
|
||||
if (params.kw) p.set("kw", params.kw);
|
||||
if (params.offset != null) p.set("offset", String(params.offset));
|
||||
if (params.limit != null) p.set("limit", String(params.limit));
|
||||
const qs = p.toString();
|
||||
return apiGet<{ items: Record<string, unknown>[]; total: number }>(
|
||||
`/v1/biz-migration/runs/${encodeURIComponent(params.runId)}/diffs${qs ? `?${qs}` : ""}`,
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue