68 lines
2.5 KiB
TypeScript
68 lines
2.5 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { listAudit, getAuditActions, AUDIT_PAGE_SIZE } from "@/lib/admin/audit";
|
|
import { PageHeader } from "@/components/app/page-header";
|
|
import { DataTable, TableToolbar, type Column } from "@/components/admin/ui/data-table";
|
|
import { SearchInput, FilterSelect, Pagination } from "@/components/admin/ui/table-controls";
|
|
import { AuditExport } from "@/components/admin/audit-export";
|
|
import { AuditMetaViewer } from "@/components/admin/audit-meta-viewer";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
export const metadata: Metadata = { title: "Admin · Audit log" };
|
|
|
|
type Row = Awaited<ReturnType<typeof listAudit>>["rows"][number];
|
|
|
|
export default async function AdminAuditPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<Record<string, string | undefined>>;
|
|
}) {
|
|
const sp = await searchParams;
|
|
const page = Math.max(1, Number(sp.page ?? "1"));
|
|
const [{ rows, total }, actions] = await Promise.all([
|
|
listAudit({ action: sp.action, actor: sp.q, page }),
|
|
getAuditActions(),
|
|
]);
|
|
|
|
const columns: Column<Row>[] = [
|
|
{
|
|
key: "when",
|
|
header: "When",
|
|
cell: (l) => <span className="whitespace-nowrap text-muted-foreground">{l.createdAt.toLocaleString()}</span>,
|
|
},
|
|
{ key: "actor", header: "Actor", cell: (l) => l.actor?.email ?? l.actorType },
|
|
{ key: "action", header: "Action", cell: (l) => <Badge variant="outline">{l.action}</Badge> },
|
|
{
|
|
key: "target",
|
|
header: "Target",
|
|
cell: (l) => <span className="font-mono text-xs text-muted-foreground">{l.target ?? "—"}</span>,
|
|
},
|
|
{
|
|
key: "meta",
|
|
header: "Details",
|
|
cell: (l) => <AuditMetaViewer metadata={l.metadata} action={l.action} />,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<PageHeader title="Audit log" description="Every administrative action, recorded." />
|
|
<TableToolbar>
|
|
<SearchInput placeholder="Filter by actor email…" />
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<FilterSelect
|
|
param="action"
|
|
placeholder="Action"
|
|
allLabel="All actions"
|
|
options={actions.map((a) => ({ value: a, label: a }))}
|
|
/>
|
|
<AuditExport filters={{ action: sp.action, actor: sp.q }} />
|
|
</div>
|
|
</TableToolbar>
|
|
<DataTable columns={columns} rows={rows} getRowKey={(l) => l.id} empty="No audit entries yet." />
|
|
<div className="mt-4">
|
|
<Pagination page={page} pageSize={AUDIT_PAGE_SIZE} total={total} />
|
|
</div>
|
|
</>
|
|
);
|
|
}
|