Files
app-service/frontend/src/pages/admin/AuditLogs.tsx

167 lines
6.0 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from '../../contexts/LanguageContext';
import { useSidebar } from '../../contexts/SidebarContext';
import TabsScroller from '../../components/TabsScroller';
import { auditAPI } from '../../api/client';
import type { AuditLogItem } from '../../api/client';
import '../../styles/AdminAudit.css';
export default function AuditLogs() {
const { t } = useTranslation();
const { toggleMobileMenu } = useSidebar();
const [items, setItems] = useState<AuditLogItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [page, setPage] = useState(1);
const pageSize = 50;
const [username, setUsername] = useState('');
const [action, setAction] = useState('');
const [resourceType, setResourceType] = useState('');
const [status, setStatus] = useState('');
const params = useMemo(() => {
const p: Record<string, unknown> = { page, page_size: pageSize };
if (username.trim()) p.username = username.trim();
if (action.trim()) p.action = action.trim();
if (resourceType.trim()) p.resource_type = resourceType.trim();
if (status.trim()) p.status = status.trim();
return p;
}, [page, pageSize, username, action, resourceType, status]);
const load = async () => {
setLoading(true);
setError('');
try {
const data = await auditAPI.list(params);
setItems(data.items || []);
} catch (err: any) {
setError(err?.response?.data?.detail || t.common.error);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params]);
const resetFilters = () => {
setUsername('');
setAction('');
setResourceType('');
setStatus('');
setPage(1);
};
return (
<main className="main-content admin-audit-root">
<div className="page-tabs-container">
<TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section">
<span className="page-title-text">{t.auditPage.title}</span>
</div>
</TabsScroller>
</div>
<div className="page-content">
<div className="audit-filters">
<input
className="audit-input"
placeholder={t.auditPage.username}
value={username}
onChange={(e) => { setUsername(e.target.value); setPage(1); }}
/>
<input
className="audit-input"
placeholder={t.auditPage.action}
value={action}
onChange={(e) => { setAction(e.target.value); setPage(1); }}
/>
<input
className="audit-input"
placeholder={t.auditPage.resourceType}
value={resourceType}
onChange={(e) => { setResourceType(e.target.value); setPage(1); }}
/>
<select
className="audit-input"
value={status}
onChange={(e) => { setStatus(e.target.value); setPage(1); }}
>
<option value="">{t.auditPage.anyStatus}</option>
<option value="success">{t.auditPage.statusSuccess}</option>
<option value="failure">{t.auditPage.statusFailure}</option>
<option value="pending">{t.auditPage.statusPending}</option>
<option value="error">{t.auditPage.statusError}</option>
</select>
<button className="audit-reset-btn" onClick={resetFilters} disabled={loading}>
{t.common.reset}
</button>
</div>
{error && <div className="error-message">{error}</div>}
{loading ? (
<div className="loading">{t.common.loading}</div>
) : items.length === 0 ? (
<div className="audit-empty">{t.auditPage.empty}</div>
) : (
<div className="audit-table-card">
<table className="audit-table">
<thead>
<tr>
<th>{t.auditPage.time}</th>
<th>{t.auditPage.user}</th>
<th>{t.auditPage.action}</th>
<th>{t.auditPage.resource}</th>
<th>{t.auditPage.status}</th>
<th>{t.auditPage.ip}</th>
</tr>
</thead>
<tbody>
{items.map((log) => (
<tr key={log.id}>
<td className="mono">{new Date(log.created_at).toLocaleString()}</td>
<td>{log.username || '—'}</td>
<td className="mono">{log.action}</td>
<td className="mono">{log.resource_type || '—'}{log.resource_id ? `:${log.resource_id}` : ''}</td>
<td>
<span className={`badge ${
log.status === 'success' ? 'badge-success' :
log.status === 'failure' ? 'badge-muted' :
log.status === 'error' ? 'badge-error' :
log.status === 'pending' ? 'badge-warning' :
'badge-neutral'
}`}>{log.status}</span>
</td>
<td className="mono">{log.ip_address || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<div className="audit-pagination">
<button className="btn-link" onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={loading || page <= 1}>
{t.auditPage.prev}
</button>
<span className="audit-page-indicator">
{t.auditPage.page} {page}
</span>
<button className="btn-link" onClick={() => setPage((p) => p + 1)} disabled={loading || items.length < pageSize}>
{t.auditPage.next}
</button>
</div>
</div>
</main>
);
}