aubm / frontend /src /components /AuditView.tsx
cesjavi's picture
Fix: Ultimate governance resolution via Database Views (Phase 8)
12b6b78
import React, { useState, useEffect } from 'react';
import {
ShieldCheck,
Search,
Filter,
Download,
Calendar,
User,
Bot,
FileText,
ChevronDown,
ChevronUp,
RefreshCw,
ExternalLink
} from 'lucide-react';
import { supabase } from '../services/supabase';
interface AuditLog {
id: string;
created_at: string;
user_id: string | null;
action: string;
agent_id: string | null;
task_id: string | null;
metadata: any;
profiles?: {
full_name: string | null;
email: string | null;
};
agents?: {
name: string | null;
};
tasks?: {
title: string | null;
};
}
const AuditView: React.FC = () => {
const [logs, setLogs] = useState<AuditLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [expandedLog, setExpandedLog] = useState<string | null>(null);
const [page, setPage] = useState(0);
const pageSize = 50;
useEffect(() => {
fetchLogs();
}, [page]);
const fetchLogs = async () => {
setLoading(true);
try {
const { data, error: fetchError } = await supabase
.from('audit_logs_with_details')
.select('*')
.order('created_at', { ascending: false })
.range(page * pageSize, (page + 1) * pageSize - 1);
if (fetchError) throw fetchError;
setLogs(data || []);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
const exportCSV = () => {
const headers = ['Timestamp', 'Action', 'User', 'Agent', 'Task', 'Metadata'];
const rows = logs.map(log => [
log.created_at,
log.action,
(log as any).actor_email || 'System',
(log as any).agent_name || 'N/A',
(log as any).task_title || 'N/A',
JSON.stringify(log.metadata)
]);
const csvContent = [headers, ...rows].map(e => e.join(',')).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `aubm_audit_logs_${new Date().toISOString()}.csv`);
link.click();
};
const filteredLogs = logs.filter(log =>
log.action.toLowerCase().includes(searchTerm.toLowerCase()) ||
((log as any).actor_email || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
((log as any).agent_name || '').toLowerCase().includes(searchTerm.toLowerCase())
);
const formatTimestamp = (ts: string) => {
return new Date(ts).toLocaleString();
};
const getActionBadgeColor = (action: string) => {
if (action.includes('error') || action.includes('failed')) return 'var(--danger)';
if (action.includes('created') || action.includes('added')) return 'var(--success)';
if (action.includes('approved')) return 'var(--accent)';
return 'var(--text-dim)';
};
return (
<div className="audit-view">
<header className="view-header">
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-md)' }}>
<ShieldCheck size={32} color="var(--accent)" />
<div>
<h2 style={{ margin: 0 }}>Audit Explorer</h2>
<p style={{ color: 'var(--text-dim)', margin: 0 }}>Track system actions and governance events</p>
</div>
</div>
<div style={{ display: 'flex', gap: 'var(--space-sm)' }}>
<button className="btn btn-glass" onClick={() => fetchLogs()} disabled={loading}>
<RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
Refresh
</button>
<button className="btn btn-glass" onClick={exportCSV}>
<Download size={18} />
Export CSV
</button>
</div>
</header>
<div className="glass-panel" style={{ marginTop: 'var(--space-lg)', padding: 'var(--space-md)' }}>
<div style={{ display: 'flex', gap: 'var(--space-md)', marginBottom: 'var(--space-md)' }}>
<div className="search-bar" style={{ flex: 1, position: 'relative' }}>
<Search size={18} style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-dim)' }} />
<input
type="text"
className="glass-input"
placeholder="Search actions, users, or agents..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ width: '100%', paddingLeft: '40px' }}
/>
</div>
<button className="btn btn-glass">
<Filter size={18} />
Filters
</button>
</div>
<div className="audit-table-container" style={{ overflowX: 'auto' }}>
<table className="audit-table" style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.1)', textAlign: 'left' }}>
<th style={{ padding: 'var(--space-md)', color: 'var(--text-dim)', fontSize: '0.85rem' }}>Timestamp</th>
<th style={{ padding: 'var(--space-md)', color: 'var(--text-dim)', fontSize: '0.85rem' }}>Action</th>
<th style={{ padding: 'var(--space-md)', color: 'var(--text-dim)', fontSize: '0.85rem' }}>Actor</th>
<th style={{ padding: 'var(--space-md)', color: 'var(--text-dim)', fontSize: '0.85rem' }}>Context</th>
<th style={{ padding: 'var(--space-md)', color: 'var(--text-dim)', fontSize: '0.85rem' }}></th>
</tr>
</thead>
<tbody>
{filteredLogs.map(log => (
<React.Fragment key={log.id}>
<tr
style={{
borderBottom: '1px solid rgba(255,255,255,0.05)',
cursor: 'pointer',
background: expandedLog === log.id ? 'rgba(255,255,255,0.05)' : 'transparent'
}}
onClick={() => setExpandedLog(expandedLog === log.id ? null : log.id)}
>
<td style={{ padding: 'var(--space-md)', whiteSpace: 'nowrap', fontSize: '0.9rem' }}>
<Calendar size={14} style={{ marginRight: '8px', opacity: 0.5 }} />
{formatTimestamp(log.created_at)}
</td>
<td style={{ padding: 'var(--space-md)' }}>
<span style={{
padding: '2px 8px',
borderRadius: '12px',
fontSize: '0.75rem',
fontWeight: 600,
background: 'rgba(255,255,255,0.1)',
color: getActionBadgeColor(log.action),
textTransform: 'uppercase'
}}>
{log.action.replace(/_/g, ' ')}
</span>
</td>
<td style={{ padding: 'var(--space-md)', fontSize: '0.9rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<User size={14} style={{ opacity: 0.5 }} />
{(log as any).actor_email || <span style={{ color: 'var(--text-dim)', fontSize: '0.8rem' }}>System</span>}
</div>
</td>
<td style={{ padding: 'var(--space-md)', fontSize: '0.9rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{log.agent_id && (
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', color: 'var(--accent)', fontSize: '0.8rem' }}>
<Bot size={12} />
{(log as any).agent_name}
</div>
)}
{log.task_id && (
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', color: 'var(--text-dim)', fontSize: '0.8rem' }}>
<FileText size={12} />
{(log as any).task_title}
</div>
)}
{!log.agent_id && !log.task_id && <span style={{ color: 'var(--text-dim)', fontSize: '0.8rem' }}>-</span>}
</div>
</td>
<td style={{ padding: 'var(--space-md)', textAlign: 'right' }}>
{expandedLog === log.id ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</td>
</tr>
{expandedLog === log.id && (
<tr>
<td colSpan={5} style={{ padding: 'var(--space-md)', background: 'rgba(0,0,0,0.3)' }}>
<div style={{ display: 'flex', gap: 'var(--space-lg)' }}>
<div style={{ flex: 1 }}>
<h5 style={{ margin: '0 0 8px 0', color: 'var(--text-dim)' }}>Metadata</h5>
<pre style={{
background: 'rgba(0,0,0,0.2)',
padding: 'var(--space-md)',
borderRadius: '8px',
fontSize: '0.8rem',
maxHeight: '200px',
overflowY: 'auto'
}}>
{JSON.stringify(log.metadata, null, 2)}
</pre>
</div>
<div style={{ width: '200px' }}>
<h5 style={{ margin: '0 0 8px 0', color: 'var(--text-dim)' }}>Quick Links</h5>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{log.task_id && (
<button className="btn btn-glass btn-sm" style={{ width: '100%', justifyContent: 'flex-start' }}>
<ExternalLink size={14} />
View Task
</button>
)}
{log.metadata?.project_id && (
<button className="btn btn-glass btn-sm" style={{ width: '100%', justifyContent: 'flex-start' }}>
<ExternalLink size={14} />
View Project
</button>
)}
</div>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
</div>
<div className="pagination" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 'var(--space-lg)', padding: '0 var(--space-md)' }}>
<div style={{ fontSize: '0.9rem', color: 'var(--text-dim)' }}>
Showing {filteredLogs.length} logs on this page
</div>
<div style={{ display: 'flex', gap: 'var(--space-sm)' }}>
<button
className="btn btn-glass btn-sm"
onClick={() => setPage(p => Math.max(0, p - 1))}
disabled={page === 0 || loading}
>
Previous
</button>
<span style={{ display: 'flex', alignItems: 'center', padding: '0 var(--space-md)', fontWeight: 600 }}>{page + 1}</span>
<button
className="btn btn-glass btn-sm"
onClick={() => setPage(p => p + 1)}
disabled={logs.length < pageSize || loading}
>
Next
</button>
</div>
</div>
</div>
</div>
);
};
export default AuditView;