mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## 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 -->
104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import { describe, expect, test } from 'vitest'
|
|
|
|
import { filterByProjects } from '@/components/interfaces/AuditLogs/AuditLogs.utils'
|
|
import { filterByUsers } from '@/components/interfaces/Organization/AuditLogs/AuditLogs.utils'
|
|
import {
|
|
TIMESTAMP_MICROS_PER_MS,
|
|
type AuditLog,
|
|
} from '@/data/organizations/organization-audit-logs-query'
|
|
|
|
// Timestamps are in microseconds (e.g. 1777471903844000 = April 2026)
|
|
const TS_A = 1777471903844000
|
|
const TS_B = 1777471903845000
|
|
const TS_C = 1777471903846000
|
|
|
|
function makeLog(overrides: Partial<AuditLog> = {}): AuditLog {
|
|
return {
|
|
timestamp: TS_A,
|
|
request_id: 'req-1',
|
|
action: { name: 'test', method: 'GET', route: '/api/test', status: 200 },
|
|
actor: { token_type: 'bearer' },
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
const logA = makeLog({ timestamp: TS_A, request_id: 'req-a', project_ref: 'proj-1' })
|
|
const logB = makeLog({
|
|
timestamp: TS_B,
|
|
request_id: 'req-b',
|
|
project_ref: 'proj-2',
|
|
actor: { token_type: 'bearer', user_id: 'user-1' },
|
|
})
|
|
const logC = makeLog({
|
|
timestamp: TS_C,
|
|
request_id: 'req-c',
|
|
actor: { token_type: 'bearer', user_id: 'user-2' },
|
|
})
|
|
|
|
describe('timestamp conversion (microseconds → milliseconds)', () => {
|
|
test('dividing by 1000 produces a valid date', () => {
|
|
expect(dayjs(TS_A / TIMESTAMP_MICROS_PER_MS).isValid()).toBe(true)
|
|
})
|
|
|
|
test('produces the correct year', () => {
|
|
expect(dayjs(TS_A / TIMESTAMP_MICROS_PER_MS).year()).toBe(2026)
|
|
})
|
|
|
|
test('dayjs.unix on a microsecond value produces an invalid date', () => {
|
|
// Guard: confirms the bug we fixed — dayjs.unix() treats the value as seconds,
|
|
// overflowing JS Date's max and producing "Invalid Date"
|
|
expect(dayjs.unix(TS_A).isValid()).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('filterByUsers', () => {
|
|
test('returns all logs when filter list is empty', () => {
|
|
expect(filterByUsers([logA, logB, logC], [])).toEqual([logA, logB, logC])
|
|
})
|
|
|
|
test('filters to matching user_id', () => {
|
|
const result = filterByUsers([logA, logB, logC], ['user-1'])
|
|
expect(result.map((l) => l.request_id)).toEqual(['req-b'])
|
|
})
|
|
|
|
test('filters to multiple user_ids', () => {
|
|
const result = filterByUsers([logA, logB, logC], ['user-1', 'user-2'])
|
|
expect(result.map((l) => l.request_id)).toEqual(['req-b', 'req-c'])
|
|
})
|
|
|
|
test('excludes logs with no user_id when filtering', () => {
|
|
const result = filterByUsers([logA, logB], ['user-1'])
|
|
expect(result.map((l) => l.request_id)).toEqual(['req-b'])
|
|
})
|
|
|
|
test('returns empty array when no logs match', () => {
|
|
expect(filterByUsers([logA, logB, logC], ['user-unknown'])).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('filterByProjects', () => {
|
|
test('returns all logs when filter list is empty', () => {
|
|
expect(filterByProjects([logA, logB, logC], [])).toEqual([logA, logB, logC])
|
|
})
|
|
|
|
test('filters to matching project_ref', () => {
|
|
const result = filterByProjects([logA, logB, logC], ['proj-1'])
|
|
expect(result.map((l) => l.request_id)).toEqual(['req-a'])
|
|
})
|
|
|
|
test('filters to multiple project_refs', () => {
|
|
const result = filterByProjects([logA, logB, logC], ['proj-1', 'proj-2'])
|
|
expect(result.map((l) => l.request_id)).toEqual(['req-a', 'req-b'])
|
|
})
|
|
|
|
test('excludes logs with no project_ref when filtering', () => {
|
|
const result = filterByProjects([logA, logC], ['proj-1'])
|
|
expect(result.map((l) => l.request_id)).toEqual(['req-a'])
|
|
})
|
|
|
|
test('returns empty array when no logs match', () => {
|
|
expect(filterByProjects([logA, logB, logC], ['proj-unknown'])).toEqual([])
|
|
})
|
|
})
|