97 lines
4.1 KiB
TypeScript
97 lines
4.1 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { format } from "date-fns";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Loader2 } from "lucide-react";
|
|
|
|
interface AuditLog {
|
|
id: string;
|
|
userId: string | null;
|
|
action: string;
|
|
entityType: string;
|
|
entityId: string | null;
|
|
source: string;
|
|
details: any;
|
|
createdAt: string;
|
|
}
|
|
|
|
export function AuditLogsTable() {
|
|
const { t } = useTranslation();
|
|
|
|
const { data: logs, isLoading, error } = useQuery<AuditLog[]>({
|
|
queryKey: ['/api/admin/audit-logs'],
|
|
});
|
|
|
|
if (isLoading) {
|
|
return <div className="flex justify-center p-8"><Loader2 className="h-8 w-8 animate-spin" /></div>;
|
|
}
|
|
|
|
if (error) {
|
|
return <div className="p-8 text-center text-red-500">Failed to load audit logs. Please check server logs.</div>;
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('settings.auditLogs.title')}</CardTitle>
|
|
<CardDescription>{t('settings.auditLogs.description')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{t('settings.auditLogs.table.time')}</TableHead>
|
|
<TableHead>{t('settings.auditLogs.table.source')}</TableHead>
|
|
<TableHead>{t('settings.auditLogs.table.action')}</TableHead>
|
|
<TableHead>{t('settings.auditLogs.table.entity')}</TableHead>
|
|
<TableHead>{t('settings.auditLogs.table.details')}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{logs?.map((log) => (
|
|
<TableRow key={log.id}>
|
|
<TableCell className="whitespace-nowrap">
|
|
{new Date(log.createdAt).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'medium' })}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={log.source === 'AI' ? 'secondary' : 'outline'}>
|
|
{t(`audit.source.${log.source}`, { defaultValue: log.source })}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="font-medium">
|
|
{t(`audit.action.${log.action}`, { defaultValue: log.action })}
|
|
</TableCell>
|
|
<TableCell>
|
|
{t(`audit.entity.${log.entityType}`, { defaultValue: log.entityType })}
|
|
{log.entityId && <span className="text-xs text-muted-foreground block truncate max-w-[100px]">{log.entityId}</span>}
|
|
</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground w-1/3">
|
|
<pre className="whitespace-pre-wrap font-mono text-xs">
|
|
{JSON.stringify(log.details, null, 2)}
|
|
</pre>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{(!logs || logs.length === 0) && (
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
|
{t('settings.auditLogs.table.empty')}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|