/** * Filterable, paginated transparency log table with stats cards, expandable * row details, and CSV export. * * Shows every KMS credential access event for the tenant -- timestamp, * device name, action, justification, operator, and correlation ID. * * Phase 31 -- Data Access Transparency Dashboard (TRUST-01, TRUST-02) */ import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Download, Eye, Activity, Clock, HardDrive, } from 'lucide-react' import { transparencyApi, type TransparencyLogEntry, type TransparencyLogParams, } from '@/lib/transparencyApi' import { cn } from '@/lib/utils' import { EmptyState } from '@/components/ui/empty-state' import { DeviceLink } from '@/components/ui/device-link' // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const JUSTIFICATION_TYPES = [ { value: '', label: 'All Justifications' }, { value: 'poller', label: 'Scheduled Poll' }, { value: 'config_push', label: 'Config Push' }, { value: 'backup', label: 'Config Backup' }, { value: 'command', label: 'Remote Command' }, { value: 'api_backup', label: 'API Backup' }, ] as const const ACTION_TYPES = [ { value: '', label: 'All Actions' }, { value: 'decrypt_credential', label: 'Decrypt Credential' }, { value: 'encrypt_credential', label: 'Encrypt Credential' }, { value: 'rotate_key', label: 'Rotate Key' }, { value: 'provision_key', label: 'Provision Key' }, ] as const const PER_PAGE_OPTIONS = [25, 50, 100] as const /** Human-readable justification labels. */ function justificationLabel(justification: string | null): string { if (!justification) return 'System' const map: Record = { poller: 'Scheduled Poll', config_push: 'Config Push', backup: 'Config Backup', command: 'Remote Command', api_backup: 'API Backup', } return map[justification] ?? 'System' } /** Justification badge color classes. */ function justificationBadgeClasses(justification: string | null): string { switch (justification) { case 'poller': return 'bg-info/10 text-info border-info/20' case 'config_push': return 'bg-warning/10 text-warning border-warning/20' case 'backup': case 'api_backup': return 'bg-success/10 text-success border-success/20' case 'command': return 'bg-error/10 text-error border-error/20' default: return 'bg-elevated text-text-secondary border-border' } } /** Formats an ISO timestamp into a human-readable relative time string. */ function formatRelativeTime(iso: string): string { const now = Date.now() const then = new Date(iso).getTime() const diffMs = now - then if (diffMs < 0) return 'just now' const seconds = Math.floor(diffMs / 1000) if (seconds < 60) return 'just now' const minutes = Math.floor(seconds / 60) if (minutes < 60) return `${minutes}m ago` const hours = Math.floor(minutes / 60) if (hours < 24) return `${hours}h ago` const days = Math.floor(hours / 24) if (days < 7) return `${days}d ago` const weeks = Math.floor(days / 7) if (weeks < 4) return `${weeks}w ago` const months = Math.floor(days / 30) return `${months}mo ago` } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- interface TransparencyLogTableProps { tenantId: string } export function TransparencyLogTable({ tenantId }: TransparencyLogTableProps) { const [page, setPage] = useState(1) const [perPage, setPerPage] = useState(50) const [justificationFilter, setJustificationFilter] = useState('') const [actionFilter, setActionFilter] = useState('') const [dateFrom, setDateFrom] = useState('') const [dateTo, setDateTo] = useState('') const [expandedId, setExpandedId] = useState(null) const [exporting, setExporting] = useState(false) const params: TransparencyLogParams = { page, per_page: perPage, ...(justificationFilter ? { justification: justificationFilter } : {}), ...(actionFilter ? { action: actionFilter } : {}), ...(dateFrom ? { date_from: new Date(dateFrom).toISOString() } : {}), ...(dateTo ? { date_to: new Date(dateTo + 'T23:59:59').toISOString() } : {}), } const { data, isLoading, isError } = useQuery({ queryKey: [ 'transparency-logs', tenantId, page, perPage, justificationFilter, actionFilter, dateFrom, dateTo, ], queryFn: () => transparencyApi.list(tenantId, params), enabled: !!tenantId, }) const { data: stats } = useQuery({ queryKey: ['transparency-stats', tenantId], queryFn: () => transparencyApi.stats(tenantId), enabled: !!tenantId, }) const totalPages = data ? Math.ceil(data.total / perPage) : 0 const handleExport = async () => { setExporting(true) try { await transparencyApi.exportCsv(tenantId, { ...(justificationFilter ? { justification: justificationFilter } : {}), ...(actionFilter ? { action: actionFilter } : {}), ...(dateFrom ? { date_from: new Date(dateFrom).toISOString() } : {}), ...(dateTo ? { date_to: new Date(dateTo + 'T23:59:59').toISOString() } : {}), }) } finally { setExporting(false) } } return (
{/* Stats cards */} {stats && (
By Justification
{Object.entries(stats.justification_breakdown).map(([key, count]) => ( {justificationLabel(key === 'system' ? null : key)}: {count} ))} {Object.keys(stats.justification_breakdown).length === 0 && ( No events )}
)} {/* Filter bar */}
{/* Justification filter */} {/* Action filter */} {/* Date from */}
From { setDateFrom(e.target.value) setPage(1) }} className="h-8 rounded-md border border-border bg-surface px-2 text-xs text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" />
{/* Date to */}
To { setDateTo(e.target.value) setPage(1) }} className="h-8 rounded-md border border-border bg-surface px-2 text-xs text-text-primary focus:outline-none focus:ring-1 focus:ring-accent" />
{/* Spacer */}
{/* Export CSV */}
{/* Table */}
{isLoading ? (

Loading transparency logs...

) : isError ? (

Failed to load transparency logs.

) : !data || data.items.length === 0 ? ( ) : ( {data.items.map((item) => ( setExpandedId(expandedId === item.id ? null : item.id) } /> ))}
Time Device Action Justification Operator Correlation ID
)}
{/* Pagination */} {data && data.total > 0 && (
Rows per page: {(page - 1) * perPage + 1}-- {Math.min(page * perPage, data.total)} of {data.total}
Page {page} of {totalPages}
)}
) } // --------------------------------------------------------------------------- // Stats card sub-component // --------------------------------------------------------------------------- interface StatsCardProps { icon: React.FC<{ className?: string }> label: string value: string } function StatsCard({ icon: Icon, label, value }: StatsCardProps) { return (
{label}
{value}
) } // --------------------------------------------------------------------------- // Row sub-component // --------------------------------------------------------------------------- interface TransparencyLogRowProps { item: TransparencyLogEntry tenantId: string isExpanded: boolean onToggle: () => void } function TransparencyLogRow({ item, tenantId, isExpanded, onToggle, }: TransparencyLogRowProps) { const hasDetails = !!(item.resource_type || item.resource_id || item.ip_address) return ( <> {hasDetails ? ( isExpanded ? ( ) : ( ) ) : ( )} {formatRelativeTime(item.created_at)} {item.device_name && item.device_id ? ( {item.device_name} ) : (item.device_name ?? '--')} {item.action.replace(/_/g, ' ')} {justificationLabel(item.justification)} {item.operator_email ?? '--'} {item.correlation_id ? item.correlation_id.length > 12 ? item.correlation_id.substring(0, 12) + '...' : item.correlation_id : '--'} {/* Expanded details row */} {isExpanded && hasDetails && (
{item.resource_type && (
Resource Type:{' '} {item.resource_type}
)} {item.resource_id && (
Resource ID:{' '} {item.resource_id}
)} {item.ip_address && (
IP Address:{' '} {item.ip_address}
)} {item.correlation_id && (
Full Correlation ID:{' '} {item.correlation_id}
)} {item.device_id && (
Device ID:{' '} {item.device_id}
)}
)} ) }