import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { api, SettingsActor } from '../lib/api'; import { Card, StatusBadge } from '../components/ui/Card'; const ROLES = ['viewer', 'project-admin', 'approver', 'org-admin', 'auditor']; const STATUS = ['pending', 'approved', 'rejected', 'auto_allowed', 'all']; function parseValue(raw: string): unknown { try { return JSON.parse(raw); } catch { return raw; } } export function Approvals() { const queryClient = useQueryClient(); const [actor, setActor] = useState({ actor: 'alice', role: 'project-admin', project: 'default', tenant: 'default' }); const [status, setStatus] = useState('pending'); const [key, setKey] = useState('security.strict'); const [value, setValue] = useState('true'); const [sensitive, setSensitive] = useState(true); const [reason, setReason] = useState('review requested from Control Panel'); const [decisionReason, setDecisionReason] = useState('reviewed in approval inbox'); const [message, setMessage] = useState(null); const inbox = useQuery({ queryKey: ['approvals', actor, status], queryFn: () => api.approvals(actor, status), retry: false, }); const submit = useMutation({ mutationFn: () => api.submitApproval(actor, { action: 'settings.write', target: key, risk: sensitive ? 'high' : 'standard', sensitive, reason, payload: { key, value: parseValue(value) }, }), onSuccess: (res) => { setMessage(`submitted ${res.proposal.id} status=${res.proposal.status}`); void queryClient.invalidateQueries({ queryKey: ['approvals'] }); }, onError: (err: any) => setMessage(err?.response?.data?.message || err.message || 'submit failed'), }); const decide = useMutation({ mutationFn: ({ id, decision }: { id: string; decision: 'approve' | 'reject' }) => api.decideApproval(actor, { id, decision, reason: decisionReason }), onSuccess: (res) => { setMessage(`${res.proposal.status} ${res.proposal.id}${res.applied ? ` applied v${res.applied.version}` : ''}`); void queryClient.invalidateQueries({ queryKey: ['approvals'] }); void queryClient.invalidateQueries({ queryKey: ['settings'] }); }, onError: (err: any) => setMessage(err?.response?.data?.message || err.message || 'decision failed'), }); if (inbox.isLoading || !inbox.data) return
Loading…
; return ( <> }>
{message &&
{message}
}
{inbox.data.proposals.map((p) => (
{p.id} · {p.action} · {p.target}
{p.proposer} · {p.created_at} · {p.delegation?.level} · {p.delegation?.reason}
{p.reason}
{JSON.stringify(p.payload, null, 2)}
{p.status === 'pending' && (
)}
))} {inbox.data.proposals.length === 0 &&
No proposals for this status.
}
{inbox.data.oversight.slice().reverse().map((o: any) => (
{o.event} · {o.proposal_id}
{o.actor} · {o.at} · hash {String(o.hash).slice(0, 12)}
{o.reason}
))}
); }