Skip to content

feat: tanstack replacement for virtual-table in root keys#5066

Merged
MichaelUnkey merged 110 commits intomainfrom
root-key-table
Mar 17, 2026
Merged

feat: tanstack replacement for virtual-table in root keys#5066
MichaelUnkey merged 110 commits intomainfrom
root-key-table

Conversation

@MichaelUnkey
Copy link
Collaborator

@MichaelUnkey MichaelUnkey commented Feb 17, 2026

What does this PR do?

  • Generic DataTable component and all supporting primitives (cells, headers, footers, hooks, utils) from the dashboard into the shared @unkey/ui package, making it reusable across the monorepo.
  • Refactors the root keys table to use the new shared DataTable, replacing the previous VirtualTable implementation with a paginated, non-virtualized approach.
  • Introduces a new root-keys-table component folder in the dashboard that owns root-key-specific columns, skeleton rendering, query hooks, and schema.

Changes

@unkey/ui (new exports)

  • DataTable — generic TanStack Table component with sorting, row selection, keyboard nav, skeleton loading, and load-more/pagination footer support
  • Cell primitives: BadgeCell, CopyCell, HiddenValueCell, TimestampCell, StatusCell, AssignedItemsCell, LastUpdatedCell, RootKeyNameCell
  • Header primitive: SortableHeader
  • Footer components: PaginationFooter, LoadMoreFooter
  • Empty state: EmptyRootKeys
  • Hooks: useDataTable, useRealtimeData, useTableHeight
  • Constants: STATUS_STYLES, StatusStyle

Dashboard — components/root-keys-table/ (new)

  • columns/create-root-key-columns.tsx — column definitions for the root keys table
  • components/skeletons/render-root-key-skeleton-row.tsx — skeleton row renderer
  • components/settings-root-keys/ — delete/edit actions and popover
  • hooks/use-root-keys-list-query.ts — paginated tRPC query hook
  • schema/query-logs.schema.ts — Zod schema for the query payload

Dashboard — consumer updates

  • root-keys-list.tsx — now uses DataTable, EmptyRootKeys, PaginationFooter from @unkey/ui and the new root-keys-table module; replaces infinite-scroll loadMore with page-based onPageChange
  • keys-list.tsx — updated HiddenValueCell import to use @unkey/ui
  • Removed old components/data-table/ directory from the dashboard

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?

  • Root keys settings page loads and displays keys correctly
  • Pagination controls work (next/prev page, page size)
  • Sorting on sortable columns works
  • Delete and edit actions open correct dialogs
  • Skeleton loading state renders during fetch
  • Empty state renders when no keys exist

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces a cursor-based root-keys list with an offset-paginated DataTable system: adds a reusable data-table in the ui package (components, hooks, types, constants), migrates dashboard root-keys UI to the new paginated flow, updates TRPC schema/router for page/limit/sort, and adjusts imports.

Changes

