Show all collected metric sheets in biz-state batch workbook.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-17 21:23:19 +08:00
parent 2f0c9eb1da
commit bb0d8e4089
4 changed files with 126 additions and 48 deletions

View file

@ -221,6 +221,11 @@ const en = {
sheetCommands: "Commands",
sheetLldp: "LLDP neighbors",
sheetVrfRoute: "VRF route summary",
sheetIsis: "ISIS adjacency",
sheetIfaceBrief: "Interface brief",
sheetArp: "ARP",
sheetNd6: "ND6 cache",
sheetBgpPeer: "BGP peers",
sheetFilterPh: "Filter this sheet…",
sheetEmpty: "No rows in this sheet",
filterColumn: "Column",

View file

@ -221,6 +221,11 @@ const zh = {
sheetCommands: "命令",
sheetLldp: "LLDP 邻居",
sheetVrfRoute: "VRF 路由摘要",
sheetIsis: "ISIS 邻居",
sheetIfaceBrief: "接口 Brief",
sheetArp: "ARP",
sheetNd6: "ND6 Cache",
sheetBgpPeer: "BGP 对等体",
sheetFilterPh: "筛选当前表…",
sheetEmpty: "本表无数据",
filterColumn: "筛选列",

View file

@ -9975,6 +9975,29 @@ html.login-page--paused .login-page__flare {
border-top: 1px solid rgba(148, 163, 184, 0.2);
}
.bs-sheet-tabs--top {
align-items: center;
margin-top: 0;
margin-bottom: 2px;
padding: 0 0 8px;
border-top: none;
border-bottom: 1px solid rgba(148, 163, 184, 0.22);
max-height: 88px;
overflow: auto;
}
.bs-sheet-tabs--top .bs-sheet-tab {
border-radius: 6px;
border-bottom: 1px solid rgba(148, 163, 184, 0.28);
}
.bs-sheet-tabs--top .bs-sheet-tab.is-active {
box-shadow: none;
border-color: rgba(59, 130, 246, 0.55);
background: rgba(37, 99, 235, 0.2);
color: #93c5fd;
}
.bs-sheet-tab {
appearance: none;
border: 1px solid rgba(148, 163, 184, 0.28);

View file

@ -132,6 +132,45 @@ function neSourceOf(row: CliTargetItem): "managed" | "ume" {
return row.source === "ume" ? "ume" : "managed";
}
function columnsFromRows(rows: Record<string, unknown>[]): SheetCol[] {
const keys: string[] = [];
for (const row of rows) {
for (const k of Object.keys(row || {})) {
if (!keys.includes(k)) keys.push(k);
}
}
return keys.map((k) => ({ key: k, header: k }));
}
function metricSheetTitle(metricId: string, t: (k: string) => string): string {
const map: Record<string, string> = {
lldp_neighbor: t("bizState.sheetLldp"),
vrf_route_summary: t("bizState.sheetVrfRoute"),
isis_adjacency: t("bizState.sheetIsis"),
interface_brief: t("bizState.sheetIfaceBrief"),
arp: t("bizState.sheetArp"),
nd6_cache: t("bizState.sheetNd6"),
bgp_peer: t("bizState.sheetBgpPeer"),
};
return map[metricId] || metricId;
}
function rowsForMetric(batch: any, metricId: string): Record<string, unknown>[] {
const mid = String(metricId || "").trim();
if (!mid) return [];
const fromGeneric = (batch?.metrics || {})[mid];
if (Array.isArray(fromGeneric) && fromGeneric.length) {
return fromGeneric as Record<string, unknown>[];
}
if (mid === "lldp_neighbor") {
return ((batch?.lldp_neighbors || []) as Record<string, unknown>[]) || [];
}
if (mid === "vrf_route_summary") {
return ((batch?.vrf_route_summary || []) as Record<string, unknown>[]) || [];
}
return Array.isArray(fromGeneric) ? (fromGeneric as Record<string, unknown>[]) : [];
}
function buildBatchSheets(batch: any, t: (k: string) => string): SheetDef[] {
const sheets: SheetDef[] = [];
const cmds = (batch?.commands || []) as any[];
@ -156,32 +195,33 @@ function buildBatchSheets(batch: any, t: (k: string) => string): SheetDef[] {
})),
});
}
const lldp = (batch?.lldp_neighbors || []) as any[];
if (lldp.length) {
// Collect metric ids: command order first, then legacy / metrics payload
const metricOrder: string[] = [];
const pushMid = (mid: string) => {
const id = String(mid || "").trim();
if (!id || id === "vrf_list" || id === "commands") return;
if (!metricOrder.includes(id)) metricOrder.push(id);
};
for (const c of cmds) pushMid(String(c.metric_id || ""));
for (const mid of Object.keys(batch?.metrics || {})) pushMid(mid);
if ((batch?.lldp_neighbors || []).length) pushMid("lldp_neighbor");
if ((batch?.vrf_route_summary || []).length) pushMid("vrf_route_summary");
for (const mid of metricOrder) {
const rows = rowsForMetric(batch, mid);
// Still show a sheet when the command ran (even 0 rows) so empty/fail is visible
const ran = cmds.some((c) => String(c.metric_id || "") === mid);
if (!rows.length && !ran) continue;
const columns =
rows.length > 0
? columnsFromRows(rows)
: [{ key: "_empty", header: "—" }];
sheets.push({
id: "lldp_neighbor",
title: t("bizState.sheetLldp"),
columns: [
{ key: "local_if", header: "local_if" },
{ key: "remote_sys", header: "remote_sys" },
{ key: "remote_if", header: "remote_if" },
{ key: "remote_ip", header: "remote_ip" },
{ key: "protocol", header: "protocol" },
],
rows: lldp,
});
}
const vrf = (batch?.vrf_route_summary || []) as any[];
if (vrf.length) {
sheets.push({
id: "vrf_route_summary",
title: t("bizState.sheetVrfRoute"),
columns: [
{ key: "vrf", header: "vrf" },
{ key: "source", header: "source" },
{ key: "networks", header: "networks" },
],
rows: vrf,
id: mid,
title: metricSheetTitle(mid, t),
columns,
rows,
});
}
return sheets;
@ -559,7 +599,12 @@ export function BizStatePage() {
setSheetKeyword("");
setSheetColumn("");
const built = buildBatchSheets(d, t);
setSheetId(built[0]?.id || "");
// Prefer first metric with rows; fall back to commands / first tab
const prefer =
built.find((s) => s.id !== "commands" && s.rows.length > 0) ||
built.find((s) => s.id !== "commands") ||
built[0];
setSheetId(prefer?.id || "");
} catch (e) {
showError(formatErr(e));
}
@ -1119,6 +1164,27 @@ export function BizStatePage() {
</p>
) : null}
<div className="bs-sheet-tabs bs-sheet-tabs--top" role="tablist" aria-label={t("bizState.batchWorkbook")}>
{sheets.map((s) => (
<button
key={s.id}
type="button"
role="tab"
aria-selected={activeSheet?.id === s.id}
className={`bs-sheet-tab${activeSheet?.id === s.id ? " is-active" : ""}`}
onClick={() => {
setSheetId(s.id);
setSheetKeyword("");
setSheetColumn("");
}}
>
{s.title}
<span className="bs-sheet-tab__count">{s.rows.length}</span>
</button>
))}
{!sheets.length ? <span className="muted">{t("bizState.sheetEmpty")}</span> : null}
</div>
{activeSheet ? (
<>
<div className="filter-inline bs-sheet-filter">
@ -1171,7 +1237,7 @@ export function BizStatePage() {
))}
{!filteredSheetRows.length ? (
<tr>
<td colSpan={activeSheet.columns.length}>
<td colSpan={Math.max(1, activeSheet.columns.length)}>
<div className="pt-list-empty">{t("bizState.sheetEmpty")}</div>
</td>
</tr>
@ -1183,27 +1249,6 @@ export function BizStatePage() {
) : (
<div className="pt-list-empty">{t("bizState.sheetEmpty")}</div>
)}
<div className="bs-sheet-tabs" role="tablist" aria-label={t("bizState.batchWorkbook")}>
{sheets.map((s) => (
<button
key={s.id}
type="button"
role="tab"
aria-selected={activeSheet?.id === s.id}
className={`bs-sheet-tab${activeSheet?.id === s.id ? " is-active" : ""}`}
onClick={() => {
setSheetId(s.id);
setSheetKeyword("");
setSheetColumn("");
}}
>
{s.title}
<span className="bs-sheet-tab__count">{s.rows.length}</span>
</button>
))}
{!sheets.length ? <span className="muted">{t("bizState.sheetEmpty")}</span> : null}
</div>
</Modal.Body>
<Modal.Footer>
{batchDetail ? (