Files
atc-agents/ui/src/components/features/ApprovalCards.tsx
T
mo a11621b21f Add Command Center v2: DQ/RAG integration, S3 browser, Jupyter, GPU matrix.
Mirror mo/atc-GPU layout with config/, docs/, scripts/ for Gitea deploy.
2026-06-25 00:28:23 +00:00

99 lines
4.2 KiB
TypeScript

import { useState } from 'react'
import { Check, ShieldAlert, X } from 'lucide-react'
import type { Agent, Approval } from '../../types'
import { getAgentMeta } from '../../lib/agentMeta'
import { Button } from '../ui/Button'
import { cn } from '../../lib/utils'
const ACTION_LABELS: Record<string, string> = {
'docker.restart': 'Container restart',
'docker.update': 'Image update',
'generic.mutate': 'Infrastructure change',
}
type Props = {
approvals: Approval[]
agents: Agent[]
highlighted: boolean
onDecide: (id: string, approved: boolean, decidedBy: string, note: string) => Promise<void>
onDismissHighlight?: () => void
}
export function ApprovalCards({ approvals, agents, highlighted, onDecide, onDismissHighlight }: Props) {
const [busyId, setBusyId] = useState<string | null>(null)
const [decider, setDecider] = useState<'mo-commander' | 'bart-commander'>('mo-commander')
if (!approvals.length) return null
const agentOf = (id: string) => agents.find((a) => a.id === id)
const handle = async (id: string, approved: boolean) => {
setBusyId(id)
try {
await onDecide(id, approved, decider, '')
} finally {
setBusyId(null)
}
}
return (
<div className={cn('mx-2 mb-2 rounded-lg border bg-surface-overlay p-2.5', highlighted ? 'border-warning/50 ring-1 ring-warning/20' : 'border-border')}>
<div className="mb-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5 text-[11px] font-semibold text-warning">
<ShieldAlert className="h-3.5 w-3.5" />
Pending approvals ({approvals.length})
</div>
<div className="flex items-center gap-2">
<label className="flex items-center gap-1 text-[9px] text-foreground-muted">
As
<select
value={decider}
onChange={(e) => setDecider(e.target.value as typeof decider)}
className="rounded border border-border bg-surface px-1 py-0.5 text-[10px] text-foreground-muted"
>
<option value="mo-commander">Mo</option>
<option value="bart-commander">Bart</option>
</select>
</label>
{highlighted && onDismissHighlight && (
<button type="button" onClick={onDismissHighlight} className="text-[9px] text-foreground-muted hover:text-foreground-muted">Dismiss</button>
)}
</div>
</div>
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{approvals.map((a) => {
const ag = agentOf(a.agent_id)
const meta = ag ? getAgentMeta(ag.id) : null
const Icon = meta?.icon
return (
<div key={a.id} className="rounded-lg border border-border bg-surface p-2">
<div className="mb-1.5 flex items-start gap-1.5">
{Icon && (
<span className="flex h-6 w-6 items-center justify-center rounded bg-surface-overlay text-docker">
<Icon className="h-3.5 w-3.5" />
</span>
)}
<div className="min-w-0 flex-1">
<span className="text-[8px] uppercase tracking-wide text-warning">{ACTION_LABELS[a.action_type] || a.action_type}</span>
<p className="truncate text-[11px] font-medium text-foreground">{ag?.name || a.agent_id}</p>
</div>
<span className="font-mono text-[8px] text-foreground-faint">#{a.id.slice(0, 6)}</span>
</div>
<p className="mb-1 text-[10px] text-foreground-muted line-clamp-2">{a.action}</p>
{a.target && <p className="text-[9px] text-foreground-muted">Target: {a.target}</p>}
<div className="mt-2 flex gap-1">
<Button size="sm" variant="success" className="flex-1 text-[10px]" disabled={busyId === a.id} onClick={() => handle(a.id, true)}>
<Check className="h-3 w-3" /> Approve
</Button>
<Button size="sm" variant="danger" className="flex-1 text-[10px]" disabled={busyId === a.id} onClick={() => handle(a.id, false)}>
<X className="h-3 w-3" /> Deny
</Button>
</div>
</div>
)
})}
</div>
</div>
)
}