Files
Cibello-app/apps/admin/src/pages/Workers.tsx
T

205 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { api } from "../api.js";
interface JobCounts {
wait: number;
active: number;
delayed: number;
completed: number;
failed: number;
paused?: number;
}
interface WorkerInfo {
id: string;
addr: string;
concurrency: number;
lastSeenAt: number;
}
interface SchedulerInfo {
id: string;
pattern?: string;
every?: number;
}
interface HealthResponse {
ok: boolean;
queues: {
jobs: JobCounts;
deadLetter: JobCounts;
};
workers: WorkerInfo[];
schedulers: SchedulerInfo[];
}
interface DlqJob {
id: string;
name: string;
data: unknown;
failedReason?: string;
attemptsMade: number;
timestamp: number;
}
export function WorkersPage() {
const [health, setHealth] = useState<HealthResponse | null>(null);
const [dlq, setDlq] = useState<DlqJob[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [actionId, setActionId] = useState<string | null>(null);
const load = async () => {
setLoading(true);
try {
const [h, d] = await Promise.all([
api<HealthResponse>("/admin/v1/workers/health"),
api<{ jobs: DlqJob[] }>("/admin/v1/workers/dead-letter?limit=50"),
]);
setHealth(h);
setDlq(d.jobs);
setError(null);
} catch (e) {
setError(String((e as Error).message));
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
const t = setInterval(load, 10_000);
return () => clearInterval(t);
}, []);
const retry = async (id: string) => {
setActionId(id);
try {
await api(`/admin/v1/workers/dead-letter/${id}/retry`, { method: "POST" });
await load();
} catch (e) {
setError(String((e as Error).message));
} finally {
setActionId(null);
}
};
const discard = async (id: string) => {
if (!confirm("Ta bort jobbet permanent?")) return;
setActionId(id);
try {
await api(`/admin/v1/workers/dead-letter/${id}/discard`, { method: "POST" });
await load();
} catch (e) {
setError(String((e as Error).message));
} finally {
setActionId(null);
}
};
return (
<div className="page">
<h2>Workers & köer</h2>
{error && <div className="alert err">{error}</div>}
{loading && !health && <div>Laddar </div>}
{health && (
<>
<section className="card">
<h3>Jobbkö</h3>
<div className="metrics">
<Metric label="Väntar" value={health.queues.jobs.wait} />
<Metric label="Aktiva" value={health.queues.jobs.active} />
<Metric label="Fördröjda" value={health.queues.jobs.delayed} />
<Metric label="Misslyckade" value={health.queues.jobs.failed} />
<Metric label="Klara" value={health.queues.jobs.completed} />
</div>
</section>
<section className="card">
<h3>Dead letter ({health.queues.deadLetter.wait + health.queues.deadLetter.failed})</h3>
{dlq.length === 0 ? (
<p>Inga jobb i dead letter.</p>
) : (
<table>
<thead>
<tr>
<th>Jobb</th>
<th>Fel</th>
<th>Försök</th>
<th>Tid</th>
<th>Åtgärder</th>
</tr>
</thead>
<tbody>
{dlq.map((job) => (
<tr key={job.id}>
<td>{job.name}</td>
<td className="ellipsis" title={job.failedReason}>
{job.failedReason ?? ""}
</td>
<td>{job.attemptsMade}</td>
<td>{new Date(job.timestamp).toLocaleString("sv-SE")}</td>
<td>
<button
onClick={() => retry(job.id)}
disabled={actionId === job.id}
className="btn small"
>
Kör igen
</button>{" "}
<button
onClick={() => discard(job.id)}
disabled={actionId === job.id}
className="btn small danger"
>
Kasta
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</section>
<section className="card">
<h3>Aktiva worker-instanser</h3>
{health.workers.length === 0 ? (
<p className="err">Inga aktiva workers!</p>
) : (
<ul>
{health.workers.map((w) => (
<li key={w.id}>
{w.id} @ {w.addr} (concurrency {w.concurrency})
</li>
))}
</ul>
)}
</section>
<section className="card">
<h3>Scheduler</h3>
<ul>
{health.schedulers.map((s) => (
<li key={s.id}>
{s.id}: {s.pattern ?? `every ${s.every}ms`}
</li>
))}
</ul>
</section>
</>
)}
</div>
);
}
function Metric({ label, value }: { label: string; value: number }) {
return (
<div className="metric">
<div className="value">{value}</div>
<div className="label">{label}</div>
</div>
);
}