Files
Joshen Lim e3fec137fe Joshenlim/fe 4466 audit logs update organization logs as well (#50935)
## Context

Follows up from [this
PR](https://github.com/supabase/supabase/pull/50799) - updates the Audit
Logs UI for organization audit logs. Just differs from Account audit
logs slightly with added "Actor" + "Target" column

Reuses the same UI components from account audit logs so quite a bit of
code clean up 🙂
<img width="1451" height="955" alt="image"
src="https://github.com/user-attachments/assets/a1002cee-917f-4d9a-b977-3fab8ecd2d13"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Organization audit logs now use a sortable table with row selection
and a resizable details panel.
* Actor details show a member’s username when available, otherwise the
actor’s email; a dash appears when neither is available.
* Logs can be filtered by users and projects, with separate messages for
no logs and no matching results.
* A refresh control and loading indicators help show audit-log updates.
* **Bug Fixes**
* Organization project lists stop loading when pagination totals are
unavailable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 17:41:54 +08:00

101 lines
3.5 KiB
TypeScript

import { flexRender, type Table as TanStackTable } from '@tanstack/react-table'
import { cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import { formatCompactNumber } from '@/components/ui/DataTable/DataTable.utils'
import type { AuditLog } from '@/data/organizations/organization-audit-logs-query'
interface AuditLogColumnMeta {
cellClassName?: string
headerClassName?: string
}
interface AuditLogsTableProps {
table: TanStackTable<AuditLog>
selectedLog?: AuditLog
onSelectLog: (log: AuditLog) => void
}
export const AuditLogsTable = ({ table, selectedLog, onSelectLog }: AuditLogsTableProps) => {
const rows = table.getRowModel().rows
return (
<Table
className="table-fixed"
containerProps={{
containerClassName: 'h-full',
className: 'h-full w-full overflow-auto @container',
}}
>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="border-b-0! hover:bg-transparent">
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
className={cn(
'sticky top-0 z-10 bg-surface-100',
'[border-bottom:none]! [box-shadow:inset_0_-1px_0_var(--border-default)]!',
(header.column.columnDef.meta as AuditLogColumnMeta | undefined)?.headerClassName,
'px-2 first:pl-3'
)}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{rows.map((row) => (
<TableRow
key={row.id}
tabIndex={0}
aria-selected={selectedLog?.request_id === row.original.request_id}
onClick={() => onSelectLog(row.original)}
onKeyDown={(e) => {
if (e.target !== e.currentTarget) return
if (e.key === 'Enter') {
onSelectLog(row.original)
} else if (e.key === ' ') {
e.preventDefault()
onSelectLog(row.original)
}
}}
className={cn(
'cursor-pointer hover:bg-selection!',
selectedLog?.request_id === row.original.request_id && 'bg-selection!'
)}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={cn(
'py-2 px-2 text-xs first:pl-3',
(cell.column.columnDef.meta as AuditLogColumnMeta | undefined)?.cellClassName
)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
{rows.length > 0 && (
<TableRow className="hover:bg-transparent">
<TableCell colSpan={table.getAllLeafColumns().length} className="p-0! overflow-visible">
<div className="sticky left-0 w-[100cqw] py-2 text-center">
<p className="text-xs text-foreground-lighter">
Viewing{' '}
<span className="font-mono font-medium">{formatCompactNumber(rows.length)}</span>{' '}
logs
</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)
}