Cohort / File(s) Summary
Dashboard root-keys view
web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/root-keys-list.tsx, web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/navigation.tsx
Switched list to use centralized DataTable and new paginated hook; updated navigation import for create-rootkey-button and row selection/navigation logic.
Removed legacy hook
web/apps/dashboard/app/(app)/[workspaceSlug]/settings/root-keys/components/table/hooks/use-root-keys-list-query.ts
Deleted old cursor-based useRootKeysListQuery hook and its pagination/map logic.
Root-keys table helpers
web/apps/dashboard/components/root-keys-table/..., web/apps/dashboard/components/root-keys-table/utils/get-row-class.ts
Added createRootKeyColumns, renderRootKeySkeletonRow, useRootKeysListPaginated; moved/reworked getRowClassName and STATUS_STYLES; updated component imports/exports.
Backend pagination & schema
web/apps/dashboard/components/root-keys-table/schema/query-logs.schema.ts, web/apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts
Schema switched from cursor to page/limit/sort; router refactored to offset pagination, added sort mapping/tiebreaker, returns total/hasMore (removed nextCursor).
Dashboard keys list import fix
web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
Replaced local HiddenValueCell import with centralized @unkey/ui export.
Minor dashboard UI tweaks
web/apps/dashboard/components/logs/checkbox/filter-item.tsx, web/apps/dashboard/components/root-keys-table/components/settings-root-keys/delete-root-key.tsx, web/apps/dashboard/components/root-keys-table/components/settings-root-keys/root-keys-table-action.popover.tsx
Removed effect syncing internal open state with isActive; adjusted relative import paths for delete/create components and popover import.
Virtual table animation rewrite
web/apps/dashboard/components/virtual-table/components/loading-indicator.tsx
Replaced inline keyframe/style-jsx with utility animation classes and cn-based class composition.
@unkey/ui — data-table core & types
web/internal/ui/src/components/data-table/data-table.tsx, .../types.ts, .../constants/*, .../hooks/*, .../utils/*
Introduced a full generic DataTable implementation, types, constants, hooks (useDataTable, useRealtimeData, useTableHeight), utilities (column-width, get-page-numbers) and default config constants.
@unkey/ui — cells, headers, rows, skeletons, footers, utils
web/internal/ui/src/components/data-table/components/cells/*, .../components/headers/*, .../components/rows/*, .../components/skeletons/*, .../components/footer/*, .../components/empty/*, .../components/utils/*
Added many reusable components (CheckboxCell, BadgeCell, CopyCell, HiddenValueCell, LastUpdatedCell, RootKeyNameCell, StatusCell, TimestampCell, RowActionSkeleton, SortableHeader, SkeletonRow, root-key skeletons, LoadMoreFooter, PaginationFooter, EmptyRootKeys, RealtimeSeparator) and their prop types and skeletons.
Barrels & public API
web/internal/ui/src/components/data-table/index.ts, web/internal/ui/src/index.ts, various .../index.ts
Added barrel exports to expose the new data-table public API and re-export new components/types.
Package dep added
web/internal/ui/package.json
Added dependency @tanstack/react-table@8.21.3.
Small refactors & typings
web/internal/ui/src/components/data-table/components/cells/assigned-items-cell.tsx, .../hidden-value-cell.tsx, .../last-updated-cell.tsx, etc.
Converted inline prop types to exported interfaces, renamed some components to *Cell suffixes, adjusted import paths and small component/type renames.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Dashboard UI
    participant Hook as useRootKeysListPaginated
    participant Filters as useFilters
    participant TRPC as TRPC Client
    participant Router as Backend Router
    participant DB as Database

    Client->>Hook: mount (page, pageSize, sort)
    Hook->>Filters: read active filters
    Filters-->>Hook: filter params, filterKey
    Hook->>TRPC: query listRootKeys(page, limit, sort, filters)
    TRPC->>Router: listRootKeys(params)
    Router->>DB: count with filters
    Router->>DB: fetch keys with offset & order
    DB-->>Router: totalCount, keys
    Router-->>TRPC: { keys, totalCount, hasMore }
    TRPC-->>Hook: page data
    Hook-->>Client: rootKeys, pagination meta
Loading
sequenceDiagram
    participant Page as App Page
    participant DataTable as DataTable Component
    participant useData as useDataTable Hook
    participant TanStack as `@tanstack/react-table`
    participant Cells as Column Renderers

    Page->>DataTable: provide data, columns, config
    DataTable->>useData: init table instance
    useData->>TanStack: build table (core, sorting)
    TanStack-->>useData: table instance
    useData-->>DataTable: table API/state
    DataTable->>Cells: render per-row/per-column cell()
    Cells-->>DataTable: JSX cell nodes
    DataTable-->>Page: rendered table + footer/controls
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% 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: tanstack replacement for virtual-table in root keys' clearly summarizes the main change: replacing VirtualTable with TanStack Table for root keys, which is the primary architectural change in this PR.
Description check ✅ Passed The PR description is comprehensive and well-structured. It includes all required template sections: detailed 'What does this PR do?' with bullet points, 'Type of change' marked, complete 'How should this be tested?' with test scenarios, and all required checklist items completed.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch root-key-table
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

You can validate your CodeRabbit configuration file in your editor.

If your editor has YAML language server, you can enable auto-completion and validation by adding # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json at the top of your CodeRabbit configuration file.

@vercel
Copy link

vercel bot commented Feb 17, 2026

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

Project Deployment Actions Updated (UTC)
dashboard Ready Ready Preview, Comment Mar 17, 2026 3:35pm
engineering Ready Ready Preview, Comment Mar 17, 2026 3:35pm

Request Review

@vercel vercel bot temporarily deployed to Preview – engineering February 17, 2026 15:12 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard February 17, 2026 15:12 Inactive
chronark and others added 21 commits February 24, 2026 14:15
* fix: cleanup project side nav

* feat: simplify deployment overview page

only show build logs until it's built, then show domains and network

* chore: clean up nav
…0M +) (#4959)

* fix(clickhouse): improve clickhouse query for key logs and add  new table and mv for latest keys used

* fix valid/error count = 0 scenario

* remove identity_id from order by

* wrap identity_id with aggregating function since its removed from the order key

---------

Co-authored-by: Flo <53355483+Flo4604@users.noreply.github.com>
* fix: domain refetch and promotion disable rule

* fix: regression

---------

Co-authored-by: Andreas Thomas <dev@chronark.com>
* refactor: move custom domains to tanstack db

* fix: comment

* fix: delete mutation

* remove: unnecessary query
* remove agent

* remove agent
* remove agent

* remove agent

* use vault in dashboard

* remove
* fix: cleanup project side nav

* feat: simplify deployment overview page

only show build logs until it's built, then show domains and network

* chore: clean up nav

* feat: add per-project sticky domain and only display that
* chore: use vault in api

* chore: use vault in api

* fix harness

* use memory test

* vault container go start

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* dunno

* nextjs should allow a setting that says dynamic

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Add references to real-time performance benchmarks in:
- introduction.mdx: new 'Performance at scale' accordion
- modes.mdx: link after latency claim

Presents benchmarks as capability demonstration rather than comparison.
Add missing SEO description to frontmatter

Generated-By: mintlify-agent

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: Andreas Thomas <dev@chronark.com>
Remove Spring Boot Java, Rust, and Elixir SDK docs that are not linked in navigation and appear to be outdated/unmaintained.

Generated-By: mintlify-agent

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: Andreas Thomas <dev@chronark.com>
* rework release

* rework release
* feat: generate rpc wrappers

* bazel happyier

* more changes

* more changes

* move path

* delete old files (#5043)

* fix: rabbit comments

---------

Co-authored-by: Oz <21091016+ogzhanolguncu@users.noreply.github.com>
* add a gossip implementation

* add gossip to sentinel/frontline

* add message muxing

* sentinel fun

* cleansings

* cleansings

* cleansings

* cleansings

* use oneof

* fix bazel happiness

* do some changies

* exportoneof

* more cool fancy thingx

* change gateway choosing

* add label

* adjjust some more

* adjjust some more

* fixa test

* goodbye kafka

* fix: bazel

* rename gateway -> ambassador

* add docs

* fix: rabbit comments

* [autofix.ci] apply automated fixes

* idfk

* more changes

* more changes

* fix ordering

* fix missing files

* fix test

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* fix: retry cillium policy until CRDs are ready

* fix: blocks until all system pods are ready
* fix: cleanup project side nav

* feat: simplify deployment overview page

only show build logs until it's built, then show domains and network

* feat: new build screen for ongoing deployments

* fix: table column typo
Copy link
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: 3

♻️ Duplicate comments (1)
web/apps/dashboard/components/root-keys-table/hooks/use-root-keys-list-query.ts (1)

64-67: ⚠️ Potential issue | 🟠 Major

Enforce the schema minimum when normalizing pageSize.

normalizedPageSize currently allows 1..19, but the query schema requires limit >= 20. That can send invalid payloads and break pagination on small custom page sizes.

🔧 Proposed fix
 const MAX_PAGE_SIZE = 200;
+const MIN_PAGE_SIZE = 20;
 
 export function useRootKeysListPaginated(pageSize = DEFAULT_PAGE_SIZE) {
-  const normalizedPageSize =
-    Number.isFinite(pageSize) && pageSize > 0
-      ? Math.min(Math.floor(pageSize), MAX_PAGE_SIZE)
-      : DEFAULT_PAGE_SIZE;
+  const normalizedPageSize = Number.isFinite(pageSize)
+    ? Math.min(Math.max(Math.floor(pageSize), MIN_PAGE_SIZE), MAX_PAGE_SIZE)
+    : DEFAULT_PAGE_SIZE;

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

Also applies to: 129-129, 148-148

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

In
`@web/apps/dashboard/components/root-keys-table/hooks/use-root-keys-list-query.ts`
around lines 64 - 67, The normalizedPageSize logic currently lets values below
the query schema minimum through; update the normalization of pageSize (the
normalizedPageSize calculation that references pageSize, MAX_PAGE_SIZE and
DEFAULT_PAGE_SIZE) to clamp values to a minimum of 20 (schema limit) before
applying Math.floor and MAX_PAGE_SIZE, so the computed limit never falls below
20; apply the same minimum-enforcing change to the other occurrences of this
normalization in the file (the other normalizedPageSize usages referenced in the
diff) to keep all payloads valid.
🧹 Nitpick comments (1)
web/internal/ui/src/components/data-table/components/footer/load-more-footer.tsx (1)

91-94: Consider using cn() consistently for className construction.

The inner div uses a template literal with a ternary for conditional classes, while other elements in this file use the cn() utility. Using cn() here would improve consistency and readability.

♻️ Proposed refactor for consistency
 <div
-  className={`w-[740px] border bg-gray-1 dark:bg-black border-gray-6 min-h-[60px] flex items-center justify-center rounded-[10px] drop-shadow-lg transform-gpu shadow-sm mb-5 transition-all duration-200 hover:shadow-lg ${
-    shouldShow ? "pointer-events-auto" : "pointer-events-none"
-  }`}
+  className={cn(
+    "w-[740px] border bg-gray-1 dark:bg-black border-gray-6 min-h-[60px] flex items-center justify-center rounded-[10px] drop-shadow-lg transform-gpu shadow-sm mb-5 transition-all duration-200 hover:shadow-lg",
+    shouldShow ? "pointer-events-auto" : "pointer-events-none"
+  )}
   aria-hidden={!shouldShow}
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/internal/ui/src/components/data-table/components/footer/load-more-footer.tsx`
around lines 91 - 94, Replace the template-literal className on the inner div
with the project's cn() utility to match the rest of the file (in
load-more-footer component), keeping all static classes and moving the
conditional pointer-events class into cn() using shouldShow to toggle
"pointer-events-auto" vs "pointer-events-none"; update the className passed to
that div (where the current template literal is used) to call cn(...) so
readability and consistency are preserved.
🤖 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/components/virtual-table/components/loading-indicator.tsx`:
- Line 51: The minimized footer branch still mounts interactive controls when
hidden because the top-level wrapper only animates the expanded branch; update
the LoadingIndicator component to short-circuit and return null when hide is
true or shouldShow is false (i.e., when onLoadMore is missing) so the footer and
any focusable descendants are not mounted or focusable; locate the render in
loading-indicator.tsx and add the early return using hide || !shouldShow to
prevent mounting the interactive controls.
- Around line 96-97: The delayed fade-slide-in animations in the
LoadingIndicator JSX (the elements with className "transition-all duration-200
animate-fade-slide-in") should include animationFillMode set to "backwards" to
prevent jank during the animationDelay; update the inline style objects
(currently containing animationDelay: "0.2s" and similar) to also include
animationFillMode: "backwards" for each occurrence (the same change should be
applied to the other two instances of the same className in this file).

In
`@web/internal/ui/src/components/data-table/components/footer/load-more-footer.tsx`:
- Around line 63-70: The minimized view renders countInfoText without a
fallback, causing an empty span when undefined; update the minimized block in
the LoadMoreFooter component to render countInfoText || the same fallback used
in the expanded view (the "Viewing X of Y items" expression) so the span always
shows meaningful text, i.e., replace the direct {countInfoText} usage with the
fallback-or-value expression used in the expanded rendering and keep the
existing classes and structure.

---

Duplicate comments:
In
`@web/apps/dashboard/components/root-keys-table/hooks/use-root-keys-list-query.ts`:
- Around line 64-67: The normalizedPageSize logic currently lets values below
the query schema minimum through; update the normalization of pageSize (the
normalizedPageSize calculation that references pageSize, MAX_PAGE_SIZE and
DEFAULT_PAGE_SIZE) to clamp values to a minimum of 20 (schema limit) before
applying Math.floor and MAX_PAGE_SIZE, so the computed limit never falls below
20; apply the same minimum-enforcing change to the other occurrences of this
normalization in the file (the other normalizedPageSize usages referenced in the
diff) to keep all payloads valid.

---

Nitpick comments:
In
`@web/internal/ui/src/components/data-table/components/footer/load-more-footer.tsx`:
- Around line 91-94: Replace the template-literal className on the inner div
with the project's cn() utility to match the rest of the file (in
load-more-footer component), keeping all static classes and moving the
conditional pointer-events class into cn() using shouldShow to toggle
"pointer-events-auto" vs "pointer-events-none"; update the className passed to
that div (where the current template literal is used) to call cn(...) so
readability and consistency are preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f8151586-6255-42a6-8f51-d1f0ab407afa

📥 Commits

Reviewing files that changed from the base of the PR and between 4965984 and bfcb515.

📒 Files selected for processing (4)
  • web/apps/dashboard/components/root-keys-table/hooks/use-root-keys-list-query.ts
  • web/apps/dashboard/components/virtual-table/components/loading-indicator.tsx
  • web/internal/ui/src/components/data-table/components/footer/load-more-footer.tsx
  • web/internal/ui/src/components/data-table/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/internal/ui/src/components/data-table/types.ts

@perkinsjr
Copy link
Member

Can I request we don't show page numbers unless we have more than 1 page of keys

CleanShot 2026-03-13 at 10 16 54@2x

Also this coloring is weird both dark and light

CleanShot 2026-03-13 at 10 18 07@2x

Copy link
Collaborator

@mcstepp mcstepp left a comment

Choose a reason for hiding this comment

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

🚢

@MichaelUnkey MichaelUnkey enabled auto-merge March 17, 2026 15:32
@MichaelUnkey MichaelUnkey added this pull request to the merge queue Mar 17, 2026
Merged via the queue into main with commit c5c6ee0 Mar 17, 2026
12 checks passed
@MichaelUnkey MichaelUnkey deleted the root-key-table branch March 17, 2026 15:41
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.

8 participants