Fix empty BGP route false positives and add batch alias UX.

Skip Dest/Network headers when Total routes is 0; capture direction from match groups; expose workbook compare/data/sheet stats and prefer batch aliases in compare/cutover pickers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-22 10:58:57 +08:00
parent f321413dbf
commit 1fcd613bc6
16 changed files with 327 additions and 39 deletions

View file

@ -215,6 +215,12 @@ const en = {
unmarkBaseline: "Unmark baseline",
baseline: "Baseline",
protected: "Protected",
colAlias: "Alias",
setAlias: "Set alias",
aliasPrompt: "Batch alias (preferred in compare/cutover pickers; empty clears)",
aliasSaved: "Alias saved",
aliasEmpty: "Untitled",
workbookStats: "Compare {{compare}} · With data {{withData}} · Sheets {{sheets}}",
deleteBatch: "Delete",
bulkDelete: "Bulk delete",
confirmDeleteBatch: "Delete this batch? Protected batches cannot be deleted.",

View file

@ -215,6 +215,12 @@ const zh = {
unmarkBaseline: "撤销基线",
baseline: "基线",
protected: "已保护",
colAlias: "别名",
setAlias: "设别名",
aliasPrompt: "批次别名(比对/割接选择时优先显示;留空清除)",
aliasSaved: "别名已保存",
aliasEmpty: "未命名",
workbookStats: "对比项 {{compare}} · 有数据 {{withData}} · 表格 {{sheets}}",
deleteBatch: "删除",
bulkDelete: "批量删除",
confirmDeleteBatch: "删除该采集批次?受保护的批次无法删除。",

View file

@ -9953,6 +9953,31 @@ html.login-page--paused .login-page__flare {
gap: 8px 12px;
}
.bs-alias-link {
appearance: none;
border: 0;
background: transparent;
padding: 0;
margin: 0;
color: #93c5fd;
cursor: pointer;
text-align: left;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bs-alias-link:hover {
text-decoration: underline;
}
.bs-alias-link:disabled {
cursor: default;
opacity: 0.6;
text-decoration: none;
}
.bs-id-row {
display: flex;
flex-wrap: wrap;
@ -14297,8 +14322,8 @@ option {
}
.app-heroui-modal .pt-list-table.bs-profiles-table {
width: max-content !important;
min-width: max(100%, 1280px) !important;
width: 100% !important;
min-width: 960px !important;
table-layout: fixed !important;
}

View file

@ -40,7 +40,13 @@ type CreateJobStep = 0 | 1 | 2 | 3;
const CREATE_JOB_STEPS = 4;
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 BatchOpt = {
id: string;
status: string;
row_count: number;
started_at?: string | null;
alias?: string;
};
type MetricField = {
name: string;
display_name: string;
@ -306,6 +312,14 @@ function taskLabel(row: TaskOpt) {
return `${row.ne_name || row.ne_ip || row.id} (${row.vendor || "-"})`;
}
function batchOptLabel(b: BatchOpt) {
const alias = String(b.alias || "").trim();
const when = fmtTime(b.started_at);
const tail = `${b.status} · rows=${b.row_count}`;
if (alias) return `${alias} · ${when} · ${tail}`;
return `${when} · ${tail}`;
}
function sheetIdentity(s: { sheet_id?: string; metric_id?: string } | null | undefined): string {
return String(s?.sheet_id || s?.metric_id || "").trim();
}
@ -1929,7 +1943,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
<option value="">{t("bizCompare.pick")}</option>
{beforeBatches.map((b) => (
<option key={b.id} value={b.id}>
{fmtTime(b.started_at)} · {b.status} · rows={b.row_count}
{batchOptLabel(b)}
</option>
))}
</FieldSelect>
@ -1956,7 +1970,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
<option value="">{t("bizCompare.pick")}</option>
{afterBatches.map((b) => (
<option key={b.id} value={b.id}>
{fmtTime(b.started_at)} · {b.status} · rows={b.row_count}
{batchOptLabel(b)}
</option>
))}
</FieldSelect>

View file

@ -382,8 +382,12 @@ export function BizMigrationPage() {
const [createHfEndAt, setCreateHfEndAt] = 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 [createOldBatches, setCreateOldBatches] = useState<
{ id: string; started_at?: string | null; alias?: string }[]
>([]);
const [createNewBatches, setCreateNewBatches] = useState<
{ id: string; started_at?: string | null; alias?: string }[]
>([]);
const [createOldHasPortrait, setCreateOldHasPortrait] = useState(false);
const [createNewHasPortrait, setCreateNewHasPortrait] = useState(false);
const [monitorTpls, setMonitorTpls] = useState<MonitorTplOpt[]>([]);
@ -402,10 +406,10 @@ export function BizMigrationPage() {
const [hfEndAt, setHfEndAt] = 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 [hfOldBatches, setHfOldBatches] = useState<{ id: string; started_at?: string | null }[]>([]);
const [hfNewBatches, setHfNewBatches] = useState<{ id: string; started_at?: string | null }[]>([]);
const [oldBatches, setOldBatches] = useState<{ id: string; started_at?: string | null; alias?: string }[]>([]);
const [newBatches, setNewBatches] = useState<{ id: string; started_at?: string | null; alias?: string }[]>([]);
const [hfOldBatches, setHfOldBatches] = useState<{ id: string; started_at?: string | null; alias?: string }[]>([]);
const [hfNewBatches, setHfNewBatches] = useState<{ id: string; started_at?: string | null; alias?: string }[]>([]);
const [batches, setBatches] = useState<MigBatch[]>([]);
const [batchId, setBatchId] = useState("");
const [board, setBoard] = useState<{
@ -751,6 +755,7 @@ export function BizMigrationPage() {
(r.batches || []).map((x) => ({
id: String(x.id || ""),
started_at: x.started_at || null,
alias: String(x.alias || ""),
})),
);
});
@ -773,6 +778,7 @@ export function BizMigrationPage() {
(r.batches || []).map((x) => ({
id: String(x.id || ""),
started_at: x.started_at || null,
alias: String(x.alias || ""),
})),
);
});
@ -788,6 +794,7 @@ export function BizMigrationPage() {
((r.items || []) as Record<string, unknown>[]).map((x) => ({
id: String(x.id || ""),
started_at: (x.started_at as string) || null,
alias: String(x.alias || ""),
})),
);
});
@ -803,6 +810,7 @@ export function BizMigrationPage() {
((r.items || []) as Record<string, unknown>[]).map((x) => ({
id: String(x.id || ""),
started_at: (x.started_at as string) || null,
alias: String(x.alias || ""),
})),
);
});
@ -820,13 +828,17 @@ export function BizMigrationPage() {
}
void Promise.all(uniq.map((tid) => bizStateListBatches(tid, 30))).then((results) => {
const seen = new Set<string>();
const items: { id: string; started_at?: string | null }[] = [];
const items: { id: string; started_at?: string | null; alias?: string }[] = [];
for (const r of results) {
for (const x of (r.items || []) as Record<string, unknown>[]) {
const id = String(x.id || "");
if (!id || seen.has(id)) continue;
seen.add(id);
items.push({ id, started_at: (x.started_at as string) || null });
items.push({
id,
started_at: (x.started_at as string) || null,
alias: String(x.alias || ""),
});
}
}
setHfOldBatches(items);
@ -845,13 +857,17 @@ export function BizMigrationPage() {
}
void Promise.all(uniq.map((tid) => bizStateListBatches(tid, 30))).then((results) => {
const seen = new Set<string>();
const items: { id: string; started_at?: string | null }[] = [];
const items: { id: string; started_at?: string | null; alias?: string }[] = [];
for (const r of results) {
for (const x of (r.items || []) as Record<string, unknown>[]) {
const id = String(x.id || "");
if (!id || seen.has(id)) continue;
seen.add(id);
items.push({ id, started_at: (x.started_at as string) || null });
items.push({
id,
started_at: (x.started_at as string) || null,
alias: String(x.alias || ""),
});
}
}
setHfNewBatches(items);
@ -1424,12 +1440,19 @@ export function BizMigrationPage() {
return [...set];
}, [monitorTpls, createMonitorTplId, createCollectMetricIds]);
const batchOptions = (items: { id: string; started_at?: string | null }[]) => (
const batchOptions = (items: { id: string; started_at?: string | null; alias?: string }[]) => (
<>
<option value="">—</option>
{items.map((x) => {
const alias = String(x.alias || "").trim();
const when = fmtTime(x.started_at);
const label = when !== "—" ? `${when} · ${x.id}` : x.id;
const label = alias
? when !== "—"
? `${alias} · ${when}`
: alias
: when !== "—"
? `${when} · ${x.id}`
: x.id;
return (
<option key={x.id} value={x.id} title={x.id}>
{label}

View file

@ -25,11 +25,13 @@ import {
bizStatePauseTask,
bizStatePurgeTask,
bizStateSetBatchBaseline,
bizStateSetBatchAlias,
bizStateSetBindings,
bizStateStartTask,
fetchCliTargets,
formatErr,
} from "../../services/api";
import type { CliTargetItem } from "../../types";
import { pageCount } from "../../utils/display";
import { writeClipboardText } from "../../utils/clipboard";
@ -76,6 +78,7 @@ type BatchRow = {
is_baseline?: boolean;
protected?: boolean;
protect_reasons?: string[];
alias?: string;
};
type Candidate = {
@ -617,6 +620,24 @@ export function BizStatePage() {
}
};
const editBatchAlias = async (batchId: string, current?: string) => {
const next = window.prompt(t("bizState.aliasPrompt"), String(current || ""));
if (next === null) return;
setBusy(true);
try {
await bizStateSetBatchAlias(batchId, next.trim());
showOk(t("bizState.aliasSaved"));
if (taskId) await loadTask(taskId);
if (batchDetail?.id === batchId) {
setBatchDetail((prev: any) => (prev ? { ...prev, alias: next.trim() } : prev));
}
} catch (e) {
showError(formatErr(e));
} finally {
setBusy(false);
}
};
const removeBatch = async (batchId: string) => {
if (!window.confirm(t("bizState.confirmDeleteBatch"))) return;
setBusy(true);
@ -1528,6 +1549,7 @@ export function BizStatePage() {
<tr>
<th style={{ width: 36 }} />
<th>{t("bizState.colTime")}</th>
<th>{t("bizState.colAlias")}</th>
<th>{t("bizState.colStatus")}</th>
<th>{t("bizState.colProtect")}</th>
<th>{t("bizState.colRows")}</th>
@ -1548,6 +1570,17 @@ export function BizStatePage() {
/>
</td>
<td className="pt-list-time">{fmtTime(b.started_at)}</td>
<td>
<button
type="button"
className="bs-alias-link"
disabled={busy}
title={t("bizState.setAlias")}
onClick={() => void editBatchAlias(b.id, b.alias)}
>
{b.alias?.trim() ? b.alias : t("bizState.aliasEmpty")}
</button>
</td>
<td>
<NmStatusChip color={jobChipColor(b.status)}>{b.status}</NmStatusChip>
</td>
@ -1572,6 +1605,14 @@ export function BizStatePage() {
<Button size="sm" variant="ghost" onPress={() => void bizStateDownloadExport(b.id)}>
{t("bizState.export")}
</Button>
<Button
size="sm"
variant="ghost"
isDisabled={busy}
onPress={() => void editBatchAlias(b.id, b.alias)}
>
{t("bizState.setAlias")}
</Button>
{b.is_baseline ? (
<Button
size="sm"
@ -1606,7 +1647,7 @@ export function BizStatePage() {
})}
{!batches.length ? (
<tr>
<td colSpan={6}>
<td colSpan={7}>
<div className="pt-list-empty">{t("bizState.noBatches")}</div>
</td>
</tr>
@ -1791,7 +1832,16 @@ export function BizStatePage() {
<NmStatusChip color={jobChipColor(String(batchDetail.status || ""))}>
{String(batchDetail.status || "—")}
</NmStatusChip>
{batchDetail.alias ? (
<NmStatusChip color="accent">{String(batchDetail.alias)}</NmStatusChip>
) : null}
<span className="muted">
{t("bizState.workbookStats", {
compare: Number(batchDetail.compare_item_count ?? batchDetail.sheet_count ?? 0),
withData: Number(batchDetail.sheets_with_data ?? 0),
sheets: Number(batchDetail.sheet_count ?? 0),
})}
{" · "}
{batchDetail.command_count} cmd · {batchDetail.row_count} rows ·{" "}
{fmtTime(batchDetail.started_at)}
</span>
@ -1812,6 +1862,14 @@ export function BizStatePage() {
>
{t("bizState.copyBatchId")}
</Button>
<Button
size="sm"
variant="ghost"
isDisabled={busy}
onPress={() => void editBatchAlias(String(batchDetail.id || ""), String(batchDetail.alias || ""))}
>
{t("bizState.setAlias")}
</Button>
</div>
</div>
) : null}

View file

@ -1837,6 +1837,12 @@ export const bizStateSetBatchBaseline = (batchId: string, marked: boolean) =>
{ marked },
);
export const bizStateSetBatchAlias = (batchId: string, alias: string) =>
apiPatch<Record<string, unknown>>(
`/v1/biz-state/batches/${encodeURIComponent(batchId)}/alias`,
{ alias },
);
export const bizStateDeleteBatch = (batchId: string) =>
apiDelete<{ ok: boolean }>(`/v1/biz-state/batches/${encodeURIComponent(batchId)}`);
@ -2063,7 +2069,13 @@ export const bizMigrationNePortrait = (params: { source: string; ne_id: string;
source: string;
ne_id: string;
task: Record<string, unknown> | null;
batches: Array<{ id: string; started_at?: string | null; status?: string; row_count?: number }>;
batches: Array<{
id: string;
started_at?: string | null;
status?: string;
row_count?: number;
alias?: string;
}>;
hint?: string;
}>(`/v1/biz-migration/ne-portrait?${q.toString()}`);
};