import { cn } from '@gentleduck/libs/cn' import React from 'react' import { Refresh } from '../components/icons' import { JsonTree } from '../components/json-tree' import { DetailEmpty, FilterBar, ListItem, ListShell, Section, SplitView } from '../components/layout' import { Badge, Button } from '../components/ui' import type { IamIFlowEntry, IamIFlowRecorder } from '../lib/flow' function pad(n: number, w = 2) { return String(n).padStart(w, '0') } function fmtTime(ts: number) { const d = new Date(ts) return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}` } function fmtAgo(ts: number, now: number) { const ms = Math.max(0, now - ts) if (ms < 1000) return `${ms}ms ago` if (ms < 60_000) return `${Math.floor(ms / 1000)}s ago` if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago` return `${Math.floor(ms / 3_600_000)}h ago` } function ActionChip({ action }: { action: string }) { return ( {action} ) } function ResourceChip({ resource, resourceId }: { resource: string; resourceId?: string }) { return ( {resource} {resourceId && #{resourceId}} ) } function SubjectChip({ id }: { id: string }) { const initial = id.replace(/^u-/, '').charAt(0).toUpperCase() || '?' return (
{initial} {id}
) } export function IamFlowPanel({ flow }: { flow: IamIFlowRecorder }) { const [entries, setEntries] = React.useState(() => flow.list()) const [selected, setSelected] = React.useState(null) const [filter, setFilter] = React.useState('') const [showAllow, setShowAllow] = React.useState(true) const [showDeny, setShowDeny] = React.useState(true) const [now, setNow] = React.useState(Date.now()) const [copied, setCopied] = React.useState(false) React.useEffect(() => { const off = flow.subscribe(() => setEntries(flow.list().slice())) return off }, [flow]) React.useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000) return () => clearInterval(id) }, []) const q = filter.trim().toLowerCase() const filtered = entries.filter((e) => { if (!showAllow && e.allowed) return false if (!showDeny && !e.allowed) return false if (!q) return true return ( e.subjectId.toLowerCase().includes(q) || e.action.toLowerCase().includes(q) || e.resource.toLowerCase().includes(q) || (e.resourceId ?? '').toLowerCase().includes(q) ) }) const current = selected != null ? (flow.get(selected) ?? null) : null const counts = React.useMemo(() => { let allow = 0, deny = 0 for (const e of entries) e.allowed ? allow++ : deny++ return { allow, deny } }, [entries]) const copyEntry = async () => { if (!current) return try { await navigator.clipboard.writeText(JSON.stringify(current, null, 2)) setCopied(true) setTimeout(() => setCopied(false), 1500) } catch {} } return ( flow.clear()}> clear }>
setShowAllow((v) => !v)}> allow {counts.allow} setShowDeny((v) => !v)}> deny {counts.deny}
{filtered.length === 0 && ( )} {filtered.map((e) => ( setSelected(e.id)} primary={ {e.action} on {e.resource} {e.resourceId && #{e.resourceId}} } secondary={ {e.subjectId} {fmtAgo(e.ts, now)} {typeof e.durationMs === 'number' && ( <> {e.durationMs.toFixed(1)}ms )} } /> ))} } right={ !current ? ( ) : (
{current.allowed ? 'allow' : 'deny'} on {fmtTime(current.ts)} {typeof current.durationMs === 'number' && ( <> {current.durationMs.toFixed(2)}ms )}
{current.scope && scope: {current.scope}}
{current.reason && (

{current.reason}

)} {(current.decidingPolicy || current.decidingRule) && (
{current.decidingPolicy && } {current.decidingRule && }
)} {current.environment && Object.keys(current.environment).length > 0 && (
)}
) } /> ) } function FilterPill({ active, tone, onClick, children, }: { active: boolean tone: 'allow' | 'deny' onClick: () => void children: React.ReactNode }) { return ( ) } function Dot() { return } function Kv({ k, v }: { k: string; v: string }) { return (
{k} {v}
) }