Skip to content

feat: Audit logs table migration#5630

Open
MichaelUnkey wants to merge 11 commits intomainfrom
audit-logs-table-migration
Open

feat: Audit logs table migration#5630
MichaelUnkey wants to merge 11 commits intomainfrom
audit-logs-table-migration

Conversation

@MichaelUnkey
Copy link
Copy Markdown
Collaborator

@MichaelUnkey MichaelUnkey commented Apr 7, 2026

What does this PR do?

  • Migrates the audit logs table from the legacy VirtualTable component to the new DataTable system (@unkey/ui)
  • Replaces cursor-based infinite scroll with offset-based page pagination using PaginationFooter
  • Restructures audit log table code into the standardized feature-table directory layout at components/audit-logs-table/
  • Refactors the tRPC router to return { auditLogs, total } instead of { auditLogs, hasMore, nextCursor }, running count + data queries in parallel
  • Extracts cell renderers into dedicated components (ActorCell, AuditActionBadgeCell, MonoTextCell)

Fixes # (issue)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

  • Navigate to the Audit Logs page and verify the table loads with data
  • Verify pagination controls appear below the table and display correct page/total counts
  • Click through pages (1 → 2 → 3, etc.) and confirm navigation works without snapping back to page 1
  • Click a row and verify the log detail panel opens with correct data
  • With a row selected, verify unselected rows are dimmed and the selected row is highlighted with the correct status color (green for create, yellow for update, orange for delete)
  • Apply filters (events, users, root keys, time range) and verify:
    • Filtered results display correctly
    • Page resets to 1 when filters change
    • Removing filters restores unfiltered results
  • Verify the empty state displays when no logs match the applied filters
  • Verify skeleton loading rows appear while data is loading
  • Check the Actor column renders correctly for all actor types: user (shows name), key (shows Key icon + ID), other (shows function icon + ID)
  • Check the Action column badge colors: green for create/add events, yellow for update/edit events, orange for delete/remove events, gray for other
  • Resize the browser and verify the table is responsive on smaller viewports

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Ran make fmt on /go directory
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

  Replace VirtualTable with the shared DataTable system (@unkey/ui) for
  the audit logs page. Converts cursor-based infinite scroll to
  page-based
  pagination with PaginationFooter and prefetching. Moves
  feature-specific
  code to components/audit-logs-table/ following the established
  pattern.
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dashboard Ready Ready Preview, Comment Apr 9, 2026 2:29pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

This PR migrates the audit logs feature from cursor-based infinite-scroll pagination to explicit page-based pagination. It extracts reusable audit-logs components into a shared library at @/components/audit-logs-table, updates the tRPC backend to support offset/limit pagination with total counts, and refactors the frontend table UI from VirtualTable to DataTable with pagination controls.

Changes

