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.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}
)
}