import { useState, useMemo, useCallback } from 'react'; import { Badge, Button, DateRangePicker, Input, Select, MonoText, CodeBlock, DataTable, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { Download } from 'lucide-react'; import { useAuditLog, type AuditEvent } from '../../api/queries/admin/audit'; import styles from './AuditLogPage.module.css'; import tableStyles from '../../styles/table-section.module.css'; const CATEGORIES = [ { value: '', label: 'All categories' }, { value: 'INFRA', label: 'INFRA' }, { value: 'AUTH', label: 'AUTH' }, { value: 'USER_MGMT', label: 'USER_MGMT' }, { value: 'CONFIG', label: 'CONFIG' }, { value: 'RBAC', label: 'RBAC' }, { value: 'AGENT', label: 'AGENT' }, ]; function exportCsv(events: AuditEvent[]) { const headers = ['Timestamp', 'User', 'Category', 'Action', 'Target', 'Result', 'Details']; const rows = events.map(e => [ e.timestamp, e.username, e.category, e.action, e.target, e.result, e.detail ?? '', ]); const csv = [headers, ...rows].map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `cameleer-audit-${new Date().toISOString().slice(0, 16).replace(':', '-')}.csv`; a.click(); URL.revokeObjectURL(url); } function formatTimestamp(iso: string): string { return new Date(iso).toLocaleString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); } type AuditRow = Omit & { id: string }; const COLUMNS: Column[] = [ { key: 'timestamp', header: 'Timestamp', width: '170px', sortable: true, render: (_, row) => {formatTimestamp(row.timestamp)}, }, { key: 'username', header: 'User', sortable: true, render: (_, row) => {row.username}, }, { key: 'category', header: 'Category', width: '110px', sortable: true, render: (_, row) => , }, { key: 'action', header: 'Action' }, { key: 'target', header: 'Target', render: (_, row) => {row.target}, }, { key: 'result', header: 'Result', width: '90px', sortable: true, render: (_, row) => ( ), }, ]; export default function AuditLogPage() { const [dateRange, setDateRange] = useState({ start: new Date(Date.now() - 7 * 24 * 3600_000), end: new Date(), }); const [userFilter, setUserFilter] = useState(''); const [categoryFilter, setCategoryFilter] = useState(''); const [searchFilter, setSearchFilter] = useState(''); const [page, setPage] = useState(0); const [sortField, setSortField] = useState('timestamp'); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc'); const handleSortChange = useCallback((key: string, dir: 'asc' | 'desc') => { setSortField(key); setSortDir(dir); setPage(0); }, []); const { data } = useAuditLog({ username: userFilter || undefined, category: categoryFilter || undefined, search: searchFilter || undefined, from: dateRange.start.toISOString(), to: dateRange.end.toISOString(), sort: sortField, order: sortDir, page, size: 25, }); const rows: AuditRow[] = useMemo( () => (data?.items || []).map((item) => ({ ...item, id: String(item.id) })), [data], ); const totalCount = data?.totalCount ?? 0; return (
{ setDateRange(range); setPage(0); }} /> { setUserFilter(e.target.value); setPage(0); }} onClear={() => { setUserFilter(''); setPage(0); }} className={styles.filterInput} /> { setSearchFilter(e.target.value); setPage(0); }} onClear={() => { setSearchFilter(''); setPage(0); }} className={styles.filterInput} />
Audit Log
{totalCount} events
row.result === 'FAILURE' ? 'error' : undefined} expandedContent={(row) => (
IP Address {row.ipAddress}
User Agent {row.userAgent}
Detail
)} />
); }