Initial project structure cleanup
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Download,
|
||||
Search,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
|
||||
import {
|
||||
clearActivityLogs,
|
||||
exportActivityLogs,
|
||||
getActivityLogs,
|
||||
} from '@/services/activityLogService';
|
||||
|
||||
import type {
|
||||
ActivityLogEntry,
|
||||
ActivityLogLevel,
|
||||
ActivityLogSource,
|
||||
} from '@/types/activity';
|
||||
|
||||
const levels: Array<'all' | ActivityLogLevel> = [
|
||||
'all',
|
||||
'info',
|
||||
'success',
|
||||
'warning',
|
||||
'error',
|
||||
];
|
||||
|
||||
const sources: Array<'all' | ActivityLogSource> = [
|
||||
'all',
|
||||
'desktop',
|
||||
'router',
|
||||
'backend',
|
||||
'vps',
|
||||
];
|
||||
|
||||
function levelTone(level: ActivityLogLevel) {
|
||||
if (level === 'success') return 'green';
|
||||
if (level === 'warning') return 'purple';
|
||||
if (level === 'error') return 'red';
|
||||
|
||||
return 'blue';
|
||||
}
|
||||
|
||||
export function ActivityLogs() {
|
||||
const [logs, setLogs] = useState<
|
||||
ActivityLogEntry[]
|
||||
>([]);
|
||||
|
||||
const [levelFilter, setLevelFilter] =
|
||||
useState<'all' | ActivityLogLevel>('all');
|
||||
|
||||
const [sourceFilter, setSourceFilter] =
|
||||
useState<'all' | ActivityLogSource>('all');
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
function refreshLogs() {
|
||||
setLogs(getActivityLogs());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshLogs();
|
||||
|
||||
window.addEventListener(
|
||||
'activity-log-added',
|
||||
refreshLogs,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
'activity-log-added',
|
||||
refreshLogs,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filteredLogs = useMemo(() => {
|
||||
const normalizedQuery =
|
||||
query.trim().toLowerCase();
|
||||
|
||||
return logs.filter((log) => {
|
||||
const matchesLevel =
|
||||
levelFilter === 'all' ||
|
||||
log.level === levelFilter;
|
||||
|
||||
const matchesSource =
|
||||
sourceFilter === 'all' ||
|
||||
log.source === sourceFilter;
|
||||
|
||||
const matchesQuery =
|
||||
!normalizedQuery ||
|
||||
[
|
||||
log.action,
|
||||
log.message,
|
||||
log.routerIp,
|
||||
log.vpnIp,
|
||||
log.source,
|
||||
log.level,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(normalizedQuery);
|
||||
|
||||
return (
|
||||
matchesLevel &&
|
||||
matchesSource &&
|
||||
matchesQuery
|
||||
);
|
||||
});
|
||||
}, [logs, levelFilter, sourceFilter, query]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white">
|
||||
Activity Logs
|
||||
</h2>
|
||||
|
||||
<p className="mt-1 text-slate-400">
|
||||
Local provisioning audit trail for
|
||||
technicians.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
exportActivityLogs();
|
||||
}}
|
||||
>
|
||||
<Download size={16} />
|
||||
Export JSON
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
clearActivityLogs();
|
||||
refreshLogs();
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Clear Logs
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="relative z-30 mb-4 overflow-visible">
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-6">
|
||||
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
Search
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-2 rounded-xl border border-white/10 bg-ink-950 px-3">
|
||||
<Search
|
||||
size={16}
|
||||
className="text-slate-500"
|
||||
/>
|
||||
|
||||
<input
|
||||
value={query}
|
||||
onChange={(event) =>
|
||||
setQuery(event.target.value)
|
||||
}
|
||||
placeholder="Search action, VPN IP, router IP, message..."
|
||||
className="w-full bg-transparent py-3 text-sm text-white outline-none placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-3">
|
||||
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
Level
|
||||
</label>
|
||||
|
||||
<Select
|
||||
value={levelFilter}
|
||||
onChange={setLevelFilter}
|
||||
options={levels.map((level) => ({
|
||||
value: level,
|
||||
label: level,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-3">
|
||||
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
Source
|
||||
</label>
|
||||
|
||||
<Select
|
||||
value={sourceFilter}
|
||||
onChange={setSourceFilter}
|
||||
options={sources.map((source) => ({
|
||||
value: source,
|
||||
label: source,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-white">
|
||||
Audit Events
|
||||
</h3>
|
||||
|
||||
<span className="text-sm text-slate-500">
|
||||
{filteredLogs.length} shown /{' '}
|
||||
{logs.length} total
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto rounded-xl border border-white/10
|
||||
[scrollbar-width:thin]
|
||||
[scrollbar-color:rgba(59,130,246,0.45)_transparent]
|
||||
[&::-webkit-scrollbar]:w-2
|
||||
[&::-webkit-scrollbar-track]:bg-transparent
|
||||
[&::-webkit-scrollbar-thumb]:rounded-full
|
||||
[&::-webkit-scrollbar-thumb]:bg-blue-500/30
|
||||
hover:[&::-webkit-scrollbar-thumb]:bg-blue-500/50">
|
||||
<table className="w-full table-fixed text-sm">
|
||||
<thead className="sticky top-0 z-10 bg-slate-950 text-left text-xs uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th className="w-[180px] px-4 py-3">
|
||||
Time
|
||||
</th>
|
||||
|
||||
<th className="w-[110px] px-4 py-3">
|
||||
Level
|
||||
</th>
|
||||
|
||||
<th className="w-[120px] px-4 py-3">
|
||||
Source
|
||||
</th>
|
||||
|
||||
<th className="w-[190px] px-4 py-3">
|
||||
Action
|
||||
</th>
|
||||
|
||||
<th className="px-4 py-3">
|
||||
Message
|
||||
</th>
|
||||
|
||||
<th className="w-[160px] px-4 py-3">
|
||||
VPN IP
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody className="divide-y divide-white/10">
|
||||
{filteredLogs.map((log) => (
|
||||
<tr
|
||||
key={log.id}
|
||||
className="bg-white/[0.01] hover:bg-white/[0.04]"
|
||||
>
|
||||
<td className="px-4 py-3 font-mono text-xs text-slate-400">
|
||||
{new Date(
|
||||
log.timestamp,
|
||||
).toLocaleString()}
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3">
|
||||
<Badge tone={levelTone(log.level)}>
|
||||
{log.level}
|
||||
</Badge>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 text-slate-300">
|
||||
{log.source}
|
||||
</td>
|
||||
|
||||
<td className="truncate px-4 py-3 font-medium text-white">
|
||||
{log.action}
|
||||
</td>
|
||||
|
||||
<td className="truncate px-4 py-3 text-slate-400">
|
||||
{log.message}
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 font-mono text-slate-300">
|
||||
{log.vpnIp ?? '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{filteredLogs.length === 0 && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
className="px-4 py-10 text-center text-slate-500"
|
||||
>
|
||||
No activity logs found.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user