Frontend — consistent HSL token usage across remaining pages: - Users: shared Card, Badge with success/error tokens, h2 typography, animate-fadeIn - Audit: shared Card, muted-foreground text, animate-fadeIn - Clients: shared Card, Badge active/inactive, hsl(--primary) icon color - Storage: shared Card, StatusBadge for status pills, hsl warning/primary bars replacing hardcoded amber/blue, all gray text → muted-foreground - Login: hsl(--surface) bg, hsl(--primary) submit button, brand mark icon, animate-scaleIn card entry, hsl(--warning) dev notice Backend tests — convert print-only stubs to real assertions: - test_pptx_creator: mkdir, deterministic save path, assert file exists + slide count - test_gemini_schema_support: direct google.genai client, skipif guard on GOOGLE_API_KEY, JSON parse + Pydantic model validation assertions - test_openai_schema_support: clean skip (OpenAI removed in Phase 6) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
105 lines
3.5 KiB
TypeScript
105 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { AppDispatch, RootState } from '@/store/store';
|
|
import { fetchAuditLogs } from '@/store/slices/adminSlice';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card } from '@/components/shared/Card';
|
|
import DataExportButton from '../components/DataExportButton';
|
|
import { Search } from 'lucide-react';
|
|
|
|
export default function AuditLogPage() {
|
|
const dispatch = useDispatch<AppDispatch>();
|
|
const { auditLogs } = useSelector((state: RootState) => state.admin);
|
|
const [actionFilter, setActionFilter] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadLogs();
|
|
}, []);
|
|
|
|
const loadLogs = async (params?: Record<string, string>) => {
|
|
setLoading(true);
|
|
await dispatch(fetchAuditLogs(params));
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleSearch = () => {
|
|
const params: Record<string, string> = {};
|
|
if (actionFilter) params.action = actionFilter;
|
|
loadLogs(params);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4 animate-fadeIn">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="h2">Audit Log</h1>
|
|
<DataExportButton endpoint="/api/v1/admin/audit-log/export" />
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Input
|
|
placeholder="Filter by action..."
|
|
value={actionFilter}
|
|
onChange={(e) => setActionFilter(e.target.value)}
|
|
className="max-w-xs"
|
|
/>
|
|
<Button variant="outline" onClick={handleSearch}>
|
|
<Search className="w-4 h-4 mr-1" />Search
|
|
</Button>
|
|
</div>
|
|
|
|
<Card className="p-0 overflow-hidden">
|
|
{loading ? (
|
|
<div className="space-y-3 p-4">
|
|
{[...Array(5)].map((_, i) => (
|
|
<div key={i} className="h-10 bg-[hsl(var(--surface-hover))] rounded animate-pulse" />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Time</TableHead>
|
|
<TableHead>Action</TableHead>
|
|
<TableHead>Resource</TableHead>
|
|
<TableHead>User</TableHead>
|
|
<TableHead>IP</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{auditLogs.map((log) => (
|
|
<TableRow key={log.id}>
|
|
<TableCell className="text-sm text-muted-foreground">
|
|
{log.created_at ? new Date(log.created_at).toLocaleString() : '—'}
|
|
</TableCell>
|
|
<TableCell className="font-mono text-sm">{log.action}</TableCell>
|
|
<TableCell>{log.resource_type}</TableCell>
|
|
<TableCell className="text-sm">{log.user_id || 'System'}</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">{log.ip_address || '—'}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{auditLogs.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="text-center text-muted-foreground py-8">
|
|
No audit log entries found.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|