mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 15:38:45 -07:00
7535cd823f
- Updated Sidebar component to consolidate navigation items under new categories: Moderation, Content, and Operations. - Removed outdated pages and integrated their functionalities into tabs under Players, Activity, Content, and Settings. - Introduced new Activity and Content pages to house Audit, Player Logs, Community Board, and Loading Tips with tabbed navigation. - Created a unified Moderation page combining Players, Bans, and Reports with tabbed navigation. - Refactored individual pages (e.g., Audit, Bans, CommunityBoard, etc.) to support embedded mode for better integration within modals. - Added Tabs component for consistent tab navigation across multiple pages. - Updated README to reflect changes in navigation and settings structure. - Removed Grants page as it is not yet implemented.
77 lines
3.3 KiB
TypeScript
77 lines
3.3 KiB
TypeScript
import { useState } from 'react';
|
|
import type { AuditEntry } from '../lib/types';
|
|
import { useApi } from '../lib/useApi';
|
|
import { PageHeader } from '../components/PageHeader';
|
|
import { Empty } from '../components/Empty';
|
|
import { absoluteTime, clip, relativeTime } from '../lib/format';
|
|
import { RefreshCw } from '../components/Icons';
|
|
|
|
export function Audit({ embedded }: { embedded?: boolean } = {}) {
|
|
const [take, setTake] = useState(200);
|
|
const { data, loading, error, refresh } = useApi<AuditEntry[]>(`/audit?take=${take}`);
|
|
|
|
const controls = (
|
|
<>
|
|
<select value={take} onChange={e => setTake(parseInt(e.target.value))} className="input w-24 text-xs !py-1.5">
|
|
<option value={100}>last 100</option>
|
|
<option value={200}>last 200</option>
|
|
<option value={500}>last 500</option>
|
|
</select>
|
|
<button onClick={refresh} className="btn-secondary text-xs" disabled={loading}>
|
|
<RefreshCw className={loading ? 'animate-spin' : ''} /> Refresh
|
|
</button>
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
{embedded ? (
|
|
<div className="flex justify-end gap-2 mb-3">{controls}</div>
|
|
) : (
|
|
<PageHeader
|
|
title="Audit log"
|
|
blurb="Every admin action with actor, target, and reason. Newest first."
|
|
actions={controls}
|
|
/>
|
|
)}
|
|
|
|
{error && <div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger">{error}</div>}
|
|
|
|
<div className="card overflow-hidden">
|
|
{data && data.length === 0 && <Empty title="No audit entries yet" />}
|
|
{data && data.length > 0 && (
|
|
<div className="table-scroll"><table className="w-full text-sm min-w-[640px]">
|
|
<thead className="text-[11px] uppercase tracking-wider text-ink-400 bg-ink-900/50 border-b border-ink-800 sticky top-0">
|
|
<tr>
|
|
<th className="text-left font-medium px-4 py-2.5">When</th>
|
|
<th className="text-left font-medium px-4 py-2.5">Admin</th>
|
|
<th className="text-left font-medium px-4 py-2.5">Action</th>
|
|
<th className="text-left font-medium px-4 py-2.5">Target</th>
|
|
<th className="text-left font-medium px-4 py-2.5">Reason</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-ink-800">
|
|
{data.map(e => (
|
|
<tr key={e.id} className="table-row-hover">
|
|
<td className="px-4 py-2 text-ink-300 text-xs whitespace-nowrap" title={absoluteTime(e.timestamp)}>
|
|
{relativeTime(e.timestamp)}
|
|
</td>
|
|
<td className="px-4 py-2 text-ink-200">#{e.adminPlayerId}</td>
|
|
<td className="px-4 py-2">
|
|
<span className="font-mono text-xs text-brand-200 bg-brand-500/10 px-1.5 py-0.5 rounded">{e.action}</span>
|
|
</td>
|
|
<td className="px-4 py-2 text-ink-200">
|
|
<span className="text-ink-400">{e.targetType}</span>
|
|
{e.targetId > 0 && <> · #{e.targetId}</>}
|
|
</td>
|
|
<td className="px-4 py-2 text-ink-300 text-xs" title={e.reason}>{clip(e.reason, 100)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table></div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|