mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Wire BGP neighbor in/out discover to config BGP peer intent.
Filter peers by AFI (and VRF pairs for CE), support zip/pair bindings in discover UI, and tag FIB rows with address_families. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3c3013dd73
commit
dd8d46aebc
10 changed files with 490 additions and 110 deletions
|
|
@ -233,13 +233,14 @@ const en = {
|
|||
command: "Command template",
|
||||
params: "Bindings",
|
||||
discoverVrf: "Discover VRFs",
|
||||
discoverBind: "Discover / Bind",
|
||||
saveBindings: "Save bindings",
|
||||
cancel: "Close",
|
||||
bindTitle: "Select VRF bindings",
|
||||
bindTitle: "Select bindings",
|
||||
unbound: "Not bound",
|
||||
bindHintRequired: "Bind VRF params before collect",
|
||||
discoverLoading: "Discovering VRFs…",
|
||||
discoverEmpty: "No VRFs discovered",
|
||||
bindHintRequired: "Bind params before collect",
|
||||
discoverLoading: "Discovering…",
|
||||
discoverEmpty: "No candidates discovered",
|
||||
selectAllVrfs: "Select all",
|
||||
deselectAllVrfs: "Deselect all",
|
||||
batches: "Batches",
|
||||
|
|
|
|||
|
|
@ -233,13 +233,14 @@ const zh = {
|
|||
command: "命令模板",
|
||||
params: "参数关联",
|
||||
discoverVrf: "发现 VRF",
|
||||
discoverBind: "发现 / 关联",
|
||||
saveBindings: "保存关联",
|
||||
cancel: "关闭",
|
||||
bindTitle: "选择 VRF 绑定",
|
||||
bindTitle: "选择参数关联",
|
||||
unbound: "未关联",
|
||||
bindHintRequired: "需关联 VRF 参数后才可采集",
|
||||
discoverLoading: "正在发现 VRF…",
|
||||
discoverEmpty: "未发现可用 VRF",
|
||||
bindHintRequired: "需关联参数后才可采集",
|
||||
discoverLoading: "正在发现…",
|
||||
discoverEmpty: "未发现可用选项",
|
||||
selectAllVrfs: "全选",
|
||||
deselectAllVrfs: "取消全选",
|
||||
batches: "采集批次",
|
||||
|
|
|
|||
|
|
@ -11274,24 +11274,74 @@ html.login-page--paused .login-page__flare {
|
|||
}
|
||||
|
||||
.bs-profiles-table-wrap {
|
||||
overflow-x: auto !important;
|
||||
overflow-x: scroll !important;
|
||||
overflow-y: auto;
|
||||
max-height: min(58vh, 560px);
|
||||
/* Keep horizontal scrollbar visible (WebKit). */
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
.bs-profiles-table-wrap::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.bs-profiles-table-wrap::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.45);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.bs-profiles-table {
|
||||
min-width: 960px;
|
||||
min-width: 1100px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.bs-profiles-table th:nth-child(1),
|
||||
.bs-profiles-table td:nth-child(1) {
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.bs-profiles-table th:nth-child(2),
|
||||
.bs-profiles-table td:nth-child(2) {
|
||||
width: 28%;
|
||||
}
|
||||
|
||||
.bs-profiles-table th:nth-child(3),
|
||||
.bs-profiles-table td:nth-child(3) {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.bs-profiles-table th:nth-child(4),
|
||||
.bs-profiles-table td:nth-child(4) {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.bs-profiles-table .bs-cmd-cell {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.bs-profiles-table .bs-params-cell {
|
||||
min-width: 140px;
|
||||
max-width: 280px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.bs-profiles-table .bs-params-scroll {
|
||||
max-width: 100%;
|
||||
max-height: 4.5em;
|
||||
overflow: auto;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
line-height: 1.35;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bs-profiles-table .bs-actions-cell {
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bs-bind-hint {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,12 @@ type BatchRow = {
|
|||
protect_reasons?: string[];
|
||||
};
|
||||
|
||||
type Candidate = { value: string; label: string; rd?: string };
|
||||
type Candidate = {
|
||||
value: string;
|
||||
label: string;
|
||||
rd?: string;
|
||||
bindings?: Record<string, string>;
|
||||
};
|
||||
|
||||
type SheetCol = { key: string; header: string };
|
||||
|
||||
|
|
@ -770,7 +775,8 @@ export function BizStatePage() {
|
|||
const startDiscover = async (item: any) => {
|
||||
if (!taskId) return;
|
||||
const prof = profiles.find((p) => p.profile_id === item.source_profile_id);
|
||||
const ph = (prof?.placeholders || [])[0];
|
||||
const phs = prof?.placeholders || [];
|
||||
const ph = phs[0];
|
||||
if (!ph) {
|
||||
showError(t("bizState.noNeedBind"));
|
||||
return;
|
||||
|
|
@ -783,10 +789,12 @@ export function BizStatePage() {
|
|||
setDiscoverLoading(true);
|
||||
setBusy(true);
|
||||
try {
|
||||
// Shared discover_profile_id → omit placeholder so API returns pair candidates.
|
||||
const sharedDisc = phs.length > 1 && phs.every((p) => p.discover_profile_id === ph.discover_profile_id);
|
||||
const res = await bizStateDiscover({
|
||||
task_id: taskId,
|
||||
collect_profile_id: item.source_profile_id,
|
||||
placeholder: ph.name,
|
||||
placeholder: sharedDisc ? "" : ph.name,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = res.error || t("bizState.discoverFailed");
|
||||
|
|
@ -797,11 +805,34 @@ export function BizStatePage() {
|
|||
setDiscoverCmd(res.command || "");
|
||||
const cand = (res.candidates || []) as Candidate[];
|
||||
setCandidates(cand);
|
||||
const existing = (item.bindings || [])
|
||||
.filter((b: any) => b.placeholder === ph.name)
|
||||
.map((b: any) => String(b.value));
|
||||
// Keep prior bindings if still in candidates; otherwise start with none selected.
|
||||
const keep = existing.filter((v: string) => cand.some((c) => c.value === v));
|
||||
const existing = (item.bindings || []) as { placeholder?: string; value?: string }[];
|
||||
let keep: string[] = [];
|
||||
if (res.pair_mode || sharedDisc) {
|
||||
const phNames = phs.map((p) => p.name);
|
||||
const byPh: Record<string, string[]> = {};
|
||||
for (const b of existing) {
|
||||
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;
|
||||
const existingKeys = new Set<string>();
|
||||
for (let i = 0; i < n; i++) {
|
||||
const parts = phNames
|
||||
.slice()
|
||||
.sort()
|
||||
.map((name) => `${name}=${(byPh[name] || [])[i] || ""}`);
|
||||
existingKeys.add(parts.join("|"));
|
||||
}
|
||||
keep = cand.filter((c) => existingKeys.has(c.value)).map((c) => c.value);
|
||||
} else {
|
||||
const vals = existing
|
||||
.filter((b) => b.placeholder === ph.name)
|
||||
.map((b) => String(b.value));
|
||||
keep = vals.filter((v) => cand.some((c) => c.value === v));
|
||||
}
|
||||
setSelectedVrfs(keep);
|
||||
if (!cand.length) {
|
||||
setDiscoverError(t("bizState.discoverEmpty"));
|
||||
|
|
@ -823,13 +854,22 @@ export function BizStatePage() {
|
|||
const phName = (prof?.placeholders || [])[0]?.name || "vrf";
|
||||
const picked = values !== undefined ? values : selectedVrfs;
|
||||
if (!picked.length) return;
|
||||
const candByVal = new Map(candidates.map((c) => [c.value, c]));
|
||||
const rows: { placeholder: string; value: string }[] = [];
|
||||
for (const v of picked) {
|
||||
const c = candByVal.get(v);
|
||||
const binds = c?.bindings;
|
||||
if (binds && Object.keys(binds).length) {
|
||||
for (const [k, val] of Object.entries(binds)) {
|
||||
if (k && val) rows.push({ placeholder: k, value: String(val) });
|
||||
}
|
||||
} else {
|
||||
rows.push({ placeholder: phName, value: v });
|
||||
}
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await bizStateSetBindings(
|
||||
taskId,
|
||||
bindItemId,
|
||||
picked.map((v) => ({ placeholder: phName, value: v })),
|
||||
);
|
||||
await bizStateSetBindings(taskId, bindItemId, rows);
|
||||
showOk(t("bizState.bindingsSaved"));
|
||||
await loadTask(taskId);
|
||||
closeBindModal();
|
||||
|
|
@ -1341,8 +1381,8 @@ export function BizStatePage() {
|
|||
<th>{t("bizState.enable")}</th>
|
||||
<th>{t("bizState.profiles")}</th>
|
||||
<th>{t("bizState.params")}</th>
|
||||
<th>{t("bizState.colActions")}</th>
|
||||
<th>{t("bizState.command")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -1377,17 +1417,17 @@ export function BizStatePage() {
|
|||
</td>
|
||||
<td className="bs-params-cell">
|
||||
{needsBind ? (
|
||||
<span className={!binds.length ? "bs-params-warn" : undefined}>
|
||||
<div
|
||||
className={`bs-params-scroll${!binds.length ? " bs-params-warn" : ""}`}
|
||||
title={bindHint}
|
||||
>
|
||||
{bindHint}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<code className="bs-cmd-cell">{prof.command_template}</code>
|
||||
</td>
|
||||
<td>
|
||||
<td className="bs-actions-cell">
|
||||
{needsBind && enabled && it ? (
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
@ -1395,9 +1435,16 @@ export function BizStatePage() {
|
|||
isDisabled={busy || discoverLoading}
|
||||
onPress={() => void startDiscover(it)}
|
||||
>
|
||||
{t("bizState.discoverVrf")}
|
||||
{t("bizState.discoverBind")}
|
||||
</Button>
|
||||
) : null}
|
||||
) : needsBind ? (
|
||||
<span className="muted">—</span>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<code className="bs-cmd-cell">{prof.command_template}</code>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue