Files
cameleer-server/ui/src/pages/Admin/AuditLogPage.tsx
hsiegeln 81f85aa82d
Some checks failed
CI / build (push) Successful in 1m18s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been cancelled
CI / deploy (push) Has been cancelled
feat: replace UI with design system example pages wired to real API
Migrate all page components from the @cameleer/design-system v0.0.3
example UI, replacing mock data with real backend API hooks. This brings
richer visuals (KpiStrip, GroupCard, RouteFlow, ProcessorTimeline,
DateRangePicker, expandable rows) while preserving all existing API
integration, auth, and routing infrastructure.

Pages migrated: Dashboard, RoutesMetrics, RouteDetail, ExchangeDetail,
AgentHealth, AgentInstance, OidcConfig, AuditLog, RBAC (Users/Groups/Roles).
Also enhanced LayoutShell CommandPalette with real search data from catalog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:42:16 +01:00

149 lines
4.9 KiB
TypeScript

import { useState, useMemo } from 'react';
import {
Badge, DateRangePicker, Input, Select, MonoText, CodeBlock, DataTable,
} from '@cameleer/design-system';
import type { Column } from '@cameleer/design-system';
import { useAuditLog, type AuditEvent } from '../../api/queries/admin/audit';
import styles from './AuditLogPage.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' },
];
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<AuditEvent, 'id'> & { id: string };
const COLUMNS: Column<AuditRow>[] = [
{
key: 'timestamp', header: 'Timestamp', width: '170px', sortable: true,
render: (_, row) => <MonoText size="xs">{formatTimestamp(row.timestamp)}</MonoText>,
},
{
key: 'username', header: 'User', sortable: true,
render: (_, row) => <span style={{ fontWeight: 500 }}>{row.username}</span>,
},
{
key: 'category', header: 'Category', width: '110px', sortable: true,
render: (_, row) => <Badge label={row.category} color="auto" />,
},
{ key: 'action', header: 'Action' },
{
key: 'target', header: 'Target',
render: (_, row) => <span className={styles.target}>{row.target}</span>,
},
{
key: 'result', header: 'Result', width: '90px', sortable: true,
render: (_, row) => (
<Badge label={row.result} color={row.result === 'SUCCESS' ? 'success' : 'error'} />
),
},
];
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 { data } = useAuditLog({
username: userFilter || undefined,
category: categoryFilter || undefined,
search: searchFilter || undefined,
from: dateRange.start.toISOString(),
to: dateRange.end.toISOString(),
page,
size: 25,
});
const rows: AuditRow[] = useMemo(
() => (data?.items || []).map((item) => ({ ...item, id: String(item.id) })),
[data],
);
const totalCount = data?.totalCount ?? 0;
return (
<div>
<div className={styles.filters}>
<DateRangePicker
value={dateRange}
onChange={(range) => { setDateRange(range); setPage(0); }}
/>
<Input
placeholder="Filter by user..."
value={userFilter}
onChange={(e) => { setUserFilter(e.target.value); setPage(0); }}
onClear={() => { setUserFilter(''); setPage(0); }}
className={styles.filterInput}
/>
<Select
options={CATEGORIES}
value={categoryFilter}
onChange={(e) => { setCategoryFilter(e.target.value); setPage(0); }}
className={styles.filterSelect}
/>
<Input
placeholder="Search action or target..."
value={searchFilter}
onChange={(e) => { setSearchFilter(e.target.value); setPage(0); }}
onClear={() => { setSearchFilter(''); setPage(0); }}
className={styles.filterInput}
/>
</div>
<div className={styles.tableSection}>
<div className={styles.tableHeader}>
<span className={styles.tableTitle}>Audit Log</span>
<div className={styles.tableRight}>
<span className={styles.tableMeta}>
{totalCount} events
</span>
<Badge label="LIVE" color="success" />
</div>
</div>
<DataTable
columns={COLUMNS}
data={rows}
sortable
flush
pageSize={25}
rowAccent={(row) => row.result === 'FAILURE' ? 'error' : undefined}
expandedContent={(row) => (
<div className={styles.expandedDetail}>
<div className={styles.detailGrid}>
<div className={styles.detailField}>
<span className={styles.detailLabel}>IP Address</span>
<MonoText size="xs">{row.ipAddress}</MonoText>
</div>
<div className={styles.detailField}>
<span className={styles.detailLabel}>User Agent</span>
<span className={styles.detailValue}>{row.userAgent}</span>
</div>
</div>
<div className={styles.detailField}>
<span className={styles.detailLabel}>Detail</span>
<CodeBlock content={JSON.stringify(row.detail, null, 2)} language="json" />
</div>
</div>
)}
/>
</div>
</div>
);
}