Files
TREK/client/src/components/Admin/AuditLogPanel.tsx
T
Maurice a52179f685 refactor(admin): share the audit log loading and paging between desktop and phone
Both audit log panels now read one useAuditLog hook for the first page, refresh, load more and the row labels. The desktop table and the phone cards keep their own markup.
2026-10-09 15:19:45 +02:00

103 lines
4.2 KiB
TypeScript

import React from 'react'
import { useTranslation } from '../../i18n'
import { RefreshCw, ClipboardList, Loader2 } from 'lucide-react'
import { fs } from '../shared/DialogShell'
import { SETTINGS_BUTTON, SettingsCard, SettingsHint, StatusPill } from '../Settings/settingsKit'
import { useAuditLog } from './useAuditLog'
interface AuditLogPanelProps {
serverTimezone?: string
}
const TH = 'whitespace-nowrap px-3.5 py-2.5 text-start font-geist font-bold uppercase tracking-[.08em] text-content-faint'
const TD = 'px-3.5 py-2.5 align-top'
const CODE = 'font-geist tabular-nums'
export default function AuditLogPanel({ serverTimezone }: AuditLogPanelProps): React.ReactElement {
const { t } = useTranslation()
const { entries, total, loading, loadFirstPage, loadMore, fmtTime, fmtDetails, userLabel } = useAuditLog(serverTimezone)
let body: React.ReactNode
if (loading && entries.length === 0) {
body = (
<div className="flex items-center justify-center gap-2 py-10 text-content-faint" style={fs(12.5, 'body')}>
<Loader2 size={15} className="animate-spin" />
<span>{t('common.loading')}</span>
</div>
)
} else if (entries.length === 0) {
body = <SettingsHint className="py-6 text-center">{t('admin.audit.empty')}</SettingsHint>
} else {
body = (
<div className={`overflow-x-auto rounded-[12px] border border-edge-faint bg-surface-card transition-opacity ${loading ? 'opacity-60' : ''}`}>
<table className="w-full min-w-[720px] border-collapse">
<thead>
<tr className="border-b border-edge-faint bg-surface-secondary" style={fs(9.5)}>
<th className={TH}>{t('admin.audit.col.time')}</th>
<th className={TH}>{t('admin.audit.col.user')}</th>
<th className={TH}>{t('admin.audit.col.action')}</th>
<th className={TH}>{t('admin.audit.col.resource')}</th>
<th className={TH}>{t('admin.audit.col.ip')}</th>
<th className={TH}>{t('admin.audit.col.details')}</th>
</tr>
</thead>
<tbody className="divide-y divide-edge-faint" style={fs(12, 'body')}>
{entries.map((e) => (
<tr key={e.id} className="hover:bg-surface-secondary">
<td className={`${TD} ${CODE} whitespace-nowrap text-content-muted`}>{fmtTime(e.created_at)}</td>
<td className={`${TD} font-medium text-content`}>
{/* A table cell ignores max-width under auto layout, so the box inside truncates. */}
<span className="block max-w-[180px] truncate" title={userLabel(e)}>{userLabel(e)}</span>
</td>
<td className={`${TD} whitespace-nowrap`}>
<span className={`${CODE} inline-block rounded-[6px] bg-surface-tertiary px-1.5 py-[1px] text-content`}>{e.action}</span>
</td>
<td className={`${TD} ${CODE} max-w-[160px] break-all text-content-muted`}>{e.resource || '—'}</td>
<td className={`${TD} ${CODE} whitespace-nowrap text-content-muted`}>{e.ip || '—'}</td>
<td className={`${TD} ${CODE} max-w-[300px] break-all text-content-faint`}>{fmtDetails(e.details)}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
return (
<SettingsCard
icon={ClipboardList}
title={t('admin.tabs.audit')}
hint={t('admin.audit.subtitle')}
badge={<StatusPill>{t('admin.audit.showing', { count: entries.length, total })}</StatusPill>}
action={
<button
type="button"
disabled={loading}
onClick={() => loadFirstPage()}
className={SETTINGS_BUTTON}
style={fs(12.5, 'body')}
>
<RefreshCw size={14} strokeWidth={2.2} className={loading ? 'animate-spin' : ''} />
{t('admin.audit.refresh')}
</button>
}
>
{body}
{entries.length < total && (
<div className="flex justify-center">
<button
type="button"
disabled={loading}
onClick={() => loadMore()}
className={SETTINGS_BUTTON}
style={fs(12.5, 'body')}
>
{t('admin.audit.loadMore')}
</button>
</div>
)}
</SettingsCard>
)
}