GodSpeed / frontend /src /components /admin /AdminAuditLog.tsx
AdithyaVardan's picture
fix: type errors, merge OAuth branch, upgrade BM25Store
e2766a5
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { apiFetch } from '@/lib/http'
import { AuditLogEntry } from '@/types/settings'
import { LoadingSkeleton } from '../common/LoadingSkeleton'
const ACTION_ICONS: Record<string, string> = {
grant_channel: '✅',
revoke_channel: '❌',
change_role: '👤',
invite_user: '📨',
deactivate_user: '🚫',
query_executed: '🔍',
document_accessed: '📄',
bulk_user_import: '📥',
}
const ACTION_COLORS: Record<string, string> = {
grant_channel: 'text-green-600 dark:text-green-400',
revoke_channel: 'text-red-600 dark:text-red-400',
change_role: 'text-blue-600 dark:text-blue-400',
invite_user: 'text-purple-600 dark:text-purple-400',
deactivate_user: 'text-red-600 dark:text-red-400',
query_executed: 'text-stone-600 dark:text-stone-400',
document_accessed: 'text-stone-600 dark:text-stone-400',
bulk_user_import: 'text-green-600 dark:text-green-400',
}
export function AdminAuditLog() {
const [filterAction, setFilterAction] = useState<string>('all')
const [filterType, setFilterType] = useState<string>('all')
// Fetch audit log
const { data: auditEntries, isLoading, refetch } = useQuery({
queryKey: ['audit-log', filterAction, filterType],
queryFn: async () => {
// TODO: GET /api/audit-log?action=X&target_type=Y
return [] as AuditLogEntry[]
},
})
const allActions = [
'grant_channel',
'revoke_channel',
'change_role',
'invite_user',
'deactivate_user',
'query_executed',
'document_accessed',
'bulk_user_import',
]
const allTargetTypes = ['user', 'channel', 'team', 'query', 'document']
const handleExportCSV = () => {
// TODO: GET /api/audit-log/export?format=csv
console.log('Export audit log as CSV')
}
return (
<div className="space-y-6 p-6">
{/* Filters & Export */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div className="flex gap-2">
<div>
<label className="block text-xs font-medium text-stone-700 dark:text-stone-300">
Filter by Action
</label>
<select
value={filterAction}
onChange={(e) => setFilterAction(e.target.value)}
className="mt-1 rounded border border-stone-300 bg-white px-2 py-1 text-xs text-stone-900 focus:border-brand focus:outline-none dark:border-stone-600 dark:bg-stone-800 dark:text-white"
>
<option value="all">All Actions</option>
{allActions.map((action) => (
<option key={action} value={action}>
{action.replace(/_/g, ' ')}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-stone-700 dark:text-stone-300">
Filter by Target
</label>
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className="mt-1 rounded border border-stone-300 bg-white px-2 py-1 text-xs text-stone-900 focus:border-brand focus:outline-none dark:border-stone-600 dark:bg-stone-800 dark:text-white"
>
<option value="all">All Types</option>
{allTargetTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
</div>
<button
onClick={handleExportCSV}
className="rounded border border-stone-300 px-3 py-1.5 text-xs font-medium text-stone-700 hover:bg-stone-50 dark:border-stone-600 dark:text-stone-300 dark:hover:bg-stone-800"
>
📊 Export as CSV
</button>
</div>
{/* Audit Log Entries */}
<div>
<h3 className="mb-3 text-sm font-semibold">Recent Activity</h3>
{isLoading ? (
<LoadingSkeleton rows={5} className="h-12" />
) : auditEntries && auditEntries.length > 0 ? (
<div className="space-y-2 rounded-lg border border-stone-200 dark:border-stone-700">
{auditEntries.map((entry) => (
<div
key={entry.id}
className="flex items-start gap-3 border-b border-stone-200 p-3 last:border-b-0 dark:border-stone-700"
>
<span className={`text-lg ${ACTION_COLORS[entry.action] || 'text-stone-500'}`}>
{ACTION_ICONS[entry.action] || '•'}
</span>
<div className="flex-1 min-w-0">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-sm font-medium text-stone-900 dark:text-white">
{entry.action.replace(/_/g, ' ')}
</p>
<p className="text-xs text-stone-600 dark:text-stone-400">
{entry.actor_name && `by ${entry.actor_name}`}
{entry.actor_name && entry.target_name && ' • '}
{entry.target_name && `on ${entry.target_name}`}
</p>
</div>
<span className="text-xs text-stone-500 dark:text-stone-500 whitespace-nowrap">
{new Date(entry.created_at).toLocaleDateString()} {new Date(entry.created_at).toLocaleTimeString()}
</span>
</div>
{/* Metadata if available */}
{Object.keys(entry.metadata).length > 0 && (
<details className="mt-2">
<summary className="cursor-pointer text-xs font-medium text-stone-500 hover:text-stone-700 dark:hover:text-stone-300">
Details
</summary>
<pre className="mt-1 overflow-x-auto rounded bg-stone-100 p-2 text-xs text-stone-700 dark:bg-stone-800 dark:text-stone-300">
{JSON.stringify(entry.metadata, null, 2)}
</pre>
</details>
)}
</div>
</div>
))}
</div>
) : (
<div className="rounded-lg border-2 border-dashed border-stone-300 p-8 text-center dark:border-stone-600">
<p className="text-sm text-stone-500">No audit log entries yet</p>
<p className="mt-1 text-xs text-stone-400">
Actions will appear here as users and admins interact with the system
</p>
</div>
)}
</div>
{/* Legend */}
<div className="rounded-lg border border-stone-200 bg-stone-50 p-4 dark:border-stone-700 dark:bg-stone-900">
<h4 className="text-xs font-semibold text-stone-900 dark:text-white">What's tracked?</h4>
<ul className="mt-2 space-y-1 text-xs text-stone-600 dark:text-stone-400">
<li><strong>User actions:</strong> invite, deactivate, role changes</li>
<li><strong>Channel access:</strong> grants and revokes</li>
<li><strong>Bulk operations:</strong> CSV imports, bulk role updates</li>
<li><strong>Query activity:</strong> who searched what, when</li>
<li><strong>Document access:</strong> restricted document views (compliance)</li>
</ul>
</div>
</div>
)
}