ppt-tool/frontend/app/admin/audit/page.tsx
Vadym Samoilenko 5def8f9e84 Phase 7: Apply design system to all admin pages + fix test stubs
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>
2026-03-01 19:01:52 +00:00

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>
);
}