Cohort / File(s) Summary
Audit Logs Query Hook
web/apps/dashboard/app/(app)/[workspaceSlug]/audit/components/table/hooks/use-logs-query.ts, web/apps/dashboard/components/audit-logs-table/hooks/use-audit-logs-query.ts
Removed old infinite-scroll hook; added new paginated hook that tracks URL page state, derives filter-driven filtersKey, resets page on filter changes, constructs AuditLogsQueryPayload, and prefetches next two pages via tRPC.
Schema & Type Definitions
web/apps/dashboard/components/audit-logs-table/schema/audit-logs.schema.ts, web/apps/dashboard/lib/trpc/routers/audit/schema.ts
Replaced auditQueryLogsPayload/AuditQueryLogsPayload with auditLogsQueryPayload/AuditLogsQueryPayload; changed from cursor-based to page-based pagination (added page field, removed cursor, updated defaults).
Backend Fetch Logic
web/apps/dashboard/lib/trpc/routers/audit/fetch.ts, web/apps/dashboard/lib/trpc/routers/audit/utils.ts
Replaced cursor-based pagination with offset/limit; introduced buildWhereConditions for filter predicate construction; added parallel COUNT() query for total count; updated response to include total instead of hasMore/nextCursor; adjusted schema references and transform logic.
Shared Component Library
web/apps/dashboard/components/audit-logs-table/columns/create-audit-log-columns.tsx, web/apps/dashboard/components/audit-logs-table/components/cells/..., web/apps/dashboard/components/audit-logs-table/components/empty-audit-logs.tsx, web/apps/dashboard/components/audit-logs-table/components/skeletons/render-audit-log-skeleton-row.tsx, web/apps/dashboard/components/audit-logs-table/index.ts
Created new reusable audit-logs-table library with column definitions, specialized cell renderers (actor, action badge, mono text), empty state, skeleton rows, and barrel export aggregating utilities and components.
Audit Logs Table UI
web/apps/dashboard/app/(app)/[workspaceSlug]/audit/components/table/logs-table.tsx, web/apps/dashboard/app/(app)/[workspaceSlug]/audit/components/table/log-details/components/log-header.tsx
Migrated table from VirtualTable (infinite scroll) to DataTable (page-based pagination); removed local column definitions; imported shared components/utilities from @/components/audit-logs-table; added PaginationFooter and effect to clear selection on log removal; updated cell styling via getAuditRowClassName(log, selectedLog).
Row Styling Utility
web/apps/dashboard/components/audit-logs-table/utils/get-row-class.ts
Updated getAuditRowClassName signature to accept `selectedLog: AuditLog

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ReactUI as React UI<br/>(DataTable)
    participant Hook as useAuditLogsQuery<br/>(Paginated)
    participant tRPC as tRPC Router<br/>(audit.logs)
    participant DB as Database

    User->>ReactUI: Navigate to page/change filters
    ReactUI->>Hook: Call hook (pageSize=50)
    Hook->>Hook: Derive filtersKey from filters
    Hook->>Hook: Reset page=1 if filters changed
    Hook->>Hook: Build AuditLogsQueryPayload
    Hook->>tRPC: useQuery with {page, limit, ...filters}
    tRPC->>DB: buildWhereConditions (workspace/bucket/event/actor/time)
    DB->>DB: Execute COUNT() for total
    DB->>DB: Fetch rows with LIMIT+OFFSET
    DB-->>tRPC: Return {logs[], total}
    tRPC-->>Hook: Receive logs + totalCount
    Hook->>Hook: Compute totalPages
    Hook->>Hook: Clamp page if out of range
    Hook->>tRPC: Prefetch pages+1 and +2
    Hook-->>ReactUI: Return {auditLogs, page, pageSize, totalPages, isLoading, onPageChange}
    ReactUI->>ReactUI: Render DataTable + PaginationFooter
    User->>ReactUI: Click next page
    ReactUI->>Hook: onPageChange(newPage)
    Hook->>Hook: Update URL query param
    loop Re-executes for new page
        Hook->>tRPC: useQuery with new {page, limit, ...filters}
        tRPC->>DB: Execute with offset=(page-1)*limit
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • fix: audit log filters use search params #2769: Modifies audit logs backend routing and query parameter handling (updates to apps/dashboard/lib/trpc/routers/audit/fetch.ts and pagination schema), aligning with this PR's pagination refactor from cursor-based to page-based.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Audit logs table migration' clearly summarizes the main change—migrating the audit logs table from VirtualTable to DataTable. It is concise, specific, and directly related to the primary objective of the changeset.
Description check ✅ Passed The PR description is comprehensive and complete. It includes a clear summary of changes, properly marked type of change (New feature), detailed testing instructions covering all key scenarios, and all required checklist items are checked.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit-logs-table-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/audit/components/table/logs-table.tsx:
- Around line 22-23: The selectedLog is not cleared when pagination or filters
change, so add logic to reset it whenever the currently loaded auditLogs no
longer contain the selectedLog.auditLog.id: in the component that calls
useAuditLogsQuery(), create/modify a useEffect (or equivalent state-sync) that
watches [auditLogs, selectedLog] and calls setSelectedLog(null) (or the
component's deselect function) when selectedLog is defined but
auditLogs.some(log => log.id === selectedLog.auditLog.id) is false; apply the
same fix for the other selection-check block around lines 34-45 so selection is
cleared any time the current page's row set does not include the previously
selected log.

In
`@web/apps/dashboard/components/audit-logs-table/components/cells/actor-cell.tsx`:
- Around line 10-19: The Actor cell can be empty when user.firstName and
user.lastName are missing; update the isUser rendering in actor-cell.tsx to show
a fallback label that first tries `${user.firstName ?? ""} ${user.lastName ??
""}` but if that is blank uses `user.username`, and if that is also missing uses
`log.auditLog.actor.id` (preserving truncation/whitespace handling and the same
span/className), so the displayed value always falls back to username then actor
id.

In
`@web/apps/dashboard/components/audit-logs-table/hooks/use-audit-logs-query.ts`:
- Around line 20-25: The effect that resets page uses setPage(1) but the
component render still builds queryParams with the old page causing a stale
query; fix by deriving an effectivePage variable that equals 1 when
prevFiltersKeyRef.current !== filtersKey (and otherwise equals page) and use
effectivePage when constructing queryParams and calling useQuery/prefetch, then
let the existing useEffect call setPage(1) to sync the URL/state after render;
update references in this file to use effectivePage wherever queryParams, the
useQuery hook, and the prefetch logic (the code around prevFiltersKeyRef,
filtersKey, setPage, queryParams, and useQuery) are built so no stale request is
emitted.

In `@web/apps/dashboard/lib/trpc/routers/audit/fetch.ts`:
- Around line 49-56: The current offset pagination orders only by time DESC in
db.query.auditLog.findMany (orderBy: (table, { desc }) => desc(table.time)),
which is non-deterministic when multiple rows share the same timestamp; modify
the orderBy to include a deterministic tiebreaker by adding a secondary unique
sort key (e.g., id) in descending order as well so results are stable across
pages (ensure orderBy uses both table.time and table.id consistently).
- Around line 38-40: Clamp both params.limit and params.page to be at least 1
before computing pageSize and offset: compute a clampedLimit = Math.max(1,
params.limit ?? LIMIT), then compute pageSize = Math.min(clampedLimit,
MAX_LIMIT), compute clampedPage = Math.max(1, params.page ?? 1), and finally
offset = (clampedPage - 1) * pageSize; update the references to
params.limit/params.page in fetch.ts (the pageSize, page and offset
calculations) so negative or zero values cannot produce a negative offset or
negative pageSize.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7005be8d-b186-4f63-9730-dbf7f45badb7

📥 Commits

Reviewing files that changed from the base of the PR and between edbf840 and 332cc4d.

📒 Files selected for processing (16)
  • web/apps/dashboard/app/(app)/[workspaceSlug]/audit/components/table/hooks/use-logs-query.ts
  • web/apps/dashboard/app/(app)/[workspaceSlug]/audit/components/table/log-details/components/log-header.tsx
  • web/apps/dashboard/app/(app)/[workspaceSlug]/audit/components/table/logs-table.tsx
  • web/apps/dashboard/components/audit-logs-table/columns/create-audit-log-columns.tsx
  • web/apps/dashboard/components/audit-logs-table/components/cells/actor-cell.tsx
  • web/apps/dashboard/components/audit-logs-table/components/cells/audit-action-badge-cell.tsx
  • web/apps/dashboard/components/audit-logs-table/components/cells/mono-text-cell.tsx
  • web/apps/dashboard/components/audit-logs-table/components/empty-audit-logs.tsx
  • web/apps/dashboard/components/audit-logs-table/components/skeletons/render-audit-log-skeleton-row.tsx
  • web/apps/dashboard/components/audit-logs-table/hooks/use-audit-logs-query.ts
  • web/apps/dashboard/components/audit-logs-table/index.ts
  • web/apps/dashboard/components/audit-logs-table/schema/audit-logs.schema.ts
  • web/apps/dashboard/components/audit-logs-table/utils/get-row-class.ts
  • web/apps/dashboard/lib/trpc/routers/audit/fetch.ts
  • web/apps/dashboard/lib/trpc/routers/audit/schema.ts
  • web/apps/dashboard/lib/trpc/routers/audit/utils.ts
💤 Files with no reviewable changes (1)
  • web/apps/dashboard/app/(app)/[workspaceSlug]/audit/components/table/hooks/use-logs-query.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
web/apps/dashboard/lib/trpc/routers/audit/fetch.ts (1)

38-40: ⚠️ Potential issue | 🟠 Major

Clamp page and limit before building the offset.

Line 38 still accepts negative limit, and Line 39 still accepts page <= 0, so Line 40 can produce a negative limit/offset. Clamp both values at the RPC boundary before they reach Drizzle.

Suggested fix
-    const pageSize = Math.min(params.limit, MAX_LIMIT) || LIMIT;
-    const page = params.page ?? 1;
+    const pageSize = Math.min(Math.max(params.limit ?? LIMIT, 1), MAX_LIMIT);
+    const page = Math.max(params.page ?? 1, 1);
     const offset = (page - 1) * pageSize;

As per coding guidelines, "Make illegal states unrepresentable by modeling domain with discriminated unions and parsing inputs at boundaries into typed structures".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/apps/dashboard/lib/trpc/routers/audit/fetch.ts` around lines 38 - 40,
Clamp both params.limit and params.page before computing pageSize and offset:
ensure you derive a sanitizedLimit = Math.max(1, Math.min(params.limit ?? LIMIT,
MAX_LIMIT)) and a sanitizedPage = Math.max(1, params.page ?? 1) (or equivalent),
then use sanitizedLimit for pageSize and sanitizedPage for page so offset =
(sanitizedPage - 1) * sanitizedLimit cannot be negative; update the logic around
the variables pageSize, page, offset in fetch.ts to use these sanitized values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/apps/dashboard/lib/trpc/routers/audit/fetch.ts`:
- Around line 44-58: The current LIMIT/OFFSET pagination
(db.query.auditLog.findMany with orderBy (table, { desc }) => [desc(table.time),
desc(table.id)], pageSize, offset and the separate count() query) can drift as
new rows are inserted; fix by switching to cursor-based paging or by pinning an
upper-bound snapshot from the first page and applying it to all subsequent
reads: capture the last row's (time,id) from the first page and add a persistent
where clause (e.g., time < upperTime OR (time = upperTime AND id <= upperId)) to
both db.query.auditLog.findMany and the count() select so all pages and the
total share the same snapshot; update the API to accept a cursor (upperTime,
upperId and/or nextCursor) instead of relying on offset to avoid
duplicates/missing rows.

---

Duplicate comments:
In `@web/apps/dashboard/lib/trpc/routers/audit/fetch.ts`:
- Around line 38-40: Clamp both params.limit and params.page before computing
pageSize and offset: ensure you derive a sanitizedLimit = Math.max(1,
Math.min(params.limit ?? LIMIT, MAX_LIMIT)) and a sanitizedPage = Math.max(1,
params.page ?? 1) (or equivalent), then use sanitizedLimit for pageSize and
sanitizedPage for page so offset = (sanitizedPage - 1) * sanitizedLimit cannot
be negative; update the logic around the variables pageSize, page, offset in
fetch.ts to use these sanitized values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0fee9742-a993-4a0d-9033-e63d46b3f079

📥 Commits

Reviewing files that changed from the base of the PR and between 332cc4d and 6e0e7b9.

📒 Files selected for processing (4)
  • web/apps/dashboard/app/(app)/[workspaceSlug]/audit/components/table/logs-table.tsx
  • web/apps/dashboard/components/audit-logs-table/components/cells/actor-cell.tsx
  • web/apps/dashboard/components/navigation/sidebar/app-sidebar/components/nav-items/nested-nav-item.tsx
  • web/apps/dashboard/lib/trpc/routers/audit/fetch.ts
✅ Files skipped from review due to trivial changes (1)
  • web/apps/dashboard/components/navigation/sidebar/app-sidebar/components/nav-items/nested-nav-item.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/apps/dashboard/components/audit-logs-table/components/cells/actor-cell.tsx
  • web/apps/dashboard/app/(app)/[workspaceSlug]/audit/components/table/logs-table.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/apps/dashboard/lib/trpc/routers/audit/fetch.ts`:
- Around line 38-40: The code computes page and offset from params but only
lower-bounds page, allowing huge offsets to be used in queries; update the RPC
input/parsing or add a guard in the fetch handler to enforce a maximum
page/offset: introduce a MAX_PAGE or MAX_OFFSET constant and either clamp page
(e.g., page = Math.min(Math.max(1, params.page ?? 1), MAX_PAGE)) before
computing offset, or compute offset and throw a TRPCError when offset >
MAX_OFFSET; apply this change around the pageSize/page/offset logic (pageSize,
page, offset, LIMIT, MAX_LIMIT, params) so oversized requests are rejected or
clamped at the API boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 677c9122-9172-438c-bb40-ba60d00dc029

📥 Commits

Reviewing files that changed from the base of the PR and between 6e0e7b9 and ed0f713.

📒 Files selected for processing (1)
  • web/apps/dashboard/lib/trpc/routers/audit/fetch.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant