Approval queue

Pro

Approval requests by what they need from the viewer. "Needs you" lists what they can decide, soonest to expire first; production requests are flagged.

Markdown

Preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/approval-queue

Brings in: agent-avatar, approval-request, badge, locale, relative-time, work, utils · npm: lucide-react@^1.67.0, radix-ui@^1.8.1

Usage

TSX · The example above
"use client"

import * as React from "react"
import type { AgentIdentity } from "@/lib/chat"
import type { ApprovalRequest } from "@/lib/work"
import { ApprovalQueue } from "@/components/approval-queue"

const ago = (m: number) => new Date(Date.now() - m * 60_000).toISOString()
const ahead = (h: number) => new Date(Date.now() + h * 3_600_000).toISOString()
const keel: AgentIdentity = { id: "a-orch", name: "Keel", role: "orchestrator" }

const requests: ApprovalRequest[] = [
  {
    id: "ap1",
    title: "Add region to customers",
    summary: "A region column on customers, backfilled from billing country.",
    requester: { agent: keel },
    environment: "Acme web",
    production: true,
    artifactHash: "9f2c4e81b7a03d55c6e1f0a9b8d7c6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9",
    impact: { components: [{ kind: "migration", name: "0142_add_customer_region" }], rows: "unknown" },
    rollback: { available: true, how: "Run the migration's down step." },
    createdAt: ago(25),
    expiresAt: ahead(7 * 24),
    state: "pending",
  },
  {
    id: "ap2",
    title: "Turn on plan-change-review",
    summary: "Only billing admins can change a customer's plan while an invoice is open.",
    requester: { agent: keel },
    environment: "Acme web",
    detail: "Staging",
    production: false,
    artifactHash: "77aa1c0e5d4b3a29180f7e6d5c4b3a2918f7e6d5c4b3a2918f7e6d5c4b3a2918",
    impact: { components: [{ kind: "flag", name: "plan-change-review" }], rows: 0 },
    rollback: { available: true, how: "Turn the flag off." },
    createdAt: ago(60 * 3),
    expiresAt: ahead(10),
    state: "pending",
  },
  {
    id: "ap3",
    title: "Drop partner_tier_old",
    summary: "A migration drops customers.partner_tier_old.",
    requester: { name: "You" },
    environment: "Acme web",
    production: true,
    artifactHash: "3b1d9c8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c",
    impact: { components: [{ kind: "column", name: "customers.partner_tier_old" }], rows: "unknown" },
    rollback: { available: false, reason: "a dropped column's data only comes back from a backup restore" },
    createdAt: ago(60 * 20),
    expiresAt: ahead(30),
    state: "pending",
    cannotDecide: "You asked for this change, so another approver must decide it.",
  },
  {
    id: "ap4",
    title: "Merge signup source values",
    summary: "Merge “web” and “website” into one value.",
    requester: { agent: keel },
    environment: "Acme web",
    production: true,
    artifactHash: "c0de4f3e2d1c0b9a8f7e6d5c4b3a29180f7e6d5c4b3a29180f7e6d5c4b3a2918",
    impact: { components: [{ kind: "column", name: "customers.signup_source" }], rows: 4210 },
    rollback: { available: false, reason: "merged values can't be split back" },
    createdAt: ago(60 * 26),
    expiresAt: ahead(6 * 24),
    state: "approved",
    decision: { by: "Priya", at: ago(60 * 3) },
  },
]

export default function Example() {
  const [active, setActive] = React.useState("ap1")
  return <ApprovalQueue className="max-w-md" requests={requests} activeId={active} onOpen={setActive} />
}

<ApprovalQueue>

PropTypeDefaultDescription
requestsrequiredApprovalRequest[]—Every request; the tabs sort them into needs you, waiting on others and decided.
onOpen((id: string) => void) | undefined—Called with a request's id when its row is clicked.
activeIdstring | undefined—The id of the request open beside the queue, marked as current.
classNamestring | undefined—Extra classes for the outer element.

Strings

The fixed words, in English by default. Change any of them under approvalQueue in <LocaleProvider strings={{ … }}>; the table is exported as approvalQueueStrings.

KeyEnglish
labelApproval requests
tabsRequests
mineNeeds you
othersWaiting on others
decidedDecided
emptyMineNothing waiting on you.
emptyOthersNothing waiting on others.
emptyDecidedNo decisions yet.
productionProduction
expiresexpires