import { useState } from "react";
import {
useQuery,
useMutation,
useQueryClient,
} from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import {
fetchQueues,
pauseQueue,
resumeQueue,
drainQueue,
} from "@/lib/api";
import { toast } from "@/components/ui/toast";
import type { QueueStats } from "@/lib/api";
import { Heading } from "@/components/ui/heading";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableHeader,
TableBody,
TableRow,
TableCell,
TableColumn,
} from "@/components/ui/table";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { LagValue } from "@/components/LagValue";
export function QueuesPage() {
const queryClient = useQueryClient();
const [drainTarget, setDrainTarget] = useState<string | null>(null);
const queuesQuery = useQuery<QueueStats[]>({
queryKey: ["queues"],
queryFn: fetchQueues,
});
const pauseMutation = useMutation({
mutationFn: (queue: string) => pauseQueue(queue, "ui"),
onSuccess: (_data, queue) => {
void queryClient.invalidateQueries({ queryKey: ["queues"] });
toast.success(`Queue "${queue}" paused`);
},
onError: () => {
toast.error("Failed to pause queue");
},
});
const resumeMutation = useMutation({
mutationFn: (queue: string) => resumeQueue(queue),
onSuccess: (_data, queue) => {
void queryClient.invalidateQueries({ queryKey: ["queues"] });
toast.success(`Queue "${queue}" resumed`);
},
onError: () => {
toast.error("Failed to resume queue");
},
});
const drainMutation = useMutation({
mutationFn: (queue: string) => drainQueue(queue),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["queues"] });
void queryClient.invalidateQueries({ queryKey: ["jobs"] });
toast.success(`Queue drained`);
},
onError: () => {
toast.error("Failed to drain queue");
},
});
const queues = queuesQuery.data ?? [];
return (
<div className="space-y-4">
<Heading level={2}>Queues</Heading>
{/* Mobile card layout */}
{queues.length > 0 && (
<div className="space-y-3 sm:hidden">
{queues.map((q) => (
<div key={q.queue} className="rounded-lg border p-4">
<div className="flex items-center justify-between">
<Link
to="/queues/$name"
params={{ name: q.queue }}
className="font-medium text-primary no-underline hover:underline"
>
{q.queue}
</Link>
{q.paused ? (
<Badge intent="warning">Paused</Badge>
) : (
<Badge intent="success">Active</Badge>
)}
</div>
<div className="mt-2 grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
<span className="text-muted-fg">Available</span>
<span>{q.available}</span>
<span className="text-muted-fg">Running</span>
<span>{q.running}</span>
<span className="text-muted-fg">Failed</span>
<span className={q.failed > 0 ? "text-danger" : ""}>{q.failed}</span>
{q.waiting_external > 0 && (
<>
<span className="text-muted-fg">Waiting</span>
<span>{q.waiting_external}</span>
</>
)}
<span className="text-muted-fg">Lag</span>
<span><LagValue seconds={q.lag_seconds} /></span>
</div>
<div className="mt-3 flex gap-2">
{q.paused ? (
<Button intent="outline" size="xs" onPress={() => resumeMutation.mutate(q.queue)}>
Resume
</Button>
) : (
<Button intent="outline" size="xs" onPress={() => pauseMutation.mutate(q.queue)}>
Pause
</Button>
)}
<Button intent="outline" size="xs" className="text-danger" onPress={() => setDrainTarget(q.queue)}>
Drain
</Button>
</div>
</div>
))}
</div>
)}
{/* Desktop table layout */}
{queues.length > 0 ? (
<Table aria-label="Queues" className="hidden sm:table">
<TableHeader>
<TableColumn isRowHeader>Queue</TableColumn>
<TableColumn>Available</TableColumn>
<TableColumn>Running</TableColumn>
<TableColumn>Failed</TableColumn>
<TableColumn>Waiting</TableColumn>
<TableColumn>Completed/hr</TableColumn>
<TableColumn>Lag (s)</TableColumn>
<TableColumn>Status</TableColumn>
<TableColumn>Actions</TableColumn>
</TableHeader>
<TableBody>
{queues.map((q) => (
<TableRow key={q.queue} id={q.queue}>
<TableCell className="font-medium">
<Link
to="/queues/$name"
params={{ name: q.queue }}
className="text-primary no-underline hover:underline"
>
{q.queue}
</Link>
</TableCell>
<TableCell>{q.available}</TableCell>
<TableCell>{q.running}</TableCell>
<TableCell>
<span className={q.failed > 0 ? "text-danger" : ""}>
{q.failed}
</span>
</TableCell>
<TableCell>
{q.waiting_external > 0 ? q.waiting_external : "-"}
</TableCell>
<TableCell>{q.completed_last_hour}</TableCell>
<TableCell>
<LagValue seconds={q.lag_seconds} />
</TableCell>
<TableCell>
{q.paused ? (
<Badge intent="warning">Paused</Badge>
) : (
<Badge intent="success">Active</Badge>
)}
</TableCell>
<TableCell>
<div className="flex gap-1">
{q.paused ? (
<Button
intent="outline"
size="xs"
onPress={() => resumeMutation.mutate(q.queue)}
>
Resume
</Button>
) : (
<Button
intent="outline"
size="xs"
onPress={() => pauseMutation.mutate(q.queue)}
>
Pause
</Button>
)}
<Button
intent="outline"
size="xs"
className="text-danger"
onPress={() => setDrainTarget(q.queue)}
>
Drain
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : queuesQuery.isLoading ? (
<p className="text-sm text-muted-fg">Loading...</p>
) : (
<p className="text-sm text-muted-fg">No queues found.</p>
)}
<ConfirmDialog
isOpen={drainTarget !== null}
onOpenChange={(open) => {
if (!open) setDrainTarget(null);
}}
title={`Drain queue "${drainTarget}"`}
description="This will cancel all available, scheduled, retryable, and waiting_external jobs in this queue. Running jobs will not be affected."
confirmLabel="Drain queue"
confirmIntent="danger"
onConfirm={() => {
if (drainTarget) drainMutation.mutate(drainTarget);
}}
isPending={drainMutation.isPending}
/>
</div>
);
}