import { MetricGrid } from "@govoplan/core-webui"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Eye, RefreshCw, RotateCw, ShieldOff } from "lucide-react"; import { AdminPageLayout, Button, Card, DataGrid, Dialog, DismissibleAlert, FormField, LoadingFrame, MetricCard, ReferenceSelect, StatusBadge, TableActionGroup, ToggleSwitch, hasScope, type ApiSettings, type AuthInfo, type DataGridColumn } from "@govoplan/core-webui"; import { identityTrustAccountProvider, listAssuranceEvidence, listDeviceKeys, listEpochs, listKeyAccessDecisions, revokeDeviceKey, rotateEpoch, type AssuranceEvidence, type DeviceKey, type KeyAccessDecision, type KeyEpoch } from "../api/identityTrust"; type IdentityTrustPanelProps = { settings: ApiSettings; auth: AuthInfo; administrative?: boolean; }; type EpochDraft = { subjectKind: "identity" | "account" | "function" | "postbox" | "external_recipient"; subjectId: string; reason: string; accessDecisionRef: string; }; const EMPTY_EPOCH_DRAFT: EpochDraft = { subjectKind: "postbox", subjectId: "", reason: "", accessDecisionRef: "" }; export default function IdentityTrustPanel({ settings, auth, administrative = false }: IdentityTrustPanelProps) { const ownAccountId = auth.principal?.account_id || auth.user.account_id; const canRevokeDevice = hasScope(auth, "identity_trust:device:write") || hasScope(auth, "identity_trust:device:admin"); const [accountId, setAccountId] = useState(ownAccountId); const [keys, setKeys] = useState([]); const [evidence, setEvidence] = useState([]); const [decisions, setDecisions] = useState([]); const [epochs, setEpochs] = useState([]); const [showRevoked, setShowRevoked] = useState(false); const [loading, setLoading] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const [revoking, setRevoking] = useState(null); const [revocationReason, setRevocationReason] = useState(""); const [selectedEvidence, setSelectedEvidence] = useState(null); const [selectedDecision, setSelectedDecision] = useState(null); const [epochDraft, setEpochDraft] = useState(EMPTY_EPOCH_DRAFT); const accountProvider = useMemo(() => identityTrustAccountProvider(settings), [settings]); const loadAccount = useCallback(async () => { if (!accountId) return; setLoading(true); setError(""); try { const [nextKeys, nextEvidence, nextDecisions] = await Promise.all([ listDeviceKeys(settings, accountId, false), listAssuranceEvidence(settings, accountId, false), administrative ? listKeyAccessDecisions(settings, accountId) : Promise.resolve([]) ]); setKeys(nextKeys); setEvidence(nextEvidence); setDecisions(nextDecisions); } catch (caught) { setError(errorMessage(caught)); setKeys([]); setEvidence([]); setDecisions([]); } finally { setLoading(false); } }, [accountId, administrative, settings]); useEffect(() => { void loadAccount(); }, [loadAccount]); const visibleKeys = showRevoked ? keys : keys.filter((key) => key.status === "active"); const activeEvidence = evidence.filter((item) => item.active); const highestAssurance = activeEvidence .map((item) => item.assurance_level) .sort((left, right) => assuranceRank(right) - assuranceRank(left))[0] ?? "None"; const keyColumns = useMemo[]>(() => [ { id: "device", header: "Device", width: 180, sortable: true, filterable: true, render: (row) => row.device_id, value: (row) => row.device_id }, { id: "key", header: "Public key", width: 220, sortable: true, filterable: true, render: (row) => row.key_id, value: (row) => row.key_id }, { id: "purpose", header: "Purpose", width: 170, sortable: true, filterable: true, render: (row) => humanize(row.purpose), value: (row) => row.purpose }, { id: "algorithm", header: "Algorithm", width: 130, sortable: true, filterable: true, render: (row) => row.algorithm, value: (row) => row.algorithm }, { id: "assurance", header: "Assurance", width: 140, sortable: true, filterable: true, render: (row) => humanize(row.assurance_level), value: (row) => row.assurance_level }, { id: "status", header: "Status", width: 130, sortable: true, filterable: true, render: (row) => , value: (row) => row.status }, { id: "epoch", header: "Revision", width: 100, sortable: true, filterable: true, filterType: "integer", render: (row) => row.epoch, value: (row) => row.epoch }, { id: "expiry", header: "Expiry", width: 180, sortable: true, filterable: true, filterType: "date", render: (row) => formatDateTime(row.expires_at), value: (row) => row.expires_at ?? "" }, { id: "actions", header: "Actions", width: 90, sticky: "end", align: "right", render: (row) =>