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

112 lines
4.0 KiB
TypeScript

import dayjs from 'dayjs'
import { Activity, Clock, Copy, Globe, Hash, User } from 'lucide-react'
import { Button, cn, copyToClipboard } from 'ui'
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
import { getStatusLevel } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.utils'
import { CollapsibleDetailSection } from '@/components/ui/DataTable/CollapsibleDetailSection'
import { DataTableColumnStatusCode } from '@/components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode'
import { DetailSectionHeader } from '@/components/ui/DataTable/DetailSectionHeader'
import {
TIMESTAMP_MICROS_PER_MS,
type AuditLog,
} from '@/data/organizations/organization-audit-logs-query'
interface AuditLogOverviewProps {
selectedLog: AuditLog
}
const DetailRow = ({
label,
value,
}: {
label: string
value: string | number | undefined | null
}) => {
const isEmpty = value === null || value === undefined || value === ''
return (
<div className="flex h-9 items-center justify-between gap-x-10 px-4 pl-[38px]">
<span className="shrink-0 text-sm text-foreground-lighter">{label}</span>
<div
className={cn('flex items-center gap-x-2 min-w-0 flex-1 justify-end', isEmpty && 'pr-2')}
>
{isEmpty ? (
<span className="font-mono text-xs text-foreground-muted">—</span>
) : (
<span className="truncate font-mono text-xs text-foreground text-right">{value}</span>
)}
{!isEmpty && (
<Button
aria-label={`Copy ${label}`}
variant="text"
className="px-1"
icon={<Copy size={12} />}
onClick={() => copyToClipboard(String(value))}
/>
)}
</div>
</div>
)
}
export const AuditLogOverview = ({ selectedLog }: AuditLogOverviewProps) => {
const hasMetadata = Object.keys(selectedLog.action.metadata ?? {}).length > 0
return (
<div>
<DetailSectionHeader
title="Request started"
icon={Clock}
className="border-b"
summary={
<TimestampInfo
className="text-xs font-mono"
utcTimestamp={dayjs(selectedLog.timestamp / TIMESTAMP_MICROS_PER_MS).toISOString()}
/>
}
/>
<DetailSectionHeader
title="Request ID"
icon={Hash}
className="border-b"
summary={
<span className="truncate text-right font-mono text-xs text-foreground">
{selectedLog.request_id}
</span>
}
/>
<CollapsibleDetailSection defaultOpen className="border-b" title="Target" icon={Globe}>
<DetailRow label="Organization slug" value={selectedLog.organization_slug} />
<DetailRow label="Project ref" value={selectedLog.project_ref} />
</CollapsibleDetailSection>
<CollapsibleDetailSection defaultOpen className="border-b" title="Actor" icon={User}>
<DetailRow label="Token type" value={selectedLog.actor.token_type} />
<DetailRow label="Email" value={selectedLog.actor.email} />
<DetailRow label="User ID" value={selectedLog.actor.user_id} />
<DetailRow label="IP address" value={selectedLog.actor.ip} />
</CollapsibleDetailSection>
<CollapsibleDetailSection defaultOpen className="border-b" title="Action" icon={Activity}>
<DetailRow label="Name" value={selectedLog.action.name} />
<DetailRow label="Method" value={selectedLog.action.method} />
<DetailRow label="Route" value={selectedLog.action.route} />
<div className="flex h-9 items-center justify-between gap-x-10 px-4 pl-[38px]">
<span className="shrink-0 text-sm text-foreground-lighter">Status</span>
<DataTableColumnStatusCode
value={selectedLog.action.status}
level={getStatusLevel(selectedLog.action.status)}
className="text-xs"
/>
</div>
<DetailRow
label="Metadata"
value={hasMetadata ? JSON.stringify(selectedLog.action.metadata) : undefined}
/>
</CollapsibleDetailSection>
</div>
)
}