import { useMutation, useQuery, useQueryClient, keepPreviousData } from "@tanstack/react-query"; import { useNavigate } from "react-router"; import { RotateCcw, Star, StarOff } from "lucide-react"; import { api } from "../lib/api"; import { useMetric, useRangeStep } from "../lib/queries"; import { useRefetch } from "../lib/settings"; import type { QueueData, QueueRow } from "../lib/types"; import { bytes, compact, duration, int, num, relative, seconds } from "../lib/format"; import { Badge, Button, Card, ErrorBox, Loading, PageHeader, StatTile, Table, Td, Th, useFeedback } from "../components/ui"; import { fromQuery, TimeChart } from "../components/charts/TimeChart"; export default function Queue() { const refetchInterval = useRefetch(); const { step } = useRangeStep(); const qc = useQueryClient(); const { toast, confirm } = useFeedback(); const navigate = useNavigate(); const q = useQuery({ queryKey: ["queue"], queryFn: () => api("/queue"), refetchInterval: refetchInterval || 15000, placeholderData: keepPreviousData }); const depth = useMetric({ name: "queue_depth", agg: "last", groupBy: ["kind"], step }); const claims = useMetric({ name: "indexer_claims_total", agg: "sum", groupBy: ["queue"], step }); const jobDuration = useMetric({ name: "indexer_job_duration_seconds", agg: "p50", step }); const resetStuck = useMutation({ mutationFn: () => api<{ reset: number }>("/queue/reset-stuck", { method: "POST", body: {} }), onSuccess: (r) => { toast(`Released ${r.reset} timed-out claim${r.reset === 1 ? "" : "s"}`); qc.invalidateQueries({ queryKey: ["queue"] }); }, onError: (e) => toast(String((e as Error).message), "critical"), }); const action = useMutation({ mutationFn: ({ id, act, value }: { id: string; act: string; value?: boolean }) => api(`/addons/${encodeURIComponent(id)}/${act}`, { method: "POST", body: { value: value ?? true } }), onSuccess: () => { toast("Updated"); qc.invalidateQueries({ queryKey: ["queue"] }); }, onError: (e) => toast(String((e as Error).message), "critical"), }); if (q.isLoading) return ; if (q.error) return ; const d = q.data!; const stuck = d.indexing.filter((r) => r.stuck).length; return (
Jobs are claimed priority-first, then by oldest update · claims time out after {d.timeoutMinutes} min · updated {relative(d.asOf)}} actions={ } />
(l.kind === "priority" ? "Priority" : "All queued"), 1, (l) => (l.kind === "priority" ? 2 : 1))} /> l.queue)} /> "p50")} format={seconds} />
( <> {duration(r.elapsedSec)} {r.stuck ? past timeout : running} )} extraHead={ <> Elapsed State } actions={(r) => ( )} onOpen={(id) => navigate(`/addons/${id}`)} /> ( )} onOpen={(id) => navigate(`/addons/${id}`)} /> r.priorityIndexing ? null : ( ) } onOpen={(id) => navigate(`/addons/${id}`)} />
); } function QueueTable({ title, subtitle, rows, empty, extra, extraHead, actions, onOpen, }: { title: string; subtitle?: string; rows: QueueRow[]; empty: string; extra?: (r: QueueRow) => React.ReactNode; extraHead?: React.ReactNode; actions?: (r: QueueRow) => React.ReactNode; onOpen: (id: string) => void; }) { return ( {rows.length === 0 ? (
{empty}
) : ( {extraHead} {rows.map((r) => ( onOpen(r.id)}> {extra?.(r)} ))}
Addon Size Subscribers Waiting since
{r.name || r.id} {r.priorityIndexing && priority} {r.blocked && blocked}
{r.id}
{bytes(r.currentVersionSize)} {compact(r.subscriberCount)} {relative(r.updatedAt)} e.stopPropagation()}>{actions?.(r)}
)}
); }