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 ? (