papayu/desktop/ui/src/pages/AuditLogger.tsx
2026-01-29 12:21:43 +03:00

171 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { FileText, ArrowLeft, CheckCircle2, XCircle, Clock, Filter, Search, Activity, Lock } from 'lucide-react';
interface AuditEvent {
id: string;
event: string;
actor: string;
timestamp: string;
result?: 'success' | 'failure';
metadata?: Record<string, unknown>;
}
export function AuditLogger() {
const navigate = useNavigate();
const [events, setEvents] = useState<AuditEvent[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<{ type?: string; actor?: string }>({});
useEffect(() => {
const t = setTimeout(() => {
setEvents([
{ id: '1', event: 'command_executed', actor: 'command_router', timestamp: new Date().toISOString(), result: 'success', metadata: { tool: 'fs.read', path: './src/App.tsx' } },
{ id: '2', event: 'policy_denial', actor: 'policy_engine', timestamp: new Date(Date.now() - 3600000).toISOString(), result: 'failure', metadata: { reason: 'tool_not_in_allowlist', tool: 'shell.exec' } },
{ id: '3', event: 'secret_detected', actor: 'secrets_guard', timestamp: new Date(Date.now() - 7200000).toISOString(), result: 'success', metadata: { violationCount: 1, type: 'api_key' } },
]);
setLoading(false);
}, 500);
return () => clearTimeout(t);
}, []);
const getEventIcon = (event: string) => {
if (event.includes('command')) return Activity;
if (event.includes('policy')) return XCircle;
if (event.includes('secret')) return Lock;
return FileText;
};
const filtered = events.filter((e) => {
if (filter.type && e.event !== filter.type) return false;
if (filter.actor && e.actor !== filter.actor) return false;
return true;
});
if (loading) {
return (
<div className="p-8 md:p-12">
<div className="animate-pulse space-y-6">
<div className="h-10 bg-muted rounded w-1/3 mb-6" />
<div className="h-48 bg-muted rounded" />
<div className="space-y-3">
{[1, 2, 3].map((i) => <div key={i} className="h-24 bg-muted rounded" />)}
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen p-8 md:p-12 lg:p-16 bg-gradient-to-br from-background via-background to-purple-50/30 dark:to-purple-950/10">
<button
onClick={() => navigate('/')}
className="mb-8 inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-all-smooth"
>
<ArrowLeft className="w-4 h-4" />
Назад к панели управления
</button>
<div className="mb-10 md:mb-12 animate-fade-in">
<div className="flex items-center gap-4 mb-4">
<div className="p-3 rounded-xl bg-gradient-to-br from-purple-500/20 to-purple-600/10 ring-2 ring-purple-500/20">
<FileText className="w-8 h-8 text-purple-600" />
</div>
<div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight">Журнал аудита</h1>
<p className="text-lg md:text-xl text-muted-foreground font-light mt-2">Просмотр и анализ всех действий в системе</p>
</div>
</div>
</div>
<div className="bg-card/80 backdrop-blur-sm p-6 md:p-8 rounded-2xl border mb-8 animate-fade-in-up">
<div className="flex items-center gap-3 mb-6">
<Filter className="w-6 h-6 text-primary" />
<h2 className="text-2xl md:text-3xl font-bold tracking-tight">Фильтры</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium mb-2">Тип события</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<select
className="w-full pl-10 pr-4 py-2.5 border-2 rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary"
value={filter.type || ''}
onChange={(e) => setFilter({ ...filter, type: e.target.value })}
>
<option value="">Все события</option>
<option value="command_executed">Выполнение команды</option>
<option value="policy_denial">Отклонение политики</option>
<option value="secret_detected">Обнаружение секрета</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2">Агент</label>
<select
className="w-full px-4 py-2.5 border-2 rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary"
value={filter.actor || ''}
onChange={(e) => setFilter({ ...filter, actor: e.target.value })}
>
<option value="">Все агенты</option>
<option value="command_router">Command Router</option>
<option value="policy_engine">Policy Engine</option>
<option value="secrets_guard">Secrets Guard</option>
</select>
</div>
</div>
</div>
<div className="bg-card/80 backdrop-blur-sm p-6 md:p-8 rounded-2xl border animate-fade-in-up" style={{ animationDelay: '0.1s', animationFillMode: 'both' }}>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Activity className="w-6 h-6 text-primary" />
<h2 className="text-2xl md:text-3xl font-bold tracking-tight">События</h2>
</div>
<span className="text-sm text-muted-foreground">Всего: {filtered.length}</span>
</div>
{filtered.length === 0 ? (
<div className="text-center py-16">
<FileText className="w-8 h-8 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground text-lg">Нет событий для отображения</p>
</div>
) : (
<div className="space-y-3">
{filtered.map((event) => {
const Icon = getEventIcon(event.event);
const isSuccess = event.result === 'success';
return (
<div key={event.id} className="p-5 rounded-xl border-2 bg-card/50 hover:shadow-lg transition-all-smooth">
<div className="flex items-start gap-4">
<div className={`p-3 rounded-xl ${isSuccess ? 'bg-green-100 dark:bg-green-900/20' : 'bg-red-100 dark:bg-red-900/20'}`}>
<Icon className={`w-5 h-5 ${isSuccess ? 'text-green-600' : 'text-red-600'}`} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2 flex-wrap">
<span className="font-semibold">{event.event}</span>
<span className={`status-badge ${isSuccess ? 'status-active' : 'status-inactive'}`}>
{isSuccess ? <><CheckCircle2 className="w-4 h-4" /><span>Успех</span></> : <><XCircle className="w-4 h-4" /><span>Ошибка</span></>}
</span>
<span className="px-3 py-1 rounded-full text-xs font-medium bg-muted">{event.actor}</span>
</div>
<div className="flex items-center gap-2 mb-3 text-sm text-muted-foreground">
<Clock className="w-4 h-4" />
<span className="font-mono">{new Date(event.timestamp).toLocaleString('ru-RU')}</span>
</div>
{event.metadata && Object.keys(event.metadata).length > 0 && (
<div className="mt-3 p-3 rounded-lg bg-muted/50 border">
<pre className="text-xs font-mono text-muted-foreground break-all">{JSON.stringify(event.metadata, null, 2)}</pre>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
);
}