From bfac0b53c838b668ef43446b6038ede8f0766387 Mon Sep 17 00:00:00 2001 From: oliver Date: Tue, 22 Sep 2026 09:26:37 +0800 Subject: [PATCH] Enable BGP neighbor out profiles and show binding details on double-click. Co-authored-by: Cursor --- netx_api/biz_state/profiles.py | 12 ++-- web/src/i18n/en.ts | 10 +++ web/src/i18n/zh.ts | 10 +++ web/src/index.css | 29 +++++++- web/src/pages/network/BizStatePage.tsx | 99 ++++++++++++++++++++++++-- 5 files changed, 149 insertions(+), 11 deletions(-) diff --git a/netx_api/biz_state/profiles.py b/netx_api/biz_state/profiles.py index cf88351..d0ef0a0 100644 --- a/netx_api/biz_state/profiles.py +++ b/netx_api/biz_state/profiles.py @@ -997,8 +997,9 @@ def _zte_status_profiles() -> list[ParseProfile]: fields=list(_BGP_ROUTE_FIELDS), tags=["bgp", "vpnv4", "route"], sort_order=425, - enabled=False, + enabled=True, kind="collect", + collect_lane="heavy", aux_commands=[AuxCommand(key="bgp_summary", profile_id="zte.bgp_vpnv4_summary")], enrich_joins=[ EnrichJoin( @@ -1047,8 +1048,9 @@ def _zte_status_profiles() -> list[ParseProfile]: fields=list(_BGP_ROUTE_FIELDS), tags=["bgp", "vpnv6", "route"], sort_order=427, - enabled=False, + enabled=True, kind="collect", + collect_lane="heavy", aux_commands=[AuxCommand(key="bgp_summary", profile_id="zte.bgp_vpnv6_summary")], enrich_joins=[ EnrichJoin( @@ -1103,8 +1105,9 @@ def _zte_status_profiles() -> list[ParseProfile]: fields=list(_BGP_ROUTE_FIELDS), tags=["bgp", "vpnv4", "vrf", "route"], sort_order=435, - enabled=False, + enabled=True, kind="collect", + collect_lane="heavy", aux_commands=[AuxCommand(key="bgp_summary", profile_id="zte.bgp_vpnv4_vrf_summary")], enrich_joins=[ EnrichJoin( @@ -1159,8 +1162,9 @@ def _zte_status_profiles() -> list[ParseProfile]: fields=list(_BGP_ROUTE_FIELDS), tags=["bgp", "vpnv6", "vrf", "route"], sort_order=445, - enabled=False, + enabled=True, kind="collect", + collect_lane="heavy", aux_commands=[AuxCommand(key="bgp_summary", profile_id="zte.bgp_vpnv6_vrf_summary")], enrich_joins=[ EnrichJoin( diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 3c1bbb1..55529af 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -239,6 +239,9 @@ const en = { bindTitle: "Select bindings", unbound: "Not bound", bindHintRequired: "Bind params before collect", + paramsDblClickHint: "Double-click to view all bindings", + paramsDetailTitle: "Bound parameters", + paramsDetailCount: "{{n}} binding(s)", discoverLoading: "Discovering…", discoverEmpty: "No candidates discovered", selectAllVrfs: "Select all", @@ -1564,6 +1567,7 @@ const en = { name: "Name", vendor: "Vendor", deviceType: "Device type", + execPolicy: "CLI exec policy", source: "Source", ip: "Management IP", port: "Port", @@ -1576,6 +1580,12 @@ const en = { testedAt: "Tested at", actions: "Actions", }, + execPolicy: { + readonly: "Read-only (show/display/ping)", + linuxShell: "Linux shell (single line)", + unrestricted: "Lab open (single line)", + hint: "Applies to MCP/API execManagedNe only. Default is read-only; shell / lab-open require a Linux device type.", + }, source: { manual: "Manual", ume_sync: "UME sync", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 69e8400..616b9d4 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -239,6 +239,9 @@ const zh = { bindTitle: "选择参数关联", unbound: "未关联", bindHintRequired: "需关联参数后才可采集", + paramsDblClickHint: "双击查看全部关联明细", + paramsDetailTitle: "参数关联明细", + paramsDetailCount: "共 {{n}} 条", discoverLoading: "正在发现…", discoverEmpty: "未发现可用选项", selectAllVrfs: "全选", @@ -1551,6 +1554,7 @@ const zh = { name: "名称", vendor: "厂商", deviceType: "设备类型", + execPolicy: "CLI 执行策略", source: "来源", ip: "管理地址", port: "端口", @@ -1563,6 +1567,12 @@ const zh = { testedAt: "测试时间", actions: "操作", }, + execPolicy: { + readonly: "只读(show/display/ping)", + linuxShell: "Linux shell(单行)", + unrestricted: "实验室全开(单行)", + hint: "仅影响 MCP/API 的 execManagedNe。默认只读;仅 Linux 设备类型可选 shell / 实验室全开。", + }, source: { manual: "手工", ume_sync: "UME 同步", diff --git a/web/src/index.css b/web/src/index.css index 95a3e4a..4149236 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -11331,14 +11331,39 @@ html.login-page--paused .login-page__flare { .bs-profiles-table .bs-params-scroll { max-width: 100%; - max-height: 4.5em; - overflow: auto; + max-height: 3.2em; + overflow: hidden; white-space: normal; word-break: break-word; line-height: 1.35; font-size: 12px; } +.bs-profiles-table .bs-params-scroll--clickable { + cursor: pointer; + user-select: none; +} + +.bs-profiles-table .bs-params-scroll--clickable:hover { + text-decoration: underline; + text-underline-offset: 2px; +} + +.bs-params-detail-list { + max-height: min(50vh, 420px); +} + +.bs-params-detail-item { + padding: 4px 0; + border-bottom: 1px solid rgba(148, 163, 184, 0.2); + font-size: 12px; + word-break: break-all; +} + +.bs-params-detail-item:last-child { + border-bottom: none; +} + .bs-profiles-table .bs-actions-cell { white-space: nowrap; vertical-align: middle; diff --git a/web/src/pages/network/BizStatePage.tsx b/web/src/pages/network/BizStatePage.tsx index 7ce9303..b201843 100644 --- a/web/src/pages/network/BizStatePage.tsx +++ b/web/src/pages/network/BizStatePage.tsx @@ -247,6 +247,12 @@ export function BizStatePage() { const [discoverLoading, setDiscoverLoading] = useState(false); const [discoverError, setDiscoverError] = useState(""); + // Bound-params detail modal (double-click params cell) + const [paramsDetail, setParamsDetail] = useState<{ + title: string; + lines: string[]; + } | null>(null); + // batch workbook modal (summary + lazy-paged metric sheets) const [batchDetail, setBatchDetail] = useState(null); const [sheetId, setSheetId] = useState(""); @@ -1391,11 +1397,41 @@ export function BizStatePage() { (x: any) => x.source_profile_id === prof.profile_id, ); const enabled = Boolean(it?.enabled); - const binds = it?.bindings || []; + const binds = (it?.bindings || []) as { + placeholder?: string; + value?: string; + }[]; const needsBind = (prof.placeholders || []).length > 0; + const phNames = (prof.placeholders || []).map((p) => p.name); + let bindLines: string[] = []; + if (needsBind && binds.length) { + if (phNames.length >= 2) { + const byPh: Record = {}; + for (const b of binds) { + const name = String(b.placeholder || ""); + const val = String(b.value || ""); + if (!name || !val) continue; + (byPh[name] ||= []).push(val); + } + const counts = phNames.map((n) => (byPh[n] || []).length); + const n = counts.length ? Math.min(...counts) : 0; + for (let i = 0; i < n; i++) { + bindLines.push( + phNames.map((name) => `${name}=${(byPh[name] || [])[i] || ""}`).join(" · "), + ); + } + } else { + const ph = phNames[0] || "value"; + bindLines = binds + .filter((b) => String(b.value || "").trim()) + .map((b) => `${ph}=${String(b.value)}`); + } + } const bindHint = needsBind - ? binds.length - ? binds.map((b: any) => b.value).join(", ") + ? bindLines.length + ? bindLines.length <= 2 + ? bindLines.join("; ") + : `${bindLines.slice(0, 2).join("; ")} … (+${bindLines.length - 2})` : t("bizState.unbound") : "—"; return ( @@ -1418,8 +1454,21 @@ export function BizStatePage() { {needsBind ? (
{ + if (!bindLines.length) return; + setParamsDetail({ + title: prof.title, + lines: bindLines, + }); + }} > {bindHint}
@@ -1580,6 +1629,46 @@ export function BizStatePage() { + {/* Bound params detail — double-click params cell */} + setParamsDetail(null)} + dismissible + size="md" + className="bs-bind-modal" + > + + + {t("bizState.paramsDetailTitle")} + {paramsDetail?.title ? ` · ${paramsDetail.title}` : ""} + + + + + {paramsDetail?.lines?.length ? ( + <> +

+ {t("bizState.paramsDetailCount", { n: paramsDetail.lines.length })} +

+
+ {paramsDetail.lines.map((line, idx) => ( +
+ {line} +
+ ))} +
+ + ) : ( +

{t("bizState.unbound")}

+ )} +
+ + + +
+ {/* VRF discover / bind — blocking modal above task dialog */}