Enable BGP neighbor out profiles and show binding details on double-click.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-22 09:26:37 +08:00
parent dd8d46aebc
commit bfac0b53c8
5 changed files with 149 additions and 11 deletions

View file

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

View file

@ -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",

View file

@ -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 同步",

View file

@ -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;

View file

@ -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<any>(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<string, string[]> = {};
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() {
<td className="bs-params-cell">
{needsBind ? (
<div
className={`bs-params-scroll${!binds.length ? " bs-params-warn" : ""}`}
title={bindHint}
className={`bs-params-scroll${
!bindLines.length ? " bs-params-warn" : ""
}${bindLines.length ? " bs-params-scroll--clickable" : ""}`}
title={
bindLines.length
? `${bindHint}\n${t("bizState.paramsDblClickHint")}`
: bindHint
}
onDoubleClick={() => {
if (!bindLines.length) return;
setParamsDetail({
title: prof.title,
lines: bindLines,
});
}}
>
{bindHint}
</div>
@ -1580,6 +1629,46 @@ export function BizStatePage() {
</Modal.Footer>
</AppModalShell>
{/* Bound params detail — double-click params cell */}
<AppModalShell
open={Boolean(paramsDetail)}
onClose={() => setParamsDetail(null)}
dismissible
size="md"
className="bs-bind-modal"
>
<Modal.Header>
<Modal.Heading>
{t("bizState.paramsDetailTitle")}
{paramsDetail?.title ? ` · ${paramsDetail.title}` : ""}
</Modal.Heading>
<Modal.CloseTrigger />
</Modal.Header>
<Modal.Body className="flex flex-col gap-2">
{paramsDetail?.lines?.length ? (
<>
<p className="muted">
{t("bizState.paramsDetailCount", { n: paramsDetail.lines.length })}
</p>
<div className="bs-bind-list bs-bind-list--modal bs-params-detail-list">
{paramsDetail.lines.map((line, idx) => (
<div key={`${idx}-${line}`} className="bs-params-detail-item">
<code>{line}</code>
</div>
))}
</div>
</>
) : (
<p className="muted">{t("bizState.unbound")}</p>
)}
</Modal.Body>
<Modal.Footer>
<Button size="sm" variant="ghost" onPress={() => setParamsDetail(null)}>
{t("bizState.cancel")}
</Button>
</Modal.Footer>
</AppModalShell>
{/* VRF discover / bind — blocking modal above task dialog */}
<AppModalShell
open={Boolean(bindItemId)}