From bb0d8e408927257ac42a0dd12dcb4d89bbf478da Mon Sep 17 00:00:00 2001 From: oliver Date: Thu, 17 Sep 2026 21:23:19 +0800 Subject: [PATCH] Show all collected metric sheets in biz-state batch workbook. Co-authored-by: Cursor --- web/src/i18n/en.ts | 5 + web/src/i18n/zh.ts | 5 + web/src/index.css | 23 ++++ web/src/pages/network/BizStatePage.tsx | 141 ++++++++++++++++--------- 4 files changed, 126 insertions(+), 48 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 2c0ea31..3ee3e38 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -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", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index eed8048..da7b8f1 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -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: "筛选列", diff --git a/web/src/index.css b/web/src/index.css index c3925b4..097359c 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -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); diff --git a/web/src/pages/network/BizStatePage.tsx b/web/src/pages/network/BizStatePage.tsx index 3c59e5e..df7a3b6 100644 --- a/web/src/pages/network/BizStatePage.tsx +++ b/web/src/pages/network/BizStatePage.tsx @@ -132,6 +132,45 @@ function neSourceOf(row: CliTargetItem): "managed" | "ume" { return row.source === "ume" ? "ume" : "managed"; } +function columnsFromRows(rows: Record[]): 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 = { + 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[] { + const mid = String(metricId || "").trim(); + if (!mid) return []; + const fromGeneric = (batch?.metrics || {})[mid]; + if (Array.isArray(fromGeneric) && fromGeneric.length) { + return fromGeneric as Record[]; + } + if (mid === "lldp_neighbor") { + return ((batch?.lldp_neighbors || []) as Record[]) || []; + } + if (mid === "vrf_route_summary") { + return ((batch?.vrf_route_summary || []) as Record[]) || []; + } + return Array.isArray(fromGeneric) ? (fromGeneric as Record[]) : []; +} + 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() {

) : null} +
+ {sheets.map((s) => ( + + ))} + {!sheets.length ? {t("bizState.sheetEmpty")} : null} +
+ {activeSheet ? ( <>
@@ -1171,7 +1237,7 @@ export function BizStatePage() { ))} {!filteredSheetRows.length ? ( - +
{t("bizState.sheetEmpty")}
@@ -1183,27 +1249,6 @@ export function BizStatePage() { ) : (
{t("bizState.sheetEmpty")}
)} - -
- {sheets.map((s) => ( - - ))} - {!sheets.length ? {t("bizState.sheetEmpty")} : null} -
{batchDetail ? (