feat(phase2): bulk delete + audit log; update roadmap/parity docs
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../lib/api.ts';
|
||||
import { useServerStore } from '../stores/server.ts';
|
||||
|
||||
interface ServerSettingsModalProps {
|
||||
serverId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface AuditEntry {
|
||||
id: string;
|
||||
server_id: string;
|
||||
user_id: string | null;
|
||||
username: string | null;
|
||||
action_type: string;
|
||||
target_type: string | null;
|
||||
target_id: string | null;
|
||||
reason: string | null;
|
||||
changes: unknown;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
KICK: 'kicked member',
|
||||
BAN: 'banned member',
|
||||
UNBAN: 'unbanned member',
|
||||
MUTE: 'muted member',
|
||||
UNMUTE: 'unmuted member',
|
||||
ROLE_CREATE: 'created role',
|
||||
ROLE_UPDATE: 'updated role',
|
||||
ROLE_DELETE: 'deleted role',
|
||||
MEMBER_ROLES_UPDATE: 'updated member roles',
|
||||
CHANNEL_CREATE: 'created channel',
|
||||
CHANNEL_UPDATE: 'updated channel',
|
||||
CHANNEL_DELETE: 'deleted channel',
|
||||
SERVER_UPDATE: 'updated server',
|
||||
MESSAGE_DELETE: 'deleted message',
|
||||
BULK_DELETE: 'bulk deleted messages',
|
||||
};
|
||||
|
||||
export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalProps) {
|
||||
const [tab, setTab] = useState<'audit'>('audit');
|
||||
const [entries, setEntries] = useState<AuditEntry[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const servers = useServerStore((s) => s.servers);
|
||||
const server = servers.find((s) => s.id === serverId);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab !== 'audit') return;
|
||||
setLoading(true);
|
||||
api.get<AuditEntry[]>(`/servers/${serverId}/audit-log`)
|
||||
.then((data) => setEntries(Array.isArray(data) ? data : []))
|
||||
.catch((err) => setError(err instanceof Error ? err.message : 'Failed to load audit log'))
|
||||
.finally(() => setLoading(false));
|
||||
}, [tab, serverId]);
|
||||
|
||||
const formatTime = (iso: string) => {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-gb-bg border border-gb-bg-t w-[700px] max-h-[80vh] flex flex-col font-mono"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gb-bg-t">
|
||||
<span className="text-sm text-gb-orange">SERVER SETTINGS: {server?.name ?? serverId}</span>
|
||||
<button onClick={onClose} className="text-xs text-gb-red">[x]</button>
|
||||
</div>
|
||||
<div className="flex border-b border-gb-bg-t">
|
||||
<button
|
||||
onClick={() => setTab('audit')}
|
||||
className={`px-4 py-2 text-xs ${tab === 'audit' ? 'text-gb-orange bg-gb-bg-s' : 'text-gb-fg-f hover:text-gb-orange'}`}
|
||||
>
|
||||
[AUDIT LOG]
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{tab === 'audit' && (
|
||||
<>
|
||||
{loading && <p className="text-gb-fg-f text-xs">[loading...]</p>}
|
||||
{error && <p className="text-gb-red text-xs">ERR: {error}</p>}
|
||||
{!loading && !error && entries.length === 0 && (
|
||||
<p className="text-gb-fg-f text-xs">[no audit log entries]</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{entries.map((entry) => (
|
||||
<div key={entry.id} className="text-xs border border-gb-bg-t p-2 bg-gb-bg-s">
|
||||
<div className="flex items-center justify-between text-gb-fg-f">
|
||||
<span>{formatTime(entry.created_at)}</span>
|
||||
<span className="text-gb-orange">{entry.action_type}</span>
|
||||
</div>
|
||||
<div className="text-gb-fg mt-1">
|
||||
<span className="text-gb-aqua">{entry.username ?? entry.user_id ?? 'system'}</span>
|
||||
{' '}<span className="text-gb-fg-s">{ACTION_LABELS[entry.action_type] ?? entry.action_type}</span>
|
||||
{entry.target_id && (
|
||||
<span className="text-gb-fg-f"> target:{entry.target_id}</span>
|
||||
)}
|
||||
</div>
|
||||
{entry.reason && (
|
||||
<div className="text-gb-fg-f mt-1">reason: {entry.reason}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user