{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "queues-1",
  "title": "Queues 1: job queues",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The queues behind a product: jobs waiting, arrival and finish rates, the age of the oldest job, a queue that is filling faster than it drains called out in words, failed jobs with their errors to retry or discard, and a pause switch.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/switch.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/queues-1/queues-1.tsx",
      "content": "// Ballmac UI: Queues 1. https://ui.ballmac.com/blocks/queues-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { CircleAlert, Loader2, Pause, Play, RotateCw, Trash2, TrendingUp } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { Switch } from \"@/components/ballmac/switch\"\nimport { cn } from \"@/lib/utils\"\n\ntype Queues1FailedJob = {\n  id: string\n  /** What the job was, for example \"Send invoice 4812\". */\n  name: string\n  /** The error it ended with. */\n  error: string\n  attempts: number\n  /** When it last failed, already formatted. */\n  at: string\n}\n\ntype Queues1Queue = {\n  id: string\n  name: string\n  /** Jobs waiting. */\n  depth: number\n  /** Jobs arriving per minute. */\n  inRate: number\n  /** Jobs finished per minute. */\n  outRate: number\n  /** How long the oldest waiting job has waited, already formatted, such as \"4 min\". */\n  oldest: string\n  paused?: boolean\n  /** Recent depth, oldest first, for the small chart. */\n  series: number[]\n  failed: Queues1FailedJob[]\n}\n\ntype Queues1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    queues?: Queues1Queue[]\n    title?: string\n    /** The queue whose failed jobs are open at first. */\n    defaultOpenId?: string\n    /** Called when a queue is paused or resumed. Reject and it goes back with the message. */\n    onPause?: (queueId: string, paused: boolean) => void | Promise<void>\n    /** Called to run one failed job again, or every failed job in the queue when jobId is \"all\". */\n    onRetryJobs?: (queueId: string, jobId: string | \"all\") => void | Promise<void>\n    /** Called to throw a failed job away. */\n    onDiscard?: (queueId: string, jobId: string) => void | Promise<void>\n  }\n\nconst sample: Queues1Queue[] = [\n  { id: \"email\", name: \"email\", depth: 1840, inRate: 420, outRate: 180, oldest: \"9 min\", series: [120, 180, 260, 410, 600, 880, 1100, 1350, 1580, 1840], failed: [{ id: \"j1\", name: \"Send invoice 4812\", error: \"Connection to the mail provider timed out\", attempts: 3, at: \"2 min ago\" }, { id: \"j2\", name: \"Send receipt 9921\", error: \"Recipient address was rejected: mailbox full\", attempts: 5, at: \"11 min ago\" }] },\n  { id: \"images\", name: \"images\", depth: 36, inRate: 40, outRate: 44, oldest: \"2 s\", series: [30, 44, 28, 51, 40, 33, 47, 39, 31, 36], failed: [] },\n  { id: \"export\", name: \"export\", depth: 4, inRate: 3, outRate: 3, oldest: \"12 s\", series: [2, 5, 3, 4, 6, 3, 4, 2, 5, 4], failed: [{ id: \"j3\", name: \"Export customers for Acme\", error: \"Out of memory after 120,000 rows\", attempts: 2, at: \"1 h ago\" }] },\n  { id: \"webhooks\", name: \"webhooks\", depth: 0, inRate: 18, outRate: 18, oldest: \"none\", paused: true, series: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], failed: [] },\n]\n\nfunction Spark({ values }: { values: number[] }) {\n  const max = Math.max(...values, 1)\n  const pts = values.map((v, i) => `${(i / Math.max(values.length - 1, 1)) * 100},${28 - (v / max) * 26}`).join(\" \")\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 100 30\" preserveAspectRatio=\"none\" className=\"h-7 w-24 overflow-visible\" style={{ direction: \"ltr\" }}>\n      <polyline points={pts} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" vectorEffect=\"non-scaling-stroke\" strokeLinejoin=\"round\" strokeLinecap=\"round\" />\n    </svg>\n  )\n}\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-3\">\n      {[0, 1, 2, 3].map((i) => (\n        <Skeleton key={i} className=\"h-16 rounded-xl\" />\n      ))}\n    </div>\n  )\n}\n\nconst num = (n: number) => n.toLocaleString(\"en-US\")\n\n/**\n * The queues behind a product: how many jobs wait in each, how fast they arrive and finish, how long the oldest has waited, and a queue that is filling up\n * faster than it drains called out in words. Open a queue to read its failed jobs and run them again, one or all, or throw one away. A queue can be paused.\n */\nfunction Queues1({ queues: queuesProp = sample, title = \"Job queues\", defaultOpenId, onPause, onRetryJobs, onDiscard, state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Queues1Props) {\n  const id = React.useId()\n  const [queues, setQueues] = React.useState<Queues1Queue[]>(queuesProp)\n  React.useEffect(() => setQueues(queuesProp), [queuesProp])\n  const [open, setOpen] = React.useState<string | null>(defaultOpenId ?? null)\n  const [busy, setBusy] = React.useState<string | null>(null)\n  const [note, setNote] = React.useState<{ ok: boolean; text: string } | null>(null)\n  const view = state === \"ready\" && queues.length === 0 ? \"empty\" : state\n  const opened = queues.find((q) => q.id === open)\n\n  async function run(key: string, work: () => void | Promise<void>, done: () => void, ok: string) {\n    setBusy(key)\n    setNote(null)\n    try {\n      await work()\n      done()\n      setNote({ ok: true, text: ok })\n    } catch (e) {\n      setNote({ ok: false, text: e instanceof Error ? e.message : \"That did not go through. Nothing was changed.\" })\n    } finally {\n      setBusy(null)\n    }\n  }\n  const patch = (qid: string, fn: (q: Queues1Queue) => Queues1Queue) => setQueues((l) => l.map((q) => (q.id === qid ? fn(q) : q)))\n  const backing = (q: Queues1Queue) => !q.paused && q.inRate > q.outRate * 1.2 && q.depth > 100\n\n  return (\n    <section data-slot=\"queues-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-5xl px-4 py-10 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-8\">\n        <h1 id={`${id}-title`} className=\"text-3xl font-semibold tracking-[-0.035em] text-balance\">\n          {title}\n        </h1>\n        <p className=\"text-muted-foreground mt-2 max-w-xl text-pretty\">What is waiting, how fast it moves, and what failed.</p>\n      </header>\n      <StateView state={view} onRetry={onRetry} label=\"Loading the queues\" loading={<Skel />} empty={{ title: \"No queues yet\", description: \"When your app starts a background queue, its depth and failures will show here.\", ...emptyState }} error={{ title: \"The queues could not be loaded\", description: \"No jobs were changed. Check your connection and try again.\", ...errorState }}>\n        <div className=\"relative overflow-x-auto rounded-xl border\">\n          <table className=\"w-full min-w-[42rem] border-collapse text-sm\">\n            <caption className=\"sr-only\">{title}</caption>\n            <thead className=\"bg-muted/40 text-muted-foreground text-xs\">\n              <tr>\n                <th scope=\"col\" className=\"px-4 py-2.5 text-start font-medium\">Queue</th>\n                <th scope=\"col\" className=\"px-3 py-2.5 text-end font-medium\">Waiting</th>\n                <th scope=\"col\" className=\"px-3 py-2.5 text-end font-medium\">In / out per min</th>\n                <th scope=\"col\" className=\"px-3 py-2.5 text-end font-medium\">Oldest</th>\n                <th scope=\"col\" className=\"px-3 py-2.5 text-end font-medium\">Failed</th>\n                <th scope=\"col\" className=\"px-4 py-2.5 text-end font-medium\"><span className=\"sr-only\">Paused</span></th>\n              </tr>\n            </thead>\n            <tbody className=\"divide-y\">\n              {queues.map((q) => {\n                const on = open === q.id\n                return (\n                  <tr key={q.id} data-queue={q.id} className={cn(on && \"bg-muted/30\")}>\n                    <th scope=\"row\" className=\"px-4 py-3 text-start font-normal\">\n                      <button type=\"button\" aria-expanded={on} aria-controls={`${id}-detail`} onClick={() => { setOpen(on ? null : q.id); setNote(null) }} className=\"focus-visible:ring-ring/50 -m-1 flex items-center gap-3 rounded-md p-1 text-start outline-none focus-visible:ring-[3px]\">\n                        <span>\n                          <span className=\"block font-mono text-sm font-medium\" dir=\"ltr\">{q.name}</span>\n                          {q.paused ? <span className=\"text-muted-foreground block text-xs\">Paused</span> : backing(q) ? (\n                            <span className=\"text-foreground flex items-center gap-1 text-xs font-medium\"><TrendingUp aria-hidden=\"true\" className=\"text-chart-3 size-3\" /> Backing up</span>\n                          ) : <span className=\"text-muted-foreground block text-xs\">Keeping up</span>}\n                        </span>\n                        <span className=\"text-muted-foreground hidden sm:block\"><Spark values={q.series} /></span>\n                      </button>\n                    </th>\n                    <td className=\"px-3 py-3 text-end font-medium tabular-nums\">{num(q.depth)}</td>\n                    <td className=\"text-muted-foreground px-3 py-3 text-end tabular-nums\">{num(q.inRate)} / {num(q.outRate)}</td>\n                    <td className=\"text-muted-foreground px-3 py-3 text-end tabular-nums\">{q.oldest}</td>\n                    <td className={cn(\"px-3 py-3 text-end tabular-nums\", q.failed.length ? \"text-destructive font-medium\" : \"text-muted-foreground\")}>{q.failed.length}</td>\n                    <td className=\"px-4 py-3 text-end\">\n                      <Switch aria-label={`Pause ${q.name}`} checked={!!q.paused} disabled={busy === q.id} onCheckedChange={(paused) => void run(q.id, () => onPause?.(q.id, paused), () => patch(q.id, (x) => ({ ...x, paused })), paused ? `${q.name} is paused. Jobs keep arriving and wait.` : `${q.name} is running again.`)} />\n                    </td>\n                  </tr>\n                )\n              })}\n            </tbody>\n          </table>\n        </div>\n\n        <div id={`${id}-detail`} aria-live=\"polite\" className=\"bg-card mt-4 rounded-xl border p-4 sm:p-5\">\n          {note && <p role={note.ok ? \"status\" : \"alert\"} className={cn(\"mb-3 text-sm font-medium\", !note.ok && \"text-destructive\")}>{note.text}</p>}\n          {opened ? (\n            opened.failed.length ? (\n              <>\n                <div className=\"flex flex-wrap items-center justify-between gap-3\">\n                  <h2 className=\"flex items-center gap-2 font-semibold tracking-tight\"><CircleAlert aria-hidden=\"true\" className=\"text-destructive size-4\" /> {opened.failed.length} failed in <span className=\"font-mono\" dir=\"ltr\">{opened.name}</span></h2>\n                  <Button size=\"sm\" disabled={busy !== null} onClick={() => void run(`${opened.id}:all`, () => onRetryJobs?.(opened.id, \"all\"), () => patch(opened.id, (q) => ({ ...q, depth: q.depth + q.failed.length, failed: [] })), `All ${opened.failed.length} failed jobs were put back in ${opened.name}.`)}>\n                    {busy === `${opened.id}:all` ? <Loader2 aria-hidden=\"true\" className=\"animate-spin\" /> : <RotateCw aria-hidden=\"true\" />} Retry all\n                  </Button>\n                </div>\n                <ul className=\"mt-3 divide-y rounded-lg border\">\n                  {opened.failed.map((j) => (\n                    <li key={j.id} className=\"flex flex-wrap items-start justify-between gap-3 px-3 py-3\">\n                      <div className=\"min-w-0\">\n                        <p className=\"text-sm font-medium\">{j.name}</p>\n                        <p className=\"text-destructive mt-0.5 text-xs\">{j.error}</p>\n                        <p className=\"text-muted-foreground mt-0.5 text-xs\">{j.attempts} attempts · failed {j.at}</p>\n                      </div>\n                      <div className=\"flex gap-2\">\n                        <Button size=\"sm\" variant=\"outline\" aria-label={`Retry ${j.name}`} disabled={busy !== null} onClick={() => void run(j.id, () => onRetryJobs?.(opened.id, j.id), () => patch(opened.id, (q) => ({ ...q, depth: q.depth + 1, failed: q.failed.filter((x) => x.id !== j.id) })), `${j.name} was put back in ${opened.name}.`)}>\n                          <RotateCw aria-hidden=\"true\" /> Retry\n                        </Button>\n                        <Button size=\"sm\" variant=\"ghost\" aria-label={`Discard ${j.name}`} disabled={busy !== null} onClick={() => void run(j.id, () => onDiscard?.(opened.id, j.id), () => patch(opened.id, (q) => ({ ...q, failed: q.failed.filter((x) => x.id !== j.id) })), `${j.name} was discarded.`)}>\n                          <Trash2 aria-hidden=\"true\" /> Discard\n                        </Button>\n                      </div>\n                    </li>\n                  ))}\n                </ul>\n              </>\n            ) : (\n              <p className=\"text-muted-foreground text-sm\"><span className=\"font-mono\" dir=\"ltr\">{opened.name}</span> has no failed jobs.</p>\n            )\n          ) : (\n            <p className=\"text-muted-foreground text-sm\">Select a queue to read its failed jobs. {queues.reduce((n, q) => n + q.failed.length, 0)} failed in all.</p>\n          )}\n        </div>\n      </StateView>\n    </section>\n  )\n}\n\nexport { Queues1, type Queues1Props, type Queues1Queue, type Queues1FailedJob }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/queues-1/queues-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "queues",
      "jobs",
      "background jobs",
      "retries",
      "dead letter",
      "workers",
      "devops",
      "monitoring"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "Background job queues. <Queues1 queues={[{ id, name, depth, inRate, outRate, oldest: '9 min', paused?, series: number[], failed: [{ id, name, error, attempts, at }] }]} defaultOpenId='email' onPause={(queueId, paused) => Promise} onRetryJobs={(queueId, jobId | 'all') => Promise} onDiscard={(queueId, jobId) => Promise} />. A queue is 'Backing up' when jobs arrive over 1.2 times faster than they finish and over 100 wait. Each action updates the screen and reports success or the error.",
      "whenToUse": [
        "An operations view of workers and background jobs",
        "Finding and retrying jobs that failed"
      ],
      "whenNotToUse": [
        "Scheduled jobs and their next runs; use cron-1",
        "A deploy's stages; use pipeline-1"
      ],
      "composesWith": [
        "cron-1",
        "slo-1",
        "incident-1",
        "webhooks-1"
      ],
      "a11y": [
        {
          "keys": "Table",
          "action": "A real table with a caption; each queue name is a button (aria-expanded) that opens its failed jobs"
        },
        {
          "keys": "Backing up",
          "action": "Written in words with an icon; the sparkline is decoration"
        },
        {
          "keys": "Pause",
          "action": "A switch named 'Pause email'; the result is a polite announcement and an error is an alert"
        },
        {
          "keys": "Retry and discard",
          "action": "Buttons named for the job ('Retry Send invoice 4812')"
        }
      ],
      "customization": [
        "queues and defaultOpenId",
        "onPause, onRetryJobs and onDiscard; reject and the message says why",
        "The backing-up rule is 1.2 times and 100 jobs"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "queues-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton rows under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No queues yet",
        "example": "queues-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Says no queues yet; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "queues-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying no jobs were changed, with Try again (onRetry). A failed pause or retry puts things back and says why."
      }
    ],
    "examples": [
      "queues-1-demo",
      "queues-1-loading",
      "queues-1-empty",
      "queues-1-error"
    ],
    "url": "https://ui.ballmac.com/components/queues-1"
  },
  "categories": [
    "blocks",
    "queues"
  ],
  "type": "registry:block"
}