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:
oliver 2026-09-21 18:40:04 +08:00
parent 3576e4a69c
commit 771f7e21c2
6 changed files with 327 additions and 138 deletions

View file

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

View file

@ -238,6 +238,12 @@ const zh = {
bindTitle: "选择 VRF 绑定",
unbound: "未关联",
allVrfsDefault: "默认全部 VRF",
bindHintRequired: "需关联 VRF 参数后才可采集",
bindHintOptional: "可关联指定 VRF,未关联则采集全部",
discoverLoading: "正在发现 VRF…",
discoverEmpty: "未发现可用 VRF",
selectAllVrfs: "全选",
clearVrfs: "清空",
batches: "采集批次",
viewBatch: "查看",
export: "导出",

View file

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

View file

@ -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)}