mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 04:20:45 +08:00
Allow compare templates to split one collected metric into filtered sheets.
BGP/ISIS (and any other metric) can share a metric_id with distinct sheet_id + row_filters; drop conflicting bgp state_or_pfx and keep state/pfx_rcd. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
88b7558a48
commit
a65c9fc420
17 changed files with 745 additions and 230 deletions
|
|
@ -345,6 +345,11 @@ const en = {
|
|||
pickMetric: "Pick metric…",
|
||||
addSheet: "Add to template",
|
||||
removeSheet: "Remove sheet",
|
||||
sheetIdRequired: "sheet_id is required (unique per split)",
|
||||
duplicateSheetId: "sheet_id must be unique",
|
||||
sheetTitle: "Sheet title",
|
||||
sheetId: "sheet_id",
|
||||
splitSheet: "Split / duplicate",
|
||||
colSheets: "Sheets",
|
||||
sheetsUnit: "sheets",
|
||||
presenceShort: "presence",
|
||||
|
|
|
|||
|
|
@ -344,6 +344,11 @@ const zh = {
|
|||
pickMetric: "选择监控项…",
|
||||
addSheet: "加入模板",
|
||||
removeSheet: "移出本表",
|
||||
sheetIdRequired: "请填写 sheet_id(拆表唯一标识)",
|
||||
duplicateSheetId: "sheet_id 不能重复",
|
||||
sheetTitle: "表标题",
|
||||
sheetId: "sheet_id",
|
||||
splitSheet: "拆表复制",
|
||||
colSheets: "监控表",
|
||||
sheetsUnit: "张表",
|
||||
presenceShort: "仅条目",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ type MetricField = {
|
|||
type MetricSchema = { metric_id: string; fields: MetricField[] };
|
||||
|
||||
type MetricSheet = {
|
||||
sheet_id?: string;
|
||||
title?: string;
|
||||
metric_id: string;
|
||||
key_fields: string[];
|
||||
iface_fields: string[];
|
||||
|
|
@ -111,6 +113,8 @@ type DiffRow = {
|
|||
};
|
||||
|
||||
type RunSheet = {
|
||||
sheet_id?: string;
|
||||
title?: string;
|
||||
metric_id: string;
|
||||
key_fields: string[];
|
||||
iface_fields: string[];
|
||||
|
|
@ -285,12 +289,22 @@ function taskLabel(row: TaskOpt) {
|
|||
return `${row.ne_name || row.ne_ip || row.id} (${row.vendor || "-"})`;
|
||||
}
|
||||
|
||||
function sheetIdentity(s: { sheet_id?: string; metric_id?: string } | null | undefined): string {
|
||||
return String(s?.sheet_id || s?.metric_id || "").trim();
|
||||
}
|
||||
|
||||
function sheetLabel(s: { title?: string; sheet_id?: string; metric_id?: string } | null | undefined): string {
|
||||
return String(s?.title || s?.sheet_id || s?.metric_id || "").trim() || "—";
|
||||
}
|
||||
|
||||
function templateSheets(tpl?: Template | null): MetricSheet[] {
|
||||
if (!tpl) return [];
|
||||
if (tpl.metrics?.length) return tpl.metrics;
|
||||
if (tpl.metric_id) {
|
||||
return [
|
||||
{
|
||||
sheet_id: tpl.metric_id,
|
||||
title: tpl.metric_id,
|
||||
metric_id: tpl.metric_id,
|
||||
key_fields: [...(tpl.key_fields || [])],
|
||||
iface_fields: [...(tpl.iface_fields || [])],
|
||||
|
|
@ -311,6 +325,8 @@ function defaultSheetForMetric(schema: MetricSchema | undefined, metricId: strin
|
|||
.filter((f) => !f.is_key && (f.role === "state" || f.role === "meta"))
|
||||
.map((f) => f.name);
|
||||
const sheet: MetricSheet = {
|
||||
sheet_id: metricId,
|
||||
title: metricId,
|
||||
metric_id: metricId,
|
||||
key_fields,
|
||||
iface_fields: fields.filter((f) => f.is_interface).map((f) => f.name),
|
||||
|
|
@ -353,6 +369,8 @@ function cloneSheet(s: MetricSheet): MetricSheet {
|
|||
? [...s.display_fields]
|
||||
: [...keys, ...compare];
|
||||
return {
|
||||
sheet_id: s.sheet_id || s.metric_id,
|
||||
title: s.title || s.sheet_id || s.metric_id,
|
||||
metric_id: s.metric_id,
|
||||
key_fields: keys,
|
||||
iface_fields: [...(s.iface_fields || [])],
|
||||
|
|
@ -368,6 +386,8 @@ const TEMPLATE_EXPORT_VERSION = 1;
|
|||
|
||||
function templateExportPayload(tpl: Template) {
|
||||
const metrics = templateSheets(tpl).map((s) => ({
|
||||
sheet_id: s.sheet_id || s.metric_id,
|
||||
title: s.title || s.sheet_id || s.metric_id,
|
||||
metric_id: s.metric_id,
|
||||
key_fields: [...(s.key_fields || [])],
|
||||
iface_fields: [...(s.iface_fields || [])],
|
||||
|
|
@ -424,6 +444,8 @@ function parseTemplateImport(raw: unknown): {
|
|||
: [];
|
||||
if (!mid || !keys.length) continue;
|
||||
metrics.push({
|
||||
sheet_id: String(m.sheet_id || mid).trim() || mid,
|
||||
title: String(m.title || m.sheet_id || mid).trim() || mid,
|
||||
metric_id: mid,
|
||||
key_fields: keys,
|
||||
iface_fields: Array.isArray(m.iface_fields)
|
||||
|
|
@ -530,6 +552,7 @@ const FILTER_OPS = [
|
|||
"ne",
|
||||
"in",
|
||||
"not_in",
|
||||
"contains",
|
||||
"empty",
|
||||
"not_empty",
|
||||
"regex",
|
||||
|
|
@ -834,13 +857,13 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
setResultSheetId("");
|
||||
return;
|
||||
}
|
||||
if (!resultSheetId || !runSheets.some((s) => s.metric_id === resultSheetId)) {
|
||||
setResultSheetId(runSheets[0].metric_id);
|
||||
if (!resultSheetId || !runSheets.some((s) => sheetIdentity(s) === resultSheetId)) {
|
||||
setResultSheetId(sheetIdentity(runSheets[0]));
|
||||
}
|
||||
}, [runSheets, resultSheetId]);
|
||||
|
||||
const activeRunSheet = useMemo(
|
||||
() => runSheets.find((s) => s.metric_id === resultSheetId) || runSheets[0] || null,
|
||||
() => runSheets.find((s) => sheetIdentity(s) === resultSheetId) || runSheets[0] || null,
|
||||
[runSheets, resultSheetId],
|
||||
);
|
||||
|
||||
|
|
@ -851,7 +874,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
|
||||
useEffect(() => {
|
||||
const runId = String(runDetail?.id || "");
|
||||
const mid = resultSheetId || activeRunSheet?.metric_id || "";
|
||||
const mid = resultSheetId || sheetIdentity(activeRunSheet) || "";
|
||||
if (!runId || !mid || jobDetailTab !== "result") {
|
||||
setPagedDiffs([]);
|
||||
setResultTotal(0);
|
||||
|
|
@ -1002,6 +1025,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
const resultPages = Math.max(1, Math.ceil(resultTotal / Math.max(1, resultPageSize)));
|
||||
const sheetCards = useMemo(() => {
|
||||
const raw = (summary.sheet_cards || []) as Array<{
|
||||
sheet_id?: string;
|
||||
title?: string;
|
||||
metric_id: string;
|
||||
mode?: string;
|
||||
added?: number;
|
||||
|
|
@ -1023,7 +1048,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
return String(a.metric_id).localeCompare(String(b.metric_id));
|
||||
});
|
||||
}, [summary.sheet_cards]);
|
||||
const activeSheetCard = sheetCards.find((c) => c.metric_id === resultSheetId) || sheetCards[0];
|
||||
const activeSheetCard = sheetCards.find((c) => sheetIdentity(c) === resultSheetId) || sheetCards[0];
|
||||
const activeFail = sheetFailOf(activeSheetCard || {});
|
||||
const activeSuccess = sheetSuccessOf(activeSheetCard || {});
|
||||
const activePassRate = sheetPassRateOf(activeSheetCard || {});
|
||||
|
|
@ -1125,7 +1150,6 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
if (on) {
|
||||
const schema = metrics.find((m) => m.metric_id === metricId);
|
||||
setTplSheets((prev) => {
|
||||
if (prev.some((s) => s.metric_id === metricId)) return prev;
|
||||
const next = [...prev, defaultSheetForMetric(schema, metricId)];
|
||||
setTplSheetIdx(next.length - 1);
|
||||
if (!prev.length) setShowMetricPicker(false);
|
||||
|
|
@ -1133,13 +1157,11 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
});
|
||||
return;
|
||||
}
|
||||
// Uncheck removes ALL sheets of this source metric
|
||||
setTplSheets((prev) => {
|
||||
const idx = prev.findIndex((s) => s.metric_id === metricId);
|
||||
if (idx < 0) return prev;
|
||||
const next = prev.filter((_, i) => i !== idx);
|
||||
const next = prev.filter((s) => s.metric_id !== metricId);
|
||||
setTplSheetIdx((cur) => {
|
||||
if (!next.length) return 0;
|
||||
if (cur > idx) return cur - 1;
|
||||
if (cur >= next.length) return next.length - 1;
|
||||
return cur;
|
||||
});
|
||||
|
|
@ -1147,6 +1169,28 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
});
|
||||
};
|
||||
|
||||
const duplicateTplSheet = (idx: number) => {
|
||||
setTplSheets((prev) => {
|
||||
const src = prev[idx];
|
||||
if (!src) return prev;
|
||||
const baseId = sheetIdentity(src);
|
||||
let n = 2;
|
||||
let candidate = `${baseId}.${n}`;
|
||||
const used = new Set(prev.map((s) => sheetIdentity(s)));
|
||||
while (used.has(candidate)) {
|
||||
n += 1;
|
||||
candidate = `${baseId}.${n}`;
|
||||
}
|
||||
const copy = cloneSheet(src);
|
||||
copy.sheet_id = candidate;
|
||||
copy.title = `${sheetLabel(src)} (${n})`;
|
||||
const next = [...prev];
|
||||
next.splice(idx + 1, 0, copy);
|
||||
setTplSheetIdx(idx + 1);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const removeTplMetric = (idx: number) => {
|
||||
setTplSheets((prev) => {
|
||||
const next = prev.filter((_, i) => i !== idx);
|
||||
|
|
@ -1167,16 +1211,27 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
}
|
||||
for (const s of tplSheets) {
|
||||
if (!s.key_fields.length) {
|
||||
showError(`${metricLabel(s.metric_id)}: ${t("bizCompare.keyRequired")}`);
|
||||
showError(`${sheetLabel(s)}: ${t("bizCompare.keyRequired")}`);
|
||||
return;
|
||||
}
|
||||
if (!sheetIdentity(s)) {
|
||||
showError(`${s.metric_id}: ${t("bizCompare.sheetIdRequired")}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const ids = tplSheets.map((s) => sheetIdentity(s));
|
||||
if (new Set(ids).size !== ids.length) {
|
||||
showError(t("bizCompare.duplicateSheetId"));
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
const body = {
|
||||
name: tplName || tplSheets.map((s) => s.metric_id).join("+"),
|
||||
name: tplName || tplSheets.map((s) => sheetLabel(s)).join("+"),
|
||||
note: tplNote,
|
||||
metrics: tplSheets.map((s) => ({
|
||||
sheet_id: sheetIdentity(s),
|
||||
title: s.title || sheetIdentity(s),
|
||||
metric_id: s.metric_id,
|
||||
key_fields: s.key_fields,
|
||||
iface_fields: s.iface_fields,
|
||||
|
|
@ -1239,6 +1294,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
name: body.name,
|
||||
note: body.note,
|
||||
metrics: body.metrics.map((s) => ({
|
||||
sheet_id: sheetIdentity(s),
|
||||
title: s.title || sheetIdentity(s),
|
||||
metric_id: s.metric_id,
|
||||
key_fields: s.key_fields,
|
||||
iface_fields: s.iface_fields,
|
||||
|
|
@ -1701,9 +1758,9 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
</td>
|
||||
<td>
|
||||
<div className="bs-cmp-metric-chips">
|
||||
{sheets.map((s) => (
|
||||
<code key={s.metric_id} className="bs-cmp-metric-chip">
|
||||
{s.metric_id}
|
||||
{sheets.map((s, i) => (
|
||||
<code key={`${sheetIdentity(s)}-${i}`} className="bs-cmp-metric-chip">
|
||||
{sheetLabel(s)}
|
||||
{!s.compare_fields?.length ? (
|
||||
<span className="muted"> · {t("bizCompare.presenceShort")}</span>
|
||||
) : null}
|
||||
|
|
@ -1885,7 +1942,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
const filterN = toOrGroups(s.row_filters || []).length;
|
||||
return (
|
||||
<button
|
||||
key={s.metric_id}
|
||||
key={`${sheetIdentity(s)}-${i}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tplSheetIdx === i}
|
||||
|
|
@ -1895,7 +1952,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
setTplPaneTab("fields");
|
||||
}}
|
||||
>
|
||||
<span className="ct-editor__nav-name">{s.metric_id}</span>
|
||||
<span className="ct-editor__nav-name">{sheetLabel(s)}</span>
|
||||
<span className="ct-editor__nav-tag">
|
||||
{!s.compare_fields.length
|
||||
? t("bizCompare.presenceShort")
|
||||
|
|
@ -1911,9 +1968,26 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
{activeTplSheet ? (
|
||||
<div className="ct-editor__pane">
|
||||
<div className="ct-editor__pane-head">
|
||||
<div>
|
||||
<div className="ct-editor__metric">{activeTplSheet.metric_id}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="ct-editor__metric-edit" style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<Input
|
||||
value={activeTplSheet.title || ""}
|
||||
placeholder={t("bizCompare.sheetTitle")}
|
||||
aria-label={t("bizCompare.sheetTitle")}
|
||||
onChange={(e) => updateActiveSheet({ title: e.target.value })}
|
||||
style={{ maxWidth: 220 }}
|
||||
/>
|
||||
<Input
|
||||
value={activeTplSheet.sheet_id || activeTplSheet.metric_id}
|
||||
placeholder={t("bizCompare.sheetId")}
|
||||
aria-label={t("bizCompare.sheetId")}
|
||||
onChange={(e) => updateActiveSheet({ sheet_id: e.target.value.trim() })}
|
||||
style={{ maxWidth: 220 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="ct-editor__meta muted">
|
||||
{activeTplSheet.metric_id}
|
||||
{" · "}
|
||||
{activeTplSheet.compare_fields.length
|
||||
? t("bizCompare.modeFieldsShort")
|
||||
: t("bizCompare.presenceShort")}
|
||||
|
|
@ -1921,9 +1995,14 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
Key {(activeTplSheet.key_fields || []).join(" · ") || "—"}
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onPress={() => removeTplMetric(tplSheetIdx)}>
|
||||
{t("bizCompare.removeSheet")}
|
||||
</Button>
|
||||
<div className="btn-row" style={{ gap: 6 }}>
|
||||
<Button size="sm" variant="secondary" onPress={() => duplicateTplSheet(tplSheetIdx)}>
|
||||
{t("bizCompare.splitSheet")}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onPress={() => removeTplMetric(tplSheetIdx)}>
|
||||
{t("bizCompare.removeSheet")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-rule-tabs" role="tablist">
|
||||
|
|
@ -2354,6 +2433,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
const fail = removed + changed;
|
||||
const judged = fail + unchanged;
|
||||
return {
|
||||
sheet_id: sheetIdentity(s),
|
||||
title: sheetLabel(s),
|
||||
metric_id: s.metric_id,
|
||||
mode: s.mode,
|
||||
added: s.summary?.added,
|
||||
|
|
@ -2374,21 +2455,23 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
const fail = sheetFailOf(c);
|
||||
const ok = sheetSuccessOf(c);
|
||||
const rate = sheetPassRateOf(c);
|
||||
const active = resultSheetId === c.metric_id;
|
||||
const id = sheetIdentity(c);
|
||||
const label = sheetLabel(c);
|
||||
const active = resultSheetId === id;
|
||||
return (
|
||||
<button
|
||||
key={c.metric_id}
|
||||
key={id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
className={`bs-cmp-nav__item${active ? " is-active" : ""}${
|
||||
fail > 0 ? " has-diff" : " is-clean"
|
||||
}`}
|
||||
onClick={() => setResultSheetId(c.metric_id)}
|
||||
title={`${c.metric_id} · ${t("bizCompare.kindFail")} ${fail} · ${t("bizCompare.kindSuccess")} ${ok} · ${t("bizCompare.passRateShort")} ${rate}%`}
|
||||
onClick={() => setResultSheetId(id)}
|
||||
title={`${label} · ${t("bizCompare.kindFail")} ${fail} · ${t("bizCompare.kindSuccess")} ${ok} · ${t("bizCompare.passRateShort")} ${rate}%`}
|
||||
>
|
||||
<span className="bs-cmp-nav__dot" aria-hidden />
|
||||
<span className="bs-cmp-nav__name">{c.metric_id}</span>
|
||||
<span className="bs-cmp-nav__name">{label}</span>
|
||||
<span className="bs-cmp-nav__stats">
|
||||
<span
|
||||
className={`bs-cmp-nav__num bs-cmp-nav__num--fail${
|
||||
|
|
@ -2420,9 +2503,9 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
</span>
|
||||
<strong
|
||||
className="bs-cmp-strip__sheet-name"
|
||||
title={activeSheetCard?.metric_id || ""}
|
||||
title={sheetLabel(activeSheetCard)}
|
||||
>
|
||||
{activeSheetCard?.metric_id || "—"}
|
||||
{sheetLabel(activeSheetCard)}
|
||||
</strong>
|
||||
<span className="muted bs-cmp-strip__sheet-mode">
|
||||
{activeFail > 0 ? t("bizCompare.kindFail") : t("bizCompare.kindPass")}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,10 @@ type MigBatch = {
|
|||
id: string;
|
||||
batch_label: string;
|
||||
status: string;
|
||||
expect_set?: { ports?: string[]; items?: Array<{ metric_id: string; key?: string; keys?: string[] }> };
|
||||
expect_set?: {
|
||||
ports?: string[];
|
||||
items?: Array<{ metric_id: string; sheet_id?: string; key?: string; keys?: string[] }>;
|
||||
};
|
||||
accept_status?: string;
|
||||
accept_run_id?: string;
|
||||
accept_summary?: {
|
||||
|
|
@ -91,6 +94,7 @@ type RedTicket = {
|
|||
};
|
||||
type SheetCard = {
|
||||
metric_id: string;
|
||||
sheet_id?: string;
|
||||
title?: string;
|
||||
progress_ok: number;
|
||||
progress_total: number;
|
||||
|
|
@ -101,6 +105,7 @@ type SheetCard = {
|
|||
type DiffRow = {
|
||||
id: string;
|
||||
metric_id: string;
|
||||
sheet_id?: string;
|
||||
verdict: string;
|
||||
color: string;
|
||||
old_kind: string;
|
||||
|
|
@ -129,11 +134,21 @@ type ExpectSheetItem = {
|
|||
};
|
||||
type ExpectSheet = {
|
||||
metric_id: string;
|
||||
sheet_id?: string;
|
||||
title?: string;
|
||||
key_fields: string[];
|
||||
iface_fields: string[];
|
||||
items: ExpectSheetItem[];
|
||||
};
|
||||
|
||||
function sheetIdentity(s: { sheet_id?: string; metric_id?: string } | null | undefined): string {
|
||||
return String(s?.sheet_id || s?.metric_id || "").trim();
|
||||
}
|
||||
|
||||
function sheetLabel(s: { title?: string; sheet_id?: string; metric_id?: string } | null | undefined): string {
|
||||
return String(s?.title || s?.sheet_id || s?.metric_id || "").trim() || "—";
|
||||
}
|
||||
|
||||
type DetailTab = "setup" | "batches" | "board";
|
||||
|
||||
const COLOR: Record<string, string> = {
|
||||
|
|
@ -237,7 +252,11 @@ export function BizMigrationPage() {
|
|||
const sheetCards = board?.run?.summary?.sheet_cards || [];
|
||||
const visibleDiffs = useMemo(() => {
|
||||
let rows = diffs;
|
||||
if (boardMetricId) rows = rows.filter((d) => d.metric_id === boardMetricId);
|
||||
if (boardMetricId) {
|
||||
rows = rows.filter(
|
||||
(d) => sheetIdentity(d) === boardMetricId || d.metric_id === boardMetricId,
|
||||
);
|
||||
}
|
||||
if (onlyExpect) rows = rows.filter((d) => d.in_expect);
|
||||
return rows;
|
||||
}, [diffs, onlyExpect, boardMetricId]);
|
||||
|
|
@ -245,10 +264,12 @@ export function BizMigrationPage() {
|
|||
const boardMetricOptions = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const c of sheetCards) {
|
||||
if (c.metric_id) ids.add(c.metric_id);
|
||||
const id = sheetIdentity(c);
|
||||
if (id) ids.add(id);
|
||||
}
|
||||
for (const d of diffs) {
|
||||
if (d.metric_id) ids.add(d.metric_id);
|
||||
const id = sheetIdentity(d);
|
||||
if (id) ids.add(id);
|
||||
}
|
||||
return [...ids];
|
||||
}, [sheetCards, diffs]);
|
||||
|
|
@ -288,7 +309,8 @@ export function BizMigrationPage() {
|
|||
}, [batch]);
|
||||
|
||||
const filteredExpectItems = useMemo(() => {
|
||||
const sheet = expectSheets.find((s) => s.metric_id === expectMetricId) || expectSheets[0];
|
||||
const sheet =
|
||||
expectSheets.find((s) => sheetIdentity(s) === expectMetricId) || expectSheets[0];
|
||||
const items = sheet?.items || [];
|
||||
const kw = portFilter.trim().toLowerCase();
|
||||
if (!kw) return items;
|
||||
|
|
@ -378,8 +400,8 @@ export function BizMigrationPage() {
|
|||
const sheets = (res.sheets || []) as ExpectSheet[];
|
||||
setExpectSheets(sheets);
|
||||
setExpectMetricId((prev) => {
|
||||
if (prev && sheets.some((s) => s.metric_id === prev)) return prev;
|
||||
return sheets[0]?.metric_id || "";
|
||||
if (prev && sheets.some((s) => sheetIdentity(s) === prev)) return prev;
|
||||
return sheets[0] ? sheetIdentity(sheets[0]) : "";
|
||||
});
|
||||
} catch {
|
||||
setExpectSheets([]);
|
||||
|
|
@ -712,11 +734,14 @@ export function BizMigrationPage() {
|
|||
async function onCreateBatch() {
|
||||
if (!projectId) return;
|
||||
// One item per selected key. A flat `keys: [a, b]` is parsed as one composite key.
|
||||
const items: Array<{ metric_id: string; key: string }> = [];
|
||||
const items: Array<{ metric_id: string; sheet_id?: string; key: string }> = [];
|
||||
const ports: string[] = [];
|
||||
for (const sheet of expectSheets) {
|
||||
const keys = sheet.items.map((it) => it.key).filter((k) => selectedExpectKeys.has(k));
|
||||
for (const key of keys) items.push({ metric_id: sheet.metric_id, key });
|
||||
const sid = sheetIdentity(sheet);
|
||||
for (const key of keys) {
|
||||
items.push({ metric_id: sheet.metric_id, sheet_id: sid, key });
|
||||
}
|
||||
if (sheet.metric_id === "interface_brief") ports.push(...keys);
|
||||
}
|
||||
if (!items.length && !ports.length) {
|
||||
|
|
@ -1416,20 +1441,30 @@ export function BizMigrationPage() {
|
|||
<div className="ct-editor__nav-list" role="tablist">
|
||||
{(expectSheets.length
|
||||
? expectSheets
|
||||
: [{ metric_id: "—", items: [] as ExpectSheetItem[], key_fields: [] as string[], iface_fields: [] as string[] }]
|
||||
: [
|
||||
{
|
||||
metric_id: "—",
|
||||
sheet_id: "—",
|
||||
title: "—",
|
||||
items: [] as ExpectSheetItem[],
|
||||
key_fields: [] as string[],
|
||||
iface_fields: [] as string[],
|
||||
},
|
||||
]
|
||||
).map((s) => {
|
||||
const sid = sheetIdentity(s);
|
||||
const active =
|
||||
(expectMetricId || expectSheets[0]?.metric_id) === s.metric_id;
|
||||
(expectMetricId || sheetIdentity(expectSheets[0])) === sid;
|
||||
return (
|
||||
<button
|
||||
key={s.metric_id}
|
||||
key={sid}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
className={`ct-editor__nav-item${active ? " is-active" : ""}`}
|
||||
onClick={() => setExpectMetricId(s.metric_id)}
|
||||
onClick={() => setExpectMetricId(sid)}
|
||||
>
|
||||
<span className="ct-editor__nav-name">{s.metric_id}</span>
|
||||
<span className="ct-editor__nav-name">{sheetLabel(s)}</span>
|
||||
<span className="ct-editor__nav-tag">{s.items.length}</span>
|
||||
</button>
|
||||
);
|
||||
|
|
@ -1605,7 +1640,7 @@ export function BizMigrationPage() {
|
|||
{t("bizMigration.newBaselineMissingBanner", {
|
||||
metrics: sheetCards
|
||||
.filter((c) => c.new_baseline_missing)
|
||||
.map((c) => c.metric_id)
|
||||
.map((c) => sheetLabel(c))
|
||||
.join(", "),
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -1703,22 +1738,23 @@ export function BizMigrationPage() {
|
|||
<span className="bs-cmp-nav__name">{t("bizMigration.allMetrics")}</span>
|
||||
</button>
|
||||
{sheetCards.map((c) => {
|
||||
const active = boardMetricId === c.metric_id;
|
||||
const sid = sheetIdentity(c);
|
||||
const active = boardMetricId === sid;
|
||||
const hot = (c.anomaly || 0) > 0 || Boolean(c.new_baseline_missing);
|
||||
return (
|
||||
<button
|
||||
key={c.metric_id}
|
||||
key={sid}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
className={`bs-cmp-nav__item${active ? " is-active" : ""}${
|
||||
hot ? " has-diff" : " is-clean"
|
||||
}`}
|
||||
onClick={() => setBoardMetricId(c.metric_id)}
|
||||
onClick={() => setBoardMetricId(sid)}
|
||||
>
|
||||
<span className="bs-cmp-nav__dot" />
|
||||
<span className="bs-cmp-nav__name">
|
||||
{c.metric_id}
|
||||
{sheetLabel(c)}
|
||||
{c.new_baseline_missing ? " · baseline" : ""}
|
||||
</span>
|
||||
<span className="bs-cmp-nav__stats">
|
||||
|
|
@ -1754,7 +1790,7 @@ export function BizMigrationPage() {
|
|||
<tbody>
|
||||
{visibleDiffs.map((d) => (
|
||||
<tr key={d.id}>
|
||||
<td>{d.metric_id || "—"}</td>
|
||||
<td>{sheetLabel(d)}</td>
|
||||
<td>
|
||||
<code>{d.key_str || "—"}</code>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import {
|
|||
type MetricField = { name: string; display_name?: string };
|
||||
type MetricSchema = { metric_id: string; fields: MetricField[] };
|
||||
type CompareSheet = {
|
||||
sheet_id?: string;
|
||||
title?: string;
|
||||
metric_id: string;
|
||||
key_fields?: string[];
|
||||
iface_fields?: string[];
|
||||
|
|
@ -48,6 +50,7 @@ type DualPat = {
|
|||
};
|
||||
type SheetOverride = {
|
||||
metric_id: string;
|
||||
sheet_id?: string;
|
||||
status_fields?: string[];
|
||||
down_values?: string[];
|
||||
up_values?: string[];
|
||||
|
|
@ -237,9 +240,10 @@ function defaultAnomalyPresenceOnly(): DualPat[] {
|
|||
];
|
||||
}
|
||||
|
||||
function emptyOverride(metricId: string): SheetOverride {
|
||||
function emptyOverride(metricId: string, sheetId = ""): SheetOverride {
|
||||
return {
|
||||
metric_id: metricId,
|
||||
sheet_id: sheetId || metricId,
|
||||
status_fields: [],
|
||||
down_values: [],
|
||||
up_values: [],
|
||||
|
|
@ -249,14 +253,28 @@ function emptyOverride(metricId: string): SheetOverride {
|
|||
};
|
||||
}
|
||||
|
||||
export function presetForMetric(metricId: string): SheetOverride {
|
||||
function sheetIdentity(s: { sheet_id?: string; metric_id?: string } | null | undefined): string {
|
||||
return String(s?.sheet_id || s?.metric_id || "").trim();
|
||||
}
|
||||
|
||||
function sheetLabel(s: { title?: string; sheet_id?: string; metric_id?: string } | null | undefined): string {
|
||||
return String(s?.title || s?.sheet_id || s?.metric_id || "").trim() || "—";
|
||||
}
|
||||
|
||||
export function presetForMetric(metricId: string, sheetId = ""): SheetOverride {
|
||||
const mid = metricId;
|
||||
const sid = sheetId || mid;
|
||||
const withId = (ov: SheetOverride): SheetOverride => ({
|
||||
...ov,
|
||||
metric_id: mid,
|
||||
sheet_id: sid,
|
||||
});
|
||||
if (mid === "interface_brief") {
|
||||
const upConds: RuleCond[] = [
|
||||
{ type: "value", field: "admin", op: "in", value: ["up"] },
|
||||
{ type: "value", field: "phy", op: "in", value: ["up"] },
|
||||
];
|
||||
return {
|
||||
return withId({
|
||||
metric_id: mid,
|
||||
status_fields: ["admin", "phy", "prot"],
|
||||
down_values: ["down"],
|
||||
|
|
@ -279,11 +297,11 @@ export function presetForMetric(metricId: string): SheetOverride {
|
|||
],
|
||||
anomaly: defaultAnomalyForState("admin", ["down"]),
|
||||
skip_dual: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (mid === "bgp_peer") {
|
||||
if (mid === "bgp_peer" || mid.startsWith("bgp_peer")) {
|
||||
const upConds: RuleCond[] = [{ type: "value", field: "state", op: "eq", value: "established" }];
|
||||
return {
|
||||
return withId({
|
||||
metric_id: mid,
|
||||
status_fields: ["state"],
|
||||
down_values: ["idle", "active", "connect", "down"],
|
||||
|
|
@ -310,10 +328,10 @@ export function presetForMetric(metricId: string): SheetOverride {
|
|||
],
|
||||
anomaly: defaultAnomalyForState("state", ["idle", "active", "connect", "down"]),
|
||||
skip_dual: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (mid === "arp" || mid === "nd6_cache" || mid === "lldp_neighbor") {
|
||||
return {
|
||||
return withId({
|
||||
metric_id: mid,
|
||||
status_fields: [],
|
||||
down_values: [],
|
||||
|
|
@ -321,13 +339,13 @@ export function presetForMetric(metricId: string): SheetOverride {
|
|||
success: [successFromPresence(["removed"], ["added"])],
|
||||
anomaly: defaultAnomalyPresenceOnly(),
|
||||
skip_dual: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (mid.includes("isis") || mid.includes("ospf") || mid.includes("adjacency")) {
|
||||
const upConds: RuleCond[] = [
|
||||
{ type: "value", field: "state", op: "in", value: ["up", "full", "2way"] },
|
||||
];
|
||||
return {
|
||||
return withId({
|
||||
metric_id: mid,
|
||||
status_fields: ["state"],
|
||||
down_values: ["down", "init", "idle"],
|
||||
|
|
@ -354,10 +372,10 @@ export function presetForMetric(metricId: string): SheetOverride {
|
|||
],
|
||||
anomaly: defaultAnomalyForState("state", ["down", "init", "idle"]),
|
||||
skip_dual: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (mid.includes("route") || mid.includes("vrf")) {
|
||||
return {
|
||||
return withId({
|
||||
metric_id: mid,
|
||||
status_fields: [],
|
||||
down_values: [],
|
||||
|
|
@ -365,18 +383,23 @@ export function presetForMetric(metricId: string): SheetOverride {
|
|||
success: [successFromPresence(["removed"], ["added"])],
|
||||
anomaly: defaultAnomalyPresenceOnly(),
|
||||
skip_dual: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
return emptyOverride(mid);
|
||||
return emptyOverride(mid, sid);
|
||||
}
|
||||
|
||||
function skipPreset(metricId: string): SheetOverride {
|
||||
return { ...emptyOverride(metricId), skip_dual: true, success: [], anomaly: [] };
|
||||
function skipPreset(metricId: string, sheetId = ""): SheetOverride {
|
||||
return { ...emptyOverride(metricId, sheetId), skip_dual: true, success: [], anomaly: [] };
|
||||
}
|
||||
|
||||
function overrideFor(overrides: SheetOverride[], metricId: string): SheetOverride {
|
||||
const found = overrides.find((o) => o.metric_id === metricId);
|
||||
const merged = found ? { ...emptyOverride(metricId), ...found, metric_id: metricId } : emptyOverride(metricId);
|
||||
function overrideFor(overrides: SheetOverride[], metricId: string, sheetId = ""): SheetOverride {
|
||||
const sid = sheetId || metricId;
|
||||
const found =
|
||||
overrides.find((o) => (o.sheet_id || o.metric_id) === sid) ||
|
||||
overrides.find((o) => !o.sheet_id && o.metric_id === metricId);
|
||||
const merged = found
|
||||
? { ...emptyOverride(metricId, sid), ...found, metric_id: metricId, sheet_id: sid }
|
||||
: emptyOverride(metricId, sid);
|
||||
return {
|
||||
...merged,
|
||||
success: (merged.success || []).map((p) => normalizePat(p)),
|
||||
|
|
@ -788,16 +811,20 @@ export function BizMonitorTemplatesPage() {
|
|||
const sheets = useMemo(() => selectedCompare?.metrics || [], [selectedCompare]);
|
||||
const activeSheet = sheets[activeSheetIdx] || sheets[0] || null;
|
||||
const activeMetricId = activeSheet?.metric_id || "";
|
||||
const activeSheetId = activeSheet ? sheetIdentity(activeSheet) : "";
|
||||
const activeOverride = useMemo(
|
||||
() => (activeMetricId ? overrideFor(overrides, activeMetricId) : emptyOverride("")),
|
||||
[overrides, activeMetricId],
|
||||
() =>
|
||||
activeMetricId
|
||||
? overrideFor(overrides, activeMetricId, activeSheetId)
|
||||
: emptyOverride(""),
|
||||
[overrides, activeMetricId, activeSheetId],
|
||||
);
|
||||
const activeFields = useMemo(() => {
|
||||
const schema = metricSchemas.find((m) => m.metric_id === activeMetricId);
|
||||
return schema?.fields || [];
|
||||
}, [metricSchemas, activeMetricId]);
|
||||
|
||||
const sheetKey = useMemo(() => sheets.map((s) => s.metric_id).join("|"), [sheets]);
|
||||
const sheetKey = useMemo(() => sheets.map((s) => sheetIdentity(s)).join("|"), [sheets]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sheets.length) {
|
||||
|
|
@ -810,16 +837,21 @@ export function BizMonitorTemplatesPage() {
|
|||
useEffect(() => {
|
||||
if (!sheetKey) return;
|
||||
setOverrides((prev) => {
|
||||
const byId = new Map(prev.map((o) => [o.metric_id, o]));
|
||||
return sheets.map((s) => byId.get(s.metric_id) || emptyOverride(s.metric_id));
|
||||
const byId = new Map(prev.map((o) => [o.sheet_id || o.metric_id, o]));
|
||||
return sheets.map(
|
||||
(s) =>
|
||||
byId.get(sheetIdentity(s)) ||
|
||||
emptyOverride(s.metric_id, sheetIdentity(s)),
|
||||
);
|
||||
});
|
||||
}, [compareId, sheetKey]);
|
||||
|
||||
const syncOverride = (metricId: string, patch: Partial<SheetOverride>) => {
|
||||
const syncOverride = (metricId: string, patch: Partial<SheetOverride>, sheetId = "") => {
|
||||
const sid = sheetId || sheetIdentity({ metric_id: metricId, sheet_id: sheetId });
|
||||
setOverrides((prev) => {
|
||||
const others = prev.filter((o) => o.metric_id !== metricId);
|
||||
const base = overrideFor(prev, metricId);
|
||||
return [...others, { ...base, ...patch, metric_id: metricId }];
|
||||
const others = prev.filter((o) => (o.sheet_id || o.metric_id) !== sid);
|
||||
const base = overrideFor(prev, metricId, sid);
|
||||
return [...others, { ...base, ...patch, metric_id: metricId, sheet_id: sid }];
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -858,15 +890,18 @@ export function BizMonitorTemplatesPage() {
|
|||
const closeEdit = () => setEditOpen(false);
|
||||
|
||||
const applyPreset = (kind: "auto" | "skip") => {
|
||||
if (!activeMetricId) return;
|
||||
if (!activeMetricId || !activeSheetId) return;
|
||||
syncOverride(
|
||||
activeMetricId,
|
||||
kind === "skip" ? skipPreset(activeMetricId) : presetForMetric(activeMetricId),
|
||||
kind === "skip"
|
||||
? skipPreset(activeMetricId, activeSheetId)
|
||||
: presetForMetric(activeMetricId, activeSheetId),
|
||||
activeSheetId,
|
||||
);
|
||||
};
|
||||
|
||||
const applyAllPresets = () => {
|
||||
setOverrides(sheets.map((s) => presetForMetric(s.metric_id)));
|
||||
setOverrides(sheets.map((s) => presetForMetric(s.metric_id, sheetIdentity(s))));
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
|
|
@ -1080,10 +1115,11 @@ export function BizMonitorTemplatesPage() {
|
|||
</div>
|
||||
<div className="mt-editor__nav-list" role="tablist">
|
||||
{sheets.map((s, i) => {
|
||||
const ov = overrideFor(overrides, s.metric_id);
|
||||
const sid = sheetIdentity(s);
|
||||
const ov = overrideFor(overrides, s.metric_id, sid);
|
||||
return (
|
||||
<button
|
||||
key={s.metric_id}
|
||||
key={sid}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeSheetIdx === i}
|
||||
|
|
@ -1093,7 +1129,7 @@ export function BizMonitorTemplatesPage() {
|
|||
setRuleTab("success");
|
||||
}}
|
||||
>
|
||||
<span className="mt-editor__nav-name">{s.metric_id}</span>
|
||||
<span className="mt-editor__nav-name">{sheetLabel(s)}</span>
|
||||
<span className="mt-editor__nav-tag">
|
||||
{ov.skip_dual
|
||||
? t("bizMonitorTpl.tagSkip")
|
||||
|
|
@ -1116,9 +1152,9 @@ export function BizMonitorTemplatesPage() {
|
|||
<div className="mt-editor__pane">
|
||||
<div className="mt-editor__pane-head">
|
||||
<div>
|
||||
<div className="mt-editor__metric">{activeMetricId}</div>
|
||||
<div className="mt-editor__metric">{sheetLabel(activeSheet)}</div>
|
||||
<div className="mt-editor__meta muted">
|
||||
<span>Key {(activeSheet.key_fields || []).join(" · ") || "—"}</span>
|
||||
<span>{activeMetricId}</span>
|
||||
<span>
|
||||
{t("bizMonitorTpl.compare")}{" "}
|
||||
{(activeSheet.compare_fields || []).length
|
||||
|
|
@ -1142,7 +1178,9 @@ export function BizMonitorTemplatesPage() {
|
|||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(activeOverride.skip_dual)}
|
||||
onChange={(e) => syncOverride(activeMetricId, { skip_dual: e.target.checked })}
|
||||
onChange={(e) =>
|
||||
syncOverride(activeMetricId, { skip_dual: e.target.checked }, activeSheetId)
|
||||
}
|
||||
/>
|
||||
<span>{t("bizMonitorTpl.skipDual")}</span>
|
||||
</label>
|
||||
|
|
@ -1177,7 +1215,9 @@ export function BizMonitorTemplatesPage() {
|
|||
patterns={activeOverride.success || []}
|
||||
fieldChoices={fieldChoices}
|
||||
t={t}
|
||||
onChange={(success) => syncOverride(activeMetricId, { success })}
|
||||
onChange={(success) =>
|
||||
syncOverride(activeMetricId, { success }, activeSheetId)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<DualPatternList
|
||||
|
|
@ -1185,7 +1225,9 @@ export function BizMonitorTemplatesPage() {
|
|||
patterns={activeOverride.anomaly || []}
|
||||
fieldChoices={fieldChoices}
|
||||
t={t}
|
||||
onChange={(anomaly) => syncOverride(activeMetricId, { anomaly })}
|
||||
onChange={(anomaly) =>
|
||||
syncOverride(activeMetricId, { anomaly }, activeSheetId)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -1217,26 +1259,29 @@ export function BizMonitorTemplatesPage() {
|
|||
{t("bizMonitorTpl.collectHint")}
|
||||
</p>
|
||||
<div className="mt-chip-row">
|
||||
{sheets.map((s) => {
|
||||
const checked = collectIds.length === 0 || collectIds.includes(s.metric_id);
|
||||
{Array.from(new Set(sheets.map((s) => s.metric_id).filter(Boolean))).map((mid) => {
|
||||
const checked = collectIds.length === 0 || collectIds.includes(mid);
|
||||
const explicit = collectIds.length > 0;
|
||||
const allMids = Array.from(
|
||||
new Set(sheets.map((s) => s.metric_id).filter(Boolean)),
|
||||
);
|
||||
return (
|
||||
<Chip
|
||||
key={s.metric_id}
|
||||
key={mid}
|
||||
active={checked}
|
||||
label={s.metric_id}
|
||||
label={mid}
|
||||
onClick={() => {
|
||||
if (!explicit) {
|
||||
setCollectIds(sheets.map((x) => x.metric_id).filter((id) => id !== s.metric_id));
|
||||
setCollectIds(allMids.filter((id) => id !== mid));
|
||||
return;
|
||||
}
|
||||
setCollectIds((prev) => {
|
||||
if (prev.includes(s.metric_id)) {
|
||||
const next = prev.filter((x) => x !== s.metric_id);
|
||||
return next.length === sheets.length ? [] : next;
|
||||
if (prev.includes(mid)) {
|
||||
const next = prev.filter((x) => x !== mid);
|
||||
return next.length === allMids.length ? [] : next;
|
||||
}
|
||||
const next = [...prev, s.metric_id];
|
||||
return next.length === sheets.length ? [] : next;
|
||||
const next = [...prev, mid];
|
||||
return next.length === allMids.length ? [] : next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue