Warren UI

ApprovalGate

Human-approval checkpoint card with risk tier, exact argument payload, and approve/reject/edit actions.

APPROVAL REQUIREDGATE deploy:prod · STEP 3criticalpending
Actionkubectl.rollout
env
prod
service
billing-api
replicas
4
flags
{"canary":true,"drain":30}
ReasonWrites to the production cluster outside the deploy window.
REQUESTED 14:02:11EXPIRES 14:12:11
Source
"use client"

import { ApprovalGate } from "@/registry/warren/approval-gate/approval-gate"
import type { ApprovalRequest } from "@/registry/warren/approval-gate/approval-gate"

const request: ApprovalRequest = {
  id: "req-3",
  gate: "deploy:prod",
  step: 3,
  action: "kubectl.rollout",
  args: {
    env: "prod",
    service: "billing-api",
    replicas: 4,
    flags: { canary: true, drain: 30 },
  },
  risk: "critical",
  reason: "Writes to the production cluster outside the deploy window.",
  requestedAt: "14:02:11",
  expiresAt: "14:12:11",
}

export default function ApprovalGateDemo() {
  return (
    <div className="w-full max-w-xl">
      <ApprovalGate
        request={request}
        state="pending"
        onDecision={(decision) => console.log("decision:", decision)}
      />
    </div>
  )
}

Installation

npx shadcn@latest add https://warren.eduard3v.com/r/approval-gate.json

Usage

import { ApprovalGate } from "@/components/ui/approval-gate"
<ApprovalGate request={request} state="pending" onDecision={decide} />

Structure

The supervision primitive for human-in-the-loop pipelines. A paused checkpoint renders as a bordered card tinted by its state — warning while pending, success once approved, danger when rejected, muted after expiry. The header carries the state phrase (APPROVAL REQUIRED), gate coordinates (GATE deploy:prod · STEP 3), and two chips: the risk tier and the lifecycle state. critical is the only tier rendered as a solid saturated chip; every other tier stays outlined with text-grade ink.

The body is a mono key-value table: the proposed action in bold, every argument from request.args rendered at its exact payload shape (strings verbatim, everything else through JSON.stringify — never a lossy [object Object]), then the escalation reason. An audit strip shows REQUESTED/EXPIRES timestamps.

While pending and wired to onDecision, the footer offers APPROVE / EDIT / REJECT. Any decided state hides the action row and reports itself in the header.

Compact variant

APPROVAL REQUIREDGATE db:migrate · STEP 5highpending
Actionpostgres.migrate
migration
0042_add_ledger_index
env
staging
ReasonSchema rewrite requires operator sign-off.
REQUESTED 09:14:03
APPROVEDGATE db:migrate · STEP 5mediumapproved
Actionpostgres.migrate
migration
0042_add_ledger_index
env
staging
ReasonSchema rewrite requires operator sign-off.
REQUESTED 09:14:03
REJECTEDGATE db:migrate · STEP 5mediumrejected
Actionpostgres.migrate
migration
0042_add_ledger_index
env
staging
ReasonSchema rewrite requires operator sign-off.
REQUESTED 09:14:03
EXPIREDGATE db:migrate · STEP 5mediumexpired
Actionpostgres.migrate
migration
0042_add_ledger_index
env
staging
ReasonSchema rewrite requires operator sign-off.
REQUESTED 09:14:03EXPIRES 09:24:03
APPROVAL REQUIREDGATE db:migrate · STEP 5medium
APPROVEDGATE db:migrate · STEP 5mediumapproved
Source
"use client"

import { ApprovalGate } from "@/registry/warren/approval-gate/approval-gate"
import type { ApprovalRequest } from "@/registry/warren/approval-gate/approval-gate"

const base: ApprovalRequest = {
  id: "req-7",
  gate: "db:migrate",
  step: 5,
  action: "postgres.migrate",
  args: { migration: "0042_add_ledger_index", env: "staging" },
  risk: "medium",
  reason: "Schema rewrite requires operator sign-off.",
  requestedAt: "09:14:03",
}

export default function ApprovalGateStates() {
  return (
    <div className="flex w-full max-w-xl flex-col gap-3">
      <ApprovalGate
        request={{ ...base, risk: "high" }}
        state="pending"
        onDecision={() => {}}
      />
      <ApprovalGate request={base} state="approved" />
      <ApprovalGate request={base} state="rejected" />
      <ApprovalGate
        request={{ ...base, expiresAt: "09:24:03" }}
        state="expired"
      />
      <ApprovalGate request={base} state="pending" compact onDecision={() => {}} />
      <ApprovalGate request={base} state="approved" compact />
    </div>
  )
}

Pass compact for the inline-in-stream record: one hairline row with the status dot, gate coordinates, risk chip, and — while pending — bare APPROVE/REJECT buttons. Decided states swap the buttons for the state chip.

API Reference

Prop

Type

Prop

Type

Prop

Type

Prop

Type

On this page