Use schedule checkbox for biz-state; add compare job delete action.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-17 19:11:50 +08:00
parent 108c9da00f
commit e9b8042ca7
5 changed files with 109 additions and 60 deletions

View file

@ -183,12 +183,16 @@ const en = {
colConnect: "Connect", colConnect: "Connect",
start: "Start schedule", start: "Start schedule",
pause: "Pause", pause: "Pause",
scheduleEnabled: "Enable schedule",
scheduleOn: "Scheduled",
scheduleOff: "Manual",
scheduleHint: "When checked, collect on interval; otherwise only Collect now.",
collectNow: "Collect now", collectNow: "Collect now",
delete: "Delete", delete: "Delete",
confirmDelete: "Delete this task and all batches?", confirmDelete: "Delete this task and all batches?",
deleted: "Deleted", deleted: "Deleted",
started: "Schedule started", started: "Schedule enabled",
paused: "Paused", paused: "Schedule off (manual only)",
collecting: "Collecting", collecting: "Collecting",
detail: "Details", detail: "Details",
interval: "Interval", interval: "Interval",
@ -244,6 +248,7 @@ const en = {
templateSaved: "Template saved", templateSaved: "Template saved",
templateDeleted: "Template deleted", templateDeleted: "Template deleted",
confirmDeleteTemplate: "Delete this compare template?", confirmDeleteTemplate: "Delete this compare template?",
confirmDeleteJob: "Delete this compare job and all runs?",
emptyTemplates: "No templates yet. Create one or use built-ins.", emptyTemplates: "No templates yet. Create one or use built-ins.",
templateHint: templateHint:
"A template holds multiple metric sheets. Key: row identity (no separate compare). Interface: port mapping. Compare: value checks after key match. Leave compare empty for presence-only (entry set match).", "A template holds multiple metric sheets. Key: row identity (no separate compare). Interface: port mapping. Compare: value checks after key match. Leave compare empty for presence-only (entry set match).",
@ -296,6 +301,7 @@ const en = {
runNow: "Run now", runNow: "Run now",
saveJob: "Save config", saveJob: "Save config",
jobSaved: "Job config saved", jobSaved: "Job config saved",
jobDeleted: "Compare job deleted",
jobList: "Compare jobs", jobList: "Compare jobs",
detail: "Details", detail: "Details",
edit: "Edit", edit: "Edit",

View file

@ -183,12 +183,16 @@ const zh = {
colConnect: "连通", colConnect: "连通",
start: "启动周期", start: "启动周期",
pause: "暂停", pause: "暂停",
scheduleEnabled: "启用周期调度",
scheduleOn: "周期",
scheduleOff: "手动",
scheduleHint: "勾选后按周期自动采集;不勾选则仅支持「立即采集」。",
collectNow: "立即采集", collectNow: "立即采集",
delete: "删除", delete: "删除",
confirmDelete: "删除该业务监控任务及所有批次?", confirmDelete: "删除该业务监控任务及所有批次?",
deleted: "已删除", deleted: "已删除",
started: "已启动周期采集", started: "已启用周期采集",
paused: "已暂停", paused: "已关闭周期(仅手动采集)",
collecting: "采集中", collecting: "采集中",
detail: "详情", detail: "详情",
interval: "采集周期", interval: "采集周期",
@ -244,6 +248,7 @@ const zh = {
templateSaved: "模板已保存", templateSaved: "模板已保存",
templateDeleted: "模板已删除", templateDeleted: "模板已删除",
confirmDeleteTemplate: "删除该比对模板?", confirmDeleteTemplate: "删除该比对模板?",
confirmDeleteJob: "删除该比对任务及全部运行记录?",
emptyTemplates: "暂无模板。可新建或使用系统默认。", emptyTemplates: "暂无模板。可新建或使用系统默认。",
templateHint: templateHint:
"一个模板可包含多张监控表。Key:对齐前后条目(选中后即为行身份,无需再勾比对);接口:参与端口映射;比对:勾选具体参数做值检查。若不勾任何比对字段,则只校验条目是否一致(存在性)。", "一个模板可包含多张监控表。Key:对齐前后条目(选中后即为行身份,无需再勾比对);接口:参与端口映射;比对:勾选具体参数做值检查。若不勾任何比对字段,则只校验条目是否一致(存在性)。",
@ -295,6 +300,7 @@ const zh = {
runNow: "立即比对", runNow: "立即比对",
saveJob: "保存配置", saveJob: "保存配置",
jobSaved: "任务配置已保存", jobSaved: "任务配置已保存",
jobDeleted: "比对任务已删除",
jobList: "比对任务", jobList: "比对任务",
detail: "详情", detail: "详情",
edit: "编辑", edit: "编辑",

View file

@ -9,6 +9,7 @@ import {
bizCompareCreateJob, bizCompareCreateJob,
bizCompareCreateMapping, bizCompareCreateMapping,
bizCompareCreateTemplate, bizCompareCreateTemplate,
bizCompareDeleteJob,
bizCompareDeleteTemplate, bizCompareDeleteTemplate,
bizCompareGetRun, bizCompareGetRun,
bizCompareListJobs, bizCompareListJobs,
@ -471,6 +472,21 @@ export function BizComparePage() {
} }
}; };
const removeJob = async (id: string) => {
if (!window.confirm(t("bizCompare.confirmDeleteJob"))) return;
setBusy(true);
try {
await bizCompareDeleteJob(id);
if (jobId === id) closeJob();
showOk(t("bizCompare.jobDeleted"));
await refresh();
} catch (e) {
showError(formatErr(e));
} finally {
setBusy(false);
}
};
const parseMapRows = () => { const parseMapRows = () => {
const rows: { before_if: string; after_if: string }[] = []; const rows: { before_if: string; after_if: string }[] = [];
for (const line of mapText.split(/\r?\n/)) { for (const line of mapText.split(/\r?\n/)) {
@ -945,6 +961,14 @@ export function BizComparePage() {
<Button size="sm" variant="primary" onPress={() => void openJob(j.id)}> <Button size="sm" variant="primary" onPress={() => void openJob(j.id)}>
{t("bizCompare.detail")} {t("bizCompare.detail")}
</Button> </Button>
<Button
size="sm"
variant="danger"
isDisabled={busy}
onPress={() => void removeJob(j.id)}
>
{t("bizCompare.delete")}
</Button>
</div> </div>
</td> </td>
</tr> </tr>
@ -1409,6 +1433,11 @@ export function BizComparePage() {
<Button size="sm" variant="ghost" onPress={closeJob}> <Button size="sm" variant="ghost" onPress={closeJob}>
{t("bizState.cancel")} {t("bizState.cancel")}
</Button> </Button>
{jobId ? (
<Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeJob(jobId)}>
{t("bizCompare.delete")}
</Button>
) : null}
</Modal.Footer> </Modal.Footer>
</AppModalShell> </AppModalShell>
</section> </section>

View file

@ -397,13 +397,12 @@ export function BizStatePage() {
} }
}; };
const setStatus = async (status: string) => { const setScheduleEnabled = async (id: string, enabled: boolean) => {
if (!taskId) return;
setBusy(true); setBusy(true);
try { try {
await bizStatePatchTask(taskId, { status }); await bizStatePatchTask(id, { status: enabled ? "running" : "paused" });
showOk(status === "running" ? t("bizState.started") : t("bizState.paused")); showOk(enabled ? t("bizState.started") : t("bizState.paused"));
await loadTask(taskId); if (taskId === id) await loadTask(id);
await refreshTasks(); await refreshTasks();
} catch (e) { } catch (e) {
showError(formatErr(e)); showError(formatErr(e));
@ -593,7 +592,7 @@ export function BizStatePage() {
<div className="pt-list-kpi__value">{tasks.length}</div> <div className="pt-list-kpi__value">{tasks.length}</div>
</div> </div>
<div className="pt-list-kpi pt-list-kpi--live"> <div className="pt-list-kpi pt-list-kpi--live">
<div className="pt-list-kpi__label">{t("bizState.start")}</div> <div className="pt-list-kpi__label">{t("bizState.scheduleOn")}</div>
<div className="pt-list-kpi__value">{runningCount}</div> <div className="pt-list-kpi__value">{runningCount}</div>
</div> </div>
</div> </div>
@ -613,6 +612,7 @@ export function BizStatePage() {
<th>{t("bizState.colNe")}</th> <th>{t("bizState.colNe")}</th>
<th>{t("bizState.colSource")}</th> <th>{t("bizState.colSource")}</th>
<th>{t("bizState.colInterval")}</th> <th>{t("bizState.colInterval")}</th>
<th>{t("bizState.scheduleEnabled")}</th>
<th>{t("bizState.colStatus")}</th> <th>{t("bizState.colStatus")}</th>
<th>{t("bizState.colLast")}</th> <th>{t("bizState.colLast")}</th>
<th>{t("bizState.colActions")}</th> <th>{t("bizState.colActions")}</th>
@ -641,12 +641,28 @@ export function BizStatePage() {
t("bizState.intervalUnitSeconds"), t("bizState.intervalUnitSeconds"),
)} )}
</td> </td>
<td>
<label className="config-sync-policy-check">
<input
type="checkbox"
checked={row.status === "running"}
disabled={busy}
onChange={(e) => void setScheduleEnabled(row.id, e.target.checked)}
/>
<span>
{row.status === "running" ? t("bizState.scheduleOn") : t("bizState.scheduleOff")}
</span>
</label>
</td>
<td> <td>
<div className="pt-list-actions" style={{ flexWrap: "wrap", gap: 4 }}> <div className="pt-list-actions" style={{ flexWrap: "wrap", gap: 4 }}>
<NmStatusChip color={jobChipColor(row.status)}>{row.status}</NmStatusChip>
{row.collect_running ? ( {row.collect_running ? (
<NmStatusChip color="accent">{t("bizState.collecting")}</NmStatusChip> <NmStatusChip color="accent">{t("bizState.collecting")}</NmStatusChip>
) : null} ) : (
<NmStatusChip color={row.status === "running" ? "success" : "default"}>
{row.status === "running" ? t("bizState.scheduleOn") : t("bizState.scheduleOff")}
</NmStatusChip>
)}
</div> </div>
</td> </td>
<td className="pt-list-time">{fmtTime(row.last_collect_ended_at)}</td> <td className="pt-list-time">{fmtTime(row.last_collect_ended_at)}</td>
@ -658,47 +674,33 @@ export function BizStatePage() {
<Button size="sm" variant="ghost" onPress={() => void openTask(row.id, "batches")}> <Button size="sm" variant="ghost" onPress={() => void openTask(row.id, "batches")}>
{t("bizState.batches")} {t("bizState.batches")}
</Button> </Button>
{row.status !== "running" ? ( <Button
<Button size="sm"
size="sm" variant="secondary"
variant="ghost" isDisabled={busy || row.collect_running}
isDisabled={busy} onPress={async () => {
onPress={async () => { setBusy(true);
setBusy(true); try {
try { await bizStateCollectNow(row.id);
await bizStatePatchTask(row.id, { status: "running" }); showOk(t("bizState.collecting"));
showOk(t("bizState.started")); for (let i = 0; i < 20; i++) {
await new Promise((r) => setTimeout(r, 1500));
await refreshTasks(); await refreshTasks();
} catch (e) { const cur = (await bizStateListTasks()).items?.find(
showError(formatErr(e)); (x: any) => x.id === row.id,
} finally { );
setBusy(false); if (!cur?.collect_running) break;
} }
}} await refreshTasks();
> } catch (e) {
{t("bizState.start")} showError(formatErr(e));
</Button> } finally {
) : ( setBusy(false);
<Button }
size="sm" }}
variant="ghost" >
isDisabled={busy} {t("bizState.collectNow")}
onPress={async () => { </Button>
setBusy(true);
try {
await bizStatePatchTask(row.id, { status: "paused" });
showOk(t("bizState.paused"));
await refreshTasks();
} catch (e) {
showError(formatErr(e));
} finally {
setBusy(false);
}
}}
>
{t("bizState.pause")}
</Button>
)}
<Button <Button
size="sm" size="sm"
variant="danger" variant="danger"
@ -713,7 +715,7 @@ export function BizStatePage() {
))} ))}
{!filteredTasks.length ? ( {!filteredTasks.length ? (
<tr> <tr>
<td colSpan={6}> <td colSpan={7}>
<div className="pt-list-empty">{t("bizState.empty")}</div> <div className="pt-list-empty">{t("bizState.empty")}</div>
</td> </td>
</tr> </tr>
@ -846,6 +848,15 @@ export function BizStatePage() {
<Modal.Body className="flex flex-col gap-3"> <Modal.Body className="flex flex-col gap-3">
{detail ? ( {detail ? (
<div className="config-sync-policy-row bs-schedule-row"> <div className="config-sync-policy-row bs-schedule-row">
<label className="config-sync-policy-check">
<input
type="checkbox"
checked={detail.status === "running"}
disabled={busy}
onChange={(e) => void setScheduleEnabled(taskId, e.target.checked)}
/>
<span>{t("bizState.scheduleEnabled")}</span>
</label>
<label className="config-sync-policy-field"> <label className="config-sync-policy-field">
<span>{t("bizState.interval")}</span> <span>{t("bizState.interval")}</span>
<Input <Input
@ -892,7 +903,7 @@ export function BizStatePage() {
{t("bizState.saveSchedule")} {t("bizState.saveSchedule")}
</Button> </Button>
<span className="muted"> <span className="muted">
{detail.vendor || "—"} · {detail.ne_ip || "—"} {detail.vendor || "—"} · {detail.ne_ip || "—"} · {t("bizState.scheduleHint")}
</span> </span>
</div> </div>
) : null} ) : null}
@ -1075,13 +1086,7 @@ export function BizStatePage() {
)} )}
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void setStatus("running")}> <Button size="sm" variant="primary" isDisabled={busy} onPress={() => void collectNow()}>
{t("bizState.start")}
</Button>
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void setStatus("paused")}>
{t("bizState.pause")}
</Button>
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void collectNow()}>
{t("bizState.collectNow")} {t("bizState.collectNow")}
</Button> </Button>
<Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeTask(taskId)}> <Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeTask(taskId)}>

View file

@ -1895,6 +1895,9 @@ export const bizCompareUpdateJob = (jobId: string, body: Record<string, unknown>
body, body,
); );
export const bizCompareDeleteJob = (jobId: string) =>
apiDelete<{ ok: boolean }>(`/v1/biz-state/compare/jobs/${encodeURIComponent(jobId)}`);
export const bizCompareRunJob = (jobId: string) => export const bizCompareRunJob = (jobId: string) =>
apiPost<Record<string, unknown>>( apiPost<Record<string, unknown>>(
`/v1/biz-state/compare/jobs/${encodeURIComponent(jobId)}/run`, `/v1/biz-state/compare/jobs/${encodeURIComponent(jobId)}/run`,