This commit is contained in:
1 parent
21a61068fb
commit
60278b9e71
38 files changed
+3452
No files matched your search
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { DataTable } from '@/components/admin/data-table'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { DataTableColumn, PaginatedResult } from '@/types'
|
||||
|
||||
interface AuditRow {
|
||||
id: number
|
||||
username: string
|
||||
action: string
|
||||
target: string
|
||||
targetId: number | null
|
||||
diff: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
function DiffCell({ diff }: { diff: string | null }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
if (!diff) return <span className="text-muted-foreground">-</span>
|
||||
|
||||
let parsed: Record<string, { from: unknown; to: unknown }>
|
||||
try {
|
||||
parsed = JSON.parse(diff)
|
||||
} catch {
|
||||
return <span className="text-muted-foreground">-</span>
|
||||
}
|
||||
|
||||
const keys = Object.keys(parsed)
|
||||
if (keys.length === 0) return <span className="text-muted-foreground">-</span>
|
||||
|
||||
const summary =
|
||||
keys.length === 1
|
||||
? `${keys[0]}: ${String(parsed[keys[0]].from ?? '∅')} → ${String(parsed[keys[0]].to ?? '∅')}`
|
||||
: `${keys.length} changes`
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto p-0 text-xs font-normal"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{summary}
|
||||
</Button>
|
||||
{open && (
|
||||
<div className="mt-1 space-y-0.5 text-xs">
|
||||
{keys.map((k) => (
|
||||
<div key={k} className="flex gap-1">
|
||||
<span className="font-medium">{k}:</span>
|
||||
<span className="text-[var(--admin-error)] line-through">{String(parsed[k].from ?? '∅')}</span>
|
||||
<span>→</span>
|
||||
<span className="text-[var(--admin-success)]">{String(parsed[k].to ?? '∅')}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const columns: DataTableColumn<AuditRow>[] = [
|
||||
{ key: 'id', label: 'ID', sortable: true },
|
||||
{ key: 'username', label: 'User', sortable: true },
|
||||
{
|
||||
key: 'action',
|
||||
label: 'Action',
|
||||
sortable: true,
|
||||
render: (value) => {
|
||||
const v = String(value)
|
||||
let variant: 'default' | 'secondary' | 'destructive' = 'secondary'
|
||||
if (v.includes('create') || v.includes('add')) variant = 'default'
|
||||
if (v.includes('delete') || v.includes('remove') || v.includes('ban')) variant = 'destructive'
|
||||
return <Badge variant={variant}>{v}</Badge>
|
||||
},
|
||||
},
|
||||
{ key: 'target', label: 'Target', sortable: true },
|
||||
{
|
||||
key: 'targetId',
|
||||
label: 'Target ID',
|
||||
render: (value) => <span>{value != null ? String(value) : '-'}</span>,
|
||||
},
|
||||
{
|
||||
key: 'diff',
|
||||
label: 'Details',
|
||||
render: (_value, row) => <DiffCell diff={row.diff} />,
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
label: 'Date',
|
||||
sortable: true,
|
||||
render: (value) => <span>{String(value) || 'N/A'}</span>,
|
||||
},
|
||||
]
|
||||
|
||||
interface AuditTableProps {
|
||||
data: PaginatedResult<AuditRow>
|
||||
}
|
||||
|
||||
export function AuditTable({ data }: AuditTableProps) {
|
||||
return (
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
searchPlaceholder="Search by action or target..."
|
||||
exportUrl="/api/admin/export?type=audit"
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user