mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
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:
parent
f321413dbf
commit
1fcd613bc6
16 changed files with 327 additions and 39 deletions
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -215,6 +215,12 @@ const zh = {
|
|||
unmarkBaseline: "撤销基线",
|
||||
baseline: "基线",
|
||||
protected: "已保护",
|
||||
colAlias: "别名",
|
||||
setAlias: "设别名",
|
||||
aliasPrompt: "批次别名(比对/割接选择时优先显示;留空清除)",
|
||||
aliasSaved: "别名已保存",
|
||||
aliasEmpty: "未命名",
|
||||
workbookStats: "对比项 {{compare}} · 有数据 {{withData}} · 表格 {{sheets}}",
|
||||
deleteBatch: "删除",
|
||||
bulkDelete: "批量删除",
|
||||
confirmDeleteBatch: "删除该采集批次?受保护的批次无法删除。",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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()}`);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue