mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
Improve VRF bind UX with modal and share CLI cache across lanes.
Open a blocking discover dialog for VRF selection, reorder profile columns with scroll/hints, and reuse shared collect cache between light/heavy monitor items. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3576e4a69c
commit
771f7e21c2
6 changed files with 327 additions and 138 deletions
|
|
@ -238,6 +238,12 @@ const en = {
|
|||
bindTitle: "Select VRF bindings",
|
||||
unbound: "Not bound",
|
||||
allVrfsDefault: "All VRFs (default)",
|
||||
bindHintRequired: "Bind VRF params before collect",
|
||||
bindHintOptional: "Bind VRFs, or leave empty for all",
|
||||
discoverLoading: "Discovering VRFs…",
|
||||
discoverEmpty: "No VRFs discovered",
|
||||
selectAllVrfs: "Select all",
|
||||
clearVrfs: "Clear",
|
||||
batches: "Batches",
|
||||
viewBatch: "Open",
|
||||
export: "Export",
|
||||
|
|
|
|||
|
|
@ -238,6 +238,12 @@ const zh = {
|
|||
bindTitle: "选择 VRF 绑定",
|
||||
unbound: "未关联",
|
||||
allVrfsDefault: "默认全部 VRF",
|
||||
bindHintRequired: "需关联 VRF 参数后才可采集",
|
||||
bindHintOptional: "可关联指定 VRF,未关联则采集全部",
|
||||
discoverLoading: "正在发现 VRF…",
|
||||
discoverEmpty: "未发现可用 VRF",
|
||||
selectAllVrfs: "全选",
|
||||
clearVrfs: "清空",
|
||||
batches: "采集批次",
|
||||
viewBatch: "查看",
|
||||
export: "导出",
|
||||
|
|
|
|||
|
|
@ -11261,11 +11261,52 @@ html.login-page--paused .login-page__flare {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
.bs-bind-list--modal {
|
||||
max-height: min(48vh, 360px);
|
||||
padding: 4px 2px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.bs-bind-item {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.bs-profiles-table-wrap {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto;
|
||||
max-height: min(58vh, 560px);
|
||||
}
|
||||
|
||||
.bs-profiles-table {
|
||||
min-width: 960px;
|
||||
}
|
||||
|
||||
.bs-profiles-table .bs-cmd-cell {
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bs-profiles-table .bs-params-cell {
|
||||
min-width: 140px;
|
||||
max-width: 280px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.bs-bind-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bs-params-warn {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.bs-bind-modal.app-heroui-modal {
|
||||
z-index: 80;
|
||||
}
|
||||
|
||||
/* biz_state compare result workbook / board — dark surfaces (match nm modal) */
|
||||
.bs-cmp-job-body {
|
||||
min-height: min(64vh, 680px);
|
||||
|
|
|
|||
|
|
@ -234,11 +234,13 @@ export function BizStatePage() {
|
|||
const [dailyKeepCount, setDailyKeepCount] = useState(10);
|
||||
const [selectedBatchIds, setSelectedBatchIds] = useState<string[]>([]);
|
||||
|
||||
// VRF bind (inside task modal)
|
||||
// VRF bind modal (blocking)
|
||||
const [bindItemId, setBindItemId] = useState("");
|
||||
const [candidates, setCandidates] = useState<Candidate[]>([]);
|
||||
const [selectedVrfs, setSelectedVrfs] = useState<string[]>([]);
|
||||
const [discoverCmd, setDiscoverCmd] = useState("");
|
||||
const [discoverLoading, setDiscoverLoading] = useState(false);
|
||||
const [discoverError, setDiscoverError] = useState("");
|
||||
|
||||
// batch workbook modal (summary + lazy-paged metric sheets)
|
||||
const [batchDetail, setBatchDetail] = useState<any>(null);
|
||||
|
|
@ -756,6 +758,15 @@ export function BizStatePage() {
|
|||
}
|
||||
};
|
||||
|
||||
const closeBindModal = () => {
|
||||
setBindItemId("");
|
||||
setCandidates([]);
|
||||
setSelectedVrfs([]);
|
||||
setDiscoverCmd("");
|
||||
setDiscoverError("");
|
||||
setDiscoverLoading(false);
|
||||
};
|
||||
|
||||
const startDiscover = async (item: any) => {
|
||||
if (!taskId) return;
|
||||
const prof = profiles.find((p) => p.profile_id === item.source_profile_id);
|
||||
|
|
@ -764,8 +775,13 @@ export function BizStatePage() {
|
|||
showError(t("bizState.noNeedBind"));
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setBindItemId(item.id);
|
||||
setCandidates([]);
|
||||
setSelectedVrfs([]);
|
||||
setDiscoverCmd("");
|
||||
setDiscoverError("");
|
||||
setDiscoverLoading(true);
|
||||
setBusy(true);
|
||||
try {
|
||||
const res = await bizStateDiscover({
|
||||
task_id: taskId,
|
||||
|
|
@ -773,8 +789,9 @@ export function BizStatePage() {
|
|||
placeholder: ph.name,
|
||||
});
|
||||
if (!res.ok) {
|
||||
showError(res.error || t("bizState.discoverFailed"));
|
||||
setCandidates([]);
|
||||
const err = res.error || t("bizState.discoverFailed");
|
||||
setDiscoverError(err);
|
||||
showError(err);
|
||||
return;
|
||||
}
|
||||
setDiscoverCmd(res.command || "");
|
||||
|
|
@ -784,9 +801,15 @@ export function BizStatePage() {
|
|||
.filter((b: any) => b.placeholder === ph.name)
|
||||
.map((b: any) => String(b.value));
|
||||
setSelectedVrfs(existing.length ? existing : cand.map((c) => c.value));
|
||||
if (!cand.length) {
|
||||
setDiscoverError(t("bizState.discoverEmpty"));
|
||||
}
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
const err = formatErr(e);
|
||||
setDiscoverError(err);
|
||||
showError(err);
|
||||
} finally {
|
||||
setDiscoverLoading(false);
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
|
@ -808,8 +831,7 @@ export function BizStatePage() {
|
|||
);
|
||||
showOk(t("bizState.bindingsSaved"));
|
||||
await loadTask(taskId);
|
||||
setBindItemId("");
|
||||
setCandidates([]);
|
||||
closeBindModal();
|
||||
} catch (e) {
|
||||
showError(formatErr(e));
|
||||
} finally {
|
||||
|
|
@ -1311,14 +1333,14 @@ export function BizStatePage() {
|
|||
|
||||
{taskTab === "profiles" ? (
|
||||
<>
|
||||
<div className="pt-list-table-wrap">
|
||||
<table className="data-table pt-list-table">
|
||||
<div className="pt-list-table-wrap bs-profiles-table-wrap">
|
||||
<table className="data-table pt-list-table bs-profiles-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("bizState.enable")}</th>
|
||||
<th>{t("bizState.profiles")}</th>
|
||||
<th>{t("bizState.command")}</th>
|
||||
<th>{t("bizState.params")}</th>
|
||||
<th>{t("bizState.command")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -1333,6 +1355,13 @@ export function BizStatePage() {
|
|||
const bindOptional = (prof.placeholders || []).every(
|
||||
(ph) => ph.required === false,
|
||||
);
|
||||
const bindHint = needsBind
|
||||
? binds.length
|
||||
? binds.map((b: any) => b.value).join(", ")
|
||||
: bindOptional
|
||||
? t("bizState.allVrfsDefault")
|
||||
: t("bizState.unbound")
|
||||
: "—";
|
||||
return (
|
||||
<tr key={prof.profile_id}>
|
||||
<td>
|
||||
|
|
@ -1346,25 +1375,38 @@ export function BizStatePage() {
|
|||
<td>
|
||||
<div className="pt-list-task-name">{prof.title}</div>
|
||||
{prof.description ? <div className="muted">{prof.description}</div> : null}
|
||||
{needsBind ? (
|
||||
<div className="bs-bind-hint muted">
|
||||
{bindOptional
|
||||
? t("bizState.bindHintOptional")
|
||||
: t("bizState.bindHintRequired")}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="bs-params-cell">
|
||||
{needsBind ? (
|
||||
<span
|
||||
className={
|
||||
!binds.length && !bindOptional
|
||||
? "bs-params-warn"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{bindHint}
|
||||
</span>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<code>{prof.command_template}</code>
|
||||
</td>
|
||||
<td>
|
||||
{needsBind
|
||||
? binds.length
|
||||
? binds.map((b: any) => b.value).join(", ")
|
||||
: bindOptional
|
||||
? t("bizState.allVrfsDefault")
|
||||
: t("bizState.unbound")
|
||||
: "—"}
|
||||
<code className="bs-cmd-cell">{prof.command_template}</code>
|
||||
</td>
|
||||
<td>
|
||||
{needsBind && enabled && it ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={busy}
|
||||
isDisabled={busy || discoverLoading}
|
||||
onPress={() => void startDiscover(it)}
|
||||
>
|
||||
{t("bizState.discoverVrf")}
|
||||
|
|
@ -1377,76 +1419,6 @@ export function BizStatePage() {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{bindItemId && candidates.length ? (
|
||||
<div className="bs-bind-panel">
|
||||
<h4 style={{ margin: "0 0 8px" }}>{t("bizState.bindTitle")}</h4>
|
||||
<p className="muted">
|
||||
<code>{discoverCmd}</code> · {selectedVrfs.length}
|
||||
</p>
|
||||
<div className="bs-bind-list">
|
||||
{candidates.map((c) => (
|
||||
<label key={c.value} className="bs-bind-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedVrfs.includes(c.value)}
|
||||
onChange={(e) => {
|
||||
setSelectedVrfs((prev) =>
|
||||
e.target.checked
|
||||
? [...prev, c.value]
|
||||
: prev.filter((x) => x !== c.value),
|
||||
);
|
||||
}}
|
||||
/>{" "}
|
||||
{c.label}
|
||||
{c.rd ? <span className="muted"> · RD {c.rd}</span> : null}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="btn-row" style={{ marginTop: 8 }}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
isDisabled={
|
||||
busy ||
|
||||
(!selectedVrfs.length &&
|
||||
!(
|
||||
profiles
|
||||
.find(
|
||||
(p) =>
|
||||
p.profile_id ===
|
||||
(detail?.items || []).find((it: any) => it.id === bindItemId)
|
||||
?.source_profile_id,
|
||||
)
|
||||
?.placeholders || []
|
||||
).every((ph) => ph.required === false))
|
||||
}
|
||||
onPress={() => void saveBindings()}
|
||||
>
|
||||
{t("bizState.saveBindings")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={busy}
|
||||
onPress={() => void saveBindings([])}
|
||||
>
|
||||
{t("bizState.allVrfsDefault")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={busy}
|
||||
onPress={() => {
|
||||
setBindItemId("");
|
||||
setCandidates([]);
|
||||
}}
|
||||
>
|
||||
{t("bizState.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="pt-list-table-wrap">
|
||||
|
|
@ -1575,6 +1547,113 @@ export function BizStatePage() {
|
|||
</Modal.Footer>
|
||||
</AppModalShell>
|
||||
|
||||
{/* VRF discover / bind — blocking modal above task dialog */}
|
||||
<AppModalShell
|
||||
open={Boolean(bindItemId)}
|
||||
onClose={closeBindModal}
|
||||
dismissible={!discoverLoading && !busy}
|
||||
size="md"
|
||||
className="bs-bind-modal"
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Heading>{t("bizState.bindTitle")}</Modal.Heading>
|
||||
<Modal.CloseTrigger />
|
||||
</Modal.Header>
|
||||
<Modal.Body className="flex flex-col gap-3">
|
||||
{discoverLoading ? (
|
||||
<p className="muted">{t("bizState.discoverLoading")}</p>
|
||||
) : null}
|
||||
{discoverCmd ? (
|
||||
<p className="muted">
|
||||
<code>{discoverCmd}</code>
|
||||
{candidates.length ? ` · ${selectedVrfs.length}/${candidates.length}` : null}
|
||||
</p>
|
||||
) : null}
|
||||
{discoverError ? <p className="bs-params-warn">{discoverError}</p> : null}
|
||||
{!discoverLoading && candidates.length ? (
|
||||
<>
|
||||
<div className="btn-row" style={{ gap: 8 }}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
isDisabled={busy}
|
||||
onPress={() => setSelectedVrfs(candidates.map((c) => c.value))}
|
||||
>
|
||||
{t("bizState.selectAllVrfs")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
isDisabled={busy}
|
||||
onPress={() => setSelectedVrfs([])}
|
||||
>
|
||||
{t("bizState.clearVrfs")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bs-bind-list bs-bind-list--modal">
|
||||
{candidates.map((c) => (
|
||||
<label key={c.value} className="bs-bind-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedVrfs.includes(c.value)}
|
||||
disabled={busy}
|
||||
onChange={(e) => {
|
||||
setSelectedVrfs((prev) =>
|
||||
e.target.checked
|
||||
? [...prev, c.value]
|
||||
: prev.filter((x) => x !== c.value),
|
||||
);
|
||||
}}
|
||||
/>{" "}
|
||||
{c.label}
|
||||
{c.rd ? <span className="muted"> · RD {c.rd}</span> : null}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{(() => {
|
||||
const item = (detail?.items || []).find((it: any) => it.id === bindItemId);
|
||||
const prof = profiles.find((p) => p.profile_id === item?.source_profile_id);
|
||||
const bindOptional = (prof?.placeholders || []).every((ph) => ph.required === false);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
isDisabled={
|
||||
busy || discoverLoading || (!selectedVrfs.length && !bindOptional)
|
||||
}
|
||||
onPress={() => void saveBindings()}
|
||||
>
|
||||
{t("bizState.saveBindings")}
|
||||
</Button>
|
||||
{bindOptional ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={busy || discoverLoading}
|
||||
onPress={() => void saveBindings([])}
|
||||
>
|
||||
{t("bizState.allVrfsDefault")}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
isDisabled={discoverLoading}
|
||||
onPress={closeBindModal}
|
||||
>
|
||||
{t("bizState.cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Modal.Footer>
|
||||
</AppModalShell>
|
||||
|
||||
{/* Batch workbook: summary + lazy-paged metric sheets */}
|
||||
<AppModalShell
|
||||
open={Boolean(batchDetail)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue