Add LLDP pause/resume/stop with restart recovery, and localize ops task titles.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-04 22:23:44 +08:00
parent ba038a3af6
commit 8ded8eeb7d
14 changed files with 923 additions and 136 deletions

View file

@ -2,7 +2,7 @@ import { useMemo } from "react";
import { Link } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { useI18n } from "../../i18n";
import { fetchOpsTasks } from "../../services/api";
import { fetchOpsTasks, type OpsTaskItem } from "../../services/api";
import { formatSystemTime } from "../../utils/time";
const POLL_MS = 4000;
@ -16,18 +16,47 @@ function statusTone(status: string): string {
return "other";
}
function kindLabel(kind: string, t: (k: string) => string): string {
function kindLabel(kind: string, t: (k: string, vars?: Record<string, string | number>) => string): string {
const key = `audit.tasks.kind.${kind}`;
const tr = t(key);
return tr === key ? kind : tr;
}
function statusLabel(status: string, t: (k: string) => string): string {
function statusLabel(status: string, t: (k: string, vars?: Record<string, string | number>) => string): string {
const key = `audit.tasks.status.${status}`;
const tr = t(key);
return tr === key ? status : tr;
}
function triggerLabel(trigger: string, t: (k: string, vars?: Record<string, string | number>) => string): string {
const raw = String(trigger || "").trim();
if (!raw || raw === "—") return "—";
const key = `audit.tasks.trigger.${raw}`;
const tr = t(key);
return tr === key ? raw : tr;
}
function taskTitle(row: OpsTaskItem, t: (k: string, vars?: Record<string, string | number>) => string): string {
const kind = kindLabel(row.kind, t);
const subject = String(row.title || "").trim();
if (!subject) return kind;
// Avoid "Kind · Kind · x" if an old backend still prefixed the localized kind.
if (subject === kind || subject.startsWith(`${kind} · `) || subject.startsWith(`${kind}·`)) {
return subject;
}
return `${kind} · ${subject}`;
}
function progressLabel(row: OpsTaskItem, t: (k: string, vars?: Record<string, string | number>) => string): string {
const base = String(row.progress || "").trim();
const inflight = Number(row.inflight || 0);
if (inflight > 0) {
const extra = t("audit.tasks.inflight", { n: inflight });
return base ? `${base} · ${extra}` : extra;
}
return base || "—";
}
export function TaskOverviewPage() {
const { t } = useI18n();
const query = useQuery({
@ -111,7 +140,7 @@ export function TaskOverviewPage() {
<tr key={`${row.kind}:${row.id}`} title={row.detail || undefined}>
<td>{kindLabel(row.kind, t)}</td>
<td>
<div className="pt-list-task-name">{row.title}</div>
<div className="pt-list-task-name">{taskTitle(row, t)}</div>
{row.detail ? (
<div className="muted" style={{ fontSize: 12 }}>
{row.detail}
@ -124,8 +153,8 @@ export function TaskOverviewPage() {
</span>
</td>
<td>{row.actor || "—"}</td>
<td>{row.trigger || "—"}</td>
<td className="pt-list-num">{row.progress || "—"}</td>
<td>{triggerLabel(row.trigger, t)}</td>
<td className="pt-list-num">{progressLabel(row, t)}</td>
<td className="pt-list-time">{formatSystemTime(row.started_at) || "—"}</td>
<td className="pt-list-time">{formatSystemTime(row.updated_at) || "—"}</td>
<td>

View file

@ -6,7 +6,10 @@ import {
fetchLldpCollectDashboard,
fetchLldpCollectJob,
fetchLldpCollectJobs,
pauseLldpCollectJob,
resumeLldpCollectJob,
startLldpCollect,
stopLldpCollectJob,
updateLldpCollectPolicy,
} from "../../services/api";
import { queryKeys } from "../../constants/queryKeys";
@ -55,7 +58,10 @@ export function LldpLinksPage() {
staleTime: 1000,
refetchInterval: (q) => {
const running = q.state.data?.running_job;
return running && (running.status === "running" || running.status === "pending") ? POLL_MS : false;
return running &&
(running.status === "running" || running.status === "pending" || running.status === "paused")
? POLL_MS
: false;
},
});
@ -187,6 +193,33 @@ export function LldpLinksPage() {
onError: (err) => showError(String(err)),
});
const pauseMut = useMutation({
mutationFn: (id: string) => pauseLldpCollectJob(id),
onSuccess: async () => {
showOk(t("lldpLinks.paused"));
await refresh();
},
onError: (err) => showError(String(err)),
});
const resumeMut = useMutation({
mutationFn: (id: string) => resumeLldpCollectJob(id),
onSuccess: async () => {
showOk(t("lldpLinks.resumed"));
await refresh();
},
onError: (err) => showError(String(err)),
});
const stopMut = useMutation({
mutationFn: (id: string) => stopLldpCollectJob(id),
onSuccess: async () => {
showOk(t("lldpLinks.stopped"));
await refresh();
},
onError: (err) => showError(String(err)),
});
const dash = dashQuery.data;
const running = dash?.running_job;
const last = dash?.last_job;
@ -228,6 +261,28 @@ export function LldpLinksPage() {
>
{t("lldpLinks.collectNow")}
</button>
{running?.status === "running" || running?.status === "pending" ? (
<button type="button" onClick={() => pauseMut.mutate(running.id)} disabled={pauseMut.isPending}>
{t("lldpLinks.pause")}
</button>
) : null}
{running?.status === "paused" ? (
<button type="button" onClick={() => resumeMut.mutate(running.id)} disabled={resumeMut.isPending}>
{t("lldpLinks.resume")}
</button>
) : null}
{running &&
(running.status === "running" || running.status === "paused" || running.status === "pending") ? (
<button
type="button"
onClick={() => {
if (window.confirm(t("lldpLinks.confirmStop"))) stopMut.mutate(running.id);
}}
disabled={stopMut.isPending}
>
{t("lldpLinks.stop")}
</button>
) : null}
</div>
</div>
@ -536,6 +591,7 @@ export function LldpLinksPage() {
<th>{t("lldpLinks.col.missingDelta")}</th>
<th>{t("lldpLinks.col.started")}</th>
<th>{t("lldpLinks.col.ended")}</th>
<th />
</tr>
</thead>
<tbody>
@ -559,7 +615,19 @@ export function LldpLinksPage() {
<td className="mono">{job.id.slice(0, 8)}</td>
<td>{job.trigger_mode}</td>
<td>{job.scope}</td>
<td>{job.status}</td>
<td>
<span
className={
job.status === "running" || job.status === "pending"
? "pt-list-status--running"
: job.status === "paused"
? "pt-list-status--paused"
: undefined
}
>
{job.status}
</span>
</td>
<td>
{job.done}/{job.total}
</td>
@ -569,12 +637,45 @@ export function LldpLinksPage() {
<td>{job.edges_missing ?? job.edges_stale ?? 0}</td>
<td>{job.started_at ? formatSystemTime(job.started_at) : "—"}</td>
<td>{job.ended_at ? formatSystemTime(job.ended_at) : "—"}</td>
<td>
<div className="btn-row">
{job.status === "running" || job.status === "pending" ? (
<button
type="button"
onClick={() => pauseMut.mutate(job.id)}
disabled={pauseMut.isPending}
>
{t("lldpLinks.pause")}
</button>
) : null}
{job.status === "paused" ? (
<button
type="button"
onClick={() => resumeMut.mutate(job.id)}
disabled={resumeMut.isPending}
>
{t("lldpLinks.resume")}
</button>
) : null}
{job.status === "running" || job.status === "paused" || job.status === "pending" ? (
<button
type="button"
onClick={() => {
if (window.confirm(t("lldpLinks.confirmStop"))) stopMut.mutate(job.id);
}}
disabled={stopMut.isPending}
>
{t("lldpLinks.stop")}
</button>
) : null}
</div>
</td>
</tr>
);
})}
{!jobs.length ? (
<tr>
<td colSpan={10} className="muted">
<td colSpan={11} className="muted">
{t("common.empty")}
</td>
</tr>