import { useDeferredValue, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { listAuditLogs } from '@/lib/api-client'; import { useDemoMode } from '@/lib/demo-mode'; import { type FilterConfig } from '@/lib/dynamic-filter'; import { useDynamicFilter } from '@/lib/use-dynamic-filter'; import { formatDate, formatRelative } from '@/lib/time'; import { DemoRestricted, FilterBar } from '@/shared/components'; import { QuietBtn, SCard, SPageHead, SPill, STable, type SCol } from '@/shared/components/sentry'; import { ScrollText, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'; import type { AuditLogEntity } from '@/lib/api-client'; const PAGE_SIZE = 50; const rangeOptions = [ { value: '', label: 'All time' }, { value: '24h', label: 'Last 24 hours', ms: 24 * 60 * 60 * 1000 }, { value: '7d', label: 'Last 7 days', ms: 7 * 24 * 60 * 60 * 1000 }, { value: '30d', label: 'Last 30 days', ms: 30 * 24 * 60 * 60 * 1000 }, ] as const; // Server-backed filters — the engine runs over empty data; values map to // API params. URL state makes filtered views shareable. const AUDIT_SCHEMA: FilterConfig[] = [ { key: 'resource', label: 'Resource', type: 'search' }, { key: 'action', label: 'Action', type: 'search' }, { key: 'actor', label: 'Actor', type: 'search' }, { key: 'range', label: 'Time', type: 'select', options: rangeOptions.filter((o) => o.value !== '').map((o) => ({ value: o.value, label: o.label })), }, ]; export function AuditLogsPage() { const isDemoMode = useDemoMode(); const filter = useDynamicFilter({ data: [], schema: AUDIT_SCHEMA }); const [page, setPage] = useState(1); // Back to page 1 whenever the filter set changes. const filtersJson = JSON.stringify(filter.filters); const [prevFiltersJson, setPrevFiltersJson] = useState(filtersJson); if (prevFiltersJson !== filtersJson) { setPrevFiltersJson(filtersJson); setPage(1); } const deferredResource = useDeferredValue(String(filter.filters.resource ?? '')); const deferredAction = useDeferredValue(String(filter.filters.action ?? '')); const deferredActor = useDeferredValue(String(filter.filters.actor ?? '')); const range = filter.filters.range === 'all' ? '' : String(filter.filters.range ?? ''); const filters = { resource: deferredResource.trim() || undefined, action: deferredAction.trim() || undefined, actor: deferredActor.trim() || undefined, page, limit: PAGE_SIZE, }; const logsQuery = useQuery({ queryKey: ['audit-logs', filters, range], queryFn: () => { const option = rangeOptions.find((o) => o.value === range); const since = option && 'ms' in option ? new Date(Date.now() - option.ms).toISOString() : undefined; return listAuditLogs({ ...filters, since }); }, enabled: !isDemoMode, }); if (isDemoMode) { return ; } const logs = logsQuery.data ?? []; const hasNext = logs.length === PAGE_SIZE; const hasFilters = filter.activeCount > 0; const cols: SCol[] = [ { key: 'time', label: 'Time', width: '100px', sortable: false, render: (log) => ( {log.createdAt ? formatRelative(log.createdAt) : '—'} ), }, { key: 'actor', label: 'Actor', width: 'minmax(140px,1.1fr)', sortValue: (log) => log.userEmail ?? log.userId ?? 'system', render: (log) => ( {log.userEmail || (log.userId ? log.userId.slice(0, 8) : 'system')} ), }, { key: 'resource', label: 'Resource', width: 'minmax(120px,1fr)', sortValue: (log) => log.resource ?? '', render: (log) => {log.resource}, }, { key: 'action', label: 'Action', width: 'minmax(130px,1fr)', sortValue: (log) => log.action ?? '', render: (log) => {log.action}, }, { key: 'rid', label: 'Resource ID', width: 'minmax(110px,0.8fr)', sortable: false, render: (log) => ( {log.resourceId || '—'} ), }, { key: 'ip', label: 'IP', width: '110px', sortable: false, render: (log) => {log.ipAddress?.replace(/\/\d+$/, '') || '—'}, }, { key: 'details', label: 'Details', width: 'minmax(160px,1.2fr)', sortable: false, render: (log) => ( {log.details && log.details !== '{}' ? log.details : '—'} ), }, ]; return (
} title="Event Log" accent={`page ${page}`} pad={false}>
{logsQuery.isLoading ? (
) : logsQuery.isError ? (
Failed to load audit logs.
) : logs.length === 0 ? (

No audit events{hasFilters ? ' match these filters' : ' recorded yet'}.

) : ( l.id} /> )}

Page {page}{logs.length > 0 ? ` — ${logs.length} entries` : ''}

setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}> Prev setPage((p) => p + 1)} disabled={!hasNext}> Next
); }