Skip to content

feat: update identities list styling#4480

Merged
mcstepp merged 9 commits intomainfrom
ENG-2268-identities-list-view
Dec 10, 2025
Merged

feat: update identities list styling#4480
mcstepp merged 9 commits intomainfrom
ENG-2268-identities-list-view

Conversation

@mcstepp
Copy link
Collaborator

@mcstepp mcstepp commented Dec 4, 2025

What does this PR do?

Styled identities list to match keys page:

  • External ID truncates at 50 chars, Identity ID shows tooltip with copy button
  • Replaced badges with plain text for counts
  • Added skeleton loading states
  • Updated search to use LLMSearch component

Added Create Identity feature:

  • "Create Identity" button in header
  • Form with External ID (required, 3-255 chars) and Metadata (optional JSON, <1MB)
  • Validates duplicates and JSON syntax
  • Shows success toast and refreshes table

Note: More to be added in future tickets. This is just feature parity with existing identities functionality, but with the updated styling to match the rest of the dashboard.

Fixes #4457

Screenshot 2025-12-04 at 3 55 01 PM Screenshot 2025-12-04 at 4 06 00 PM Screenshot 2025-12-04 at 3 24 12 PM

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?

  1. Go to /[workspace]/identities
  2. Click "Create Identity" → add external ID "test_user_1" → submit
  3. Verify it appears in table
  4. Hover Identity ID → verify tooltip with copy button
  5. Click row → verify navigates to detail page
  6. Test search, context menu copy actions

Key checks:

  • Compare styling side-by-side with keys page
  • External IDs >50 chars show "..."
  • Empty state shows "Learn about Identities" button
  • Duplicate external ID shows error
  • Invalid JSON in metadata shows error

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

@linear
Copy link

linear bot commented Dec 4, 2025

@changeset-bot
Copy link

changeset-bot bot commented Dec 4, 2025

⚠️ No Changeset found

Latest commit: 30945a4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Dec 4, 2025

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

Project Deployment Preview Comments Updated (UTC)
dashboard Ready Ready Preview Comment Dec 10, 2025 5:42pm
engineering Ready Ready Preview Comment Dec 10, 2025 5:42pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 4, 2025

📝 Walkthrough

Walkthrough

Adds a client-driven identities UI (search, controls, virtualized infinite table, per-row actions, skeletons, create-identity dialog) and backend support (identity query/search enhancements, totalCount, ClickHouse latest-verification endpoint, SQL LIKE escaping). Removes legacy list/search/row components.

Changes

Cohort / File(s) Summary
Search & Controls
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx, apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx
Add IdentitiesSearch (query-state + LLMSearch UI) and IdentitiesListControls composing search into the controls layout.
Client composition & page/nav
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx, apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx, apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx
New IdentitiesClient wrapper; page simplified to render it (signature changed); navigation adds CreateIdentityDialog action.
Create Identity UI
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx
Add CreateIdentityDialog using react-hook-form + zod, meta JSON parsing/size checks, trpc.identity.create mutation with success/409 handling, query invalidation and form reset.
Table, Actions & Cells
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx, .../identity-table-actions.tsx, .../last-used.tsx, .../skeletons.tsx
Add virtualized IdentitiesList with useInfiniteQuery pagination, row actions (IdentityTableActions), LastUsedCell querying latest verification, and loading skeleton components.
Removed legacy components
apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx, apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx, apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx
Deleted previous Results, SearchField, and Row components (old fetching/row rendering removed).
tRPC: identity queries & verification
apps/dashboard/lib/trpc/routers/identity/query.ts, apps/dashboard/lib/trpc/routers/identity/search.ts, apps/dashboard/lib/trpc/routers/identity/latestVerification.ts
query.ts: add optional search, reusable filters, precomputed totalCount, include keys/ratelimits in responses; search.ts: use escapeLike and include relations; latestVerification.ts: new ClickHouse-backed procedure returning last verification time.
Router & SQL utils
apps/dashboard/lib/trpc/routers/index.ts, apps/dashboard/lib/trpc/routers/utils/sql.ts
Expose latestVerification on identity router; add escapeLike(value: string) utility for safe SQL LIKE escaping.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant TRPC as tRPC Client
    participant Backend
    participant DB as Prisma
    participant CH as ClickHouse

    User->>Browser: Open identities page
    Browser->>TRPC: identity.query (useInfiniteQuery, limit=50, optional search)
    TRPC->>Backend: request identities + totalCount
    Backend->>DB: SELECT identities (+ keys, ratelimits) and COUNT with filters
    DB-->>Backend: identities[], totalCount, nextCursor
    Backend-->>TRPC: response
    TRPC-->>Browser: render virtualized table (skeletons while loading)
Loading
sequenceDiagram
    participant User
    participant Browser
    participant TRPC as tRPC Client
    participant Backend
    participant CH as ClickHouse

    User->>Browser: Hover/inspect "Last used" cell
    Browser->>TRPC: latestVerification({ identityId })
    TRPC->>Backend: request latest verification
    Backend->>CH: SELECT MAX(last_used) FROM key_verifications_raw_v2 WHERE workspace_id AND identity_id
    CH-->>Backend: last_used
    Backend-->>TRPC: { lastVerificationTime }
    TRPC-->>Browser: render timestamp or "Never used"
Loading
sequenceDiagram
    participant User
    participant Browser
    participant Form as react-hook-form
    participant TRPC as tRPC Client
    participant Backend
    participant DB as Prisma

    User->>Browser: Click "Create Identity"
    Browser->>User: Show dialog (form)
    User->>Form: Fill externalId + metadata
    Form->>Browser: Validate via zod (length, JSON, size)
    alt Valid
        Browser->>TRPC: mutation identity.create({ externalId, meta })
        TRPC->>Backend: create identity
        Backend->>DB: INSERT identity
        alt Duplicate (409)
            DB-->>Backend: constraint error
            Backend-->>TRPC: TRPCError CONFLICT
            TRPC-->>Browser: set externalId field error
        else Success
            DB-->>Backend: created identity
            Backend-->>TRPC: success
            TRPC-->>Browser: success -> toast, invalidate identity queries
        end
    else Invalid
        Browser->>User: show validation errors
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas needing extra attention:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx — infinite pagination aggregation, UI navigation/loading state, dynamic imports and virtualization.
  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts — ClickHouse parameter binding, query correctness, and error handling.
  • apps/dashboard/lib/trpc/routers/identity/query.ts — filter construction, escapeLike usage, totalCount correctness and potential performance implications.
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx — zod rules, meta JSON parsing/size constraints, mutation error branches and query invalidation.
  • Ensure removed files do not leave dangling imports or references in other modules.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title "feat: update identities list styling" is partially related to the changeset. While styling updates are present, the PR also adds significant new features (Create Identity dialog, search component, table actions) that represent the primary work.
Description check ✅ Passed The PR description comprehensively documents changes, includes testing steps, screenshots, and checks most required checklist items. It references issue #4457 and explains both styling updates and new features.
Linked Issues check ✅ Passed The code implements all acceptance criteria from #4457: table with required columns (External ID, Identity ID, keys/ratelimits counts, created, last used), search via LLMSearch, pagination, context menu with copy actions, Create Identity dialog with validation, and proper error handling.
Out of Scope Changes check ✅ Passed All changes are scoped to identities list implementation. Backend changes (latestVerification endpoint, query enhancements, SQL utilities) support the stated objectives. No unrelated refactoring or extraneous modifications detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ENG-2268-identities-list-view

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
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

🧹 Nitpick comments (5)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (1)

86-92: Consider defensive error handling for JSON parsing.

While the zod schema validation ensures meta is valid JSON before reaching this point, wrapping JSON.parse in a try-catch would provide defense-in-depth against edge cases (e.g., if validation is bypassed or the schema changes).

Apply this diff to add defensive error handling:

 const onSubmit = (data: FormValues) => {
-  const meta = data.meta?.trim() ? JSON.parse(data.meta) : null;
+  let meta = null;
+  if (data.meta?.trim()) {
+    try {
+      meta = JSON.parse(data.meta);
+    } catch (error) {
+      console.error("Failed to parse metadata JSON:", error);
+      setError("meta", { message: "Invalid JSON format" });
+      return;
+    }
+  }
   createIdentity.mutate({
     externalId: data.externalId,
     meta,
   });
 };
apps/dashboard/lib/trpc/routers/identity/latestVerification.ts (1)

37-52: Redundant nested error handling.

The TRPCError thrown at line 38-41 is immediately caught by the outer catch block at line 47, which logs and re-throws a new TRPCError with the same code and similar message. This adds unnecessary overhead and duplicates error messages in logs.

-      if (result.err) {
-        throw new TRPCError({
-          code: "INTERNAL_SERVER_ERROR",
-          message: "Something went wrong when fetching data from ClickHouse.",
-        });
-      }
-
-      return {
-        lastVerificationTime: result.val.length > 0 ? result.val[0].last_used : null,
-      };
-    } catch (error) {
-      console.error("Error querying last verification for identity:", error);
-      throw new TRPCError({
-        code: "INTERNAL_SERVER_ERROR",
-        message: "Something went wrong when fetching data from ClickHouse.",
-      });
-    }
+      if (result.err) {
+        console.error("Error querying last verification for identity:", result.err);
+        throw new TRPCError({
+          code: "INTERNAL_SERVER_ERROR",
+          message: "Something went wrong when fetching data from ClickHouse.",
+        });
+      }
+
+      return {
+        lastVerificationTime: result.val.length > 0 ? result.val[0].last_used : null,
+      };
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1)

83-93: Map is fully rebuilt on each data update.

The useEffect creates a new Map from scratch on every identitiesData change. For incremental pagination, consider merging new pages into the existing map to preserve references and avoid unnecessary re-renders.

   useEffect(() => {
     if (identitiesData) {
-      const newMap = new Map<string, Identity>();
-      identitiesData.pages.forEach((page) => {
-        page.identities.forEach((identity) => {
-          newMap.set(identity.id, identity);
+      setIdentitiesMap((prevMap) => {
+        const newMap = new Map(prevMap);
+        identitiesData.pages.forEach((page) => {
+          page.identities.forEach((identity) => {
+            newMap.set(identity.id, identity);
+          });
         });
+        return newMap;
       });
-      setIdentitiesMap(newMap);
     }
   }, [identitiesData]);
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx (2)

36-45: Consider using a non-interactive element for skeleton button.

Using a <button> element for a loading skeleton means screen readers may announce an interactive element that does nothing. A <div> with appropriate styling would be semantically cleaner.

-export const ActionColumnSkeleton = () => (
-  <button
-    type="button"
+export const ActionColumnSkeleton = () => (
+  <div
+    role="presentation"
     className={cn(
       "group size-5 p-0 rounded m-0 items-center flex justify-center animate-pulse",
       "border border-gray-6",
     )}
   >
     <Dots className="text-gray-11" iconSize="sm-regular" />
-  </button>
+  </div>
 );

16-18: Remove unused ExternalIdColumnSkeleton export.

The export at line 16 is never imported or used anywhere in the codebase. Removing it will reduce dead code.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0740117 and 7fe0005.

📒 Files selected for processing (17)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/last-used.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx (0 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx (0 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx (0 hunks)
  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/identity/query.ts (4 hunks)
  • apps/dashboard/lib/trpc/routers/identity/search.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/index.ts (2 hunks)
💤 Files with no reviewable changes (3)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx
🧰 Additional context used
🧠 Learnings (14)
📓 Common learnings
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3797
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/control-cloud/index.tsx:1-4
Timestamp: 2025-08-18T10:28:47.391Z
Learning: In Next.js App Router, components that use React hooks don't need their own "use client" directive if they are rendered within a client component that already has the directive. The client boundary propagates to child components.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx
📚 Learning: 2025-09-23T17:40:44.944Z
Learnt from: perkinsjr
Repo: unkeyed/unkey PR: 4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:40:44.944Z
Learning: In the Unkey dashboard authorization section navigation components, the maintainer prefers to wrap entire navbars in Suspense rather than scoping Suspense to individual components, even if it blocks the whole navigation during loading.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx
📚 Learning: 2025-07-28T20:38:53.244Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx:322-341
Timestamp: 2025-07-28T20:38:53.244Z
Learning: In apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx, mcstepp prefers to keep hardcoded endpoint logic in the getDiffType function during POC phases for demonstrating diff functionality, rather than implementing a generic diff algorithm. This follows the pattern of keeping simplified implementations for demonstration purposes.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx
📚 Learning: 2025-09-23T17:39:59.820Z
Learnt from: perkinsjr
Repo: unkeyed/unkey PR: 4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx:88-97
Timestamp: 2025-09-23T17:39:59.820Z
Learning: The useWorkspaceNavigation hook in the Unkey dashboard guarantees that a workspace exists. If no workspace is found, the hook redirects the user to create a new workspace. Users cannot be logged in without a workspace, and new users must create one to continue. Therefore, workspace will never be null when using this hook.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx
📚 Learning: 2025-07-25T19:11:00.208Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts:110-147
Timestamp: 2025-07-25T19:11:00.208Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts, the user mcstepp prefers to keep mock data fallbacks in POC/demonstration code for simplicity, even if it wouldn't be production-ready. This aligns with the PR being work-in-progress for demonstration purposes.

Applied to files:

  • apps/dashboard/lib/trpc/routers/index.ts
📚 Learning: 2025-07-28T20:36:36.865Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/lib/trpc/routers/branch/getByName.ts:0-0
Timestamp: 2025-07-28T20:36:36.865Z
Learning: In apps/dashboard/lib/trpc/routers/branch/getByName.ts, mcstepp prefers to keep mock data (gitCommitMessage, buildDuration, lastCommitAuthor, etc.) in the branch procedure during POC phases to demonstrate what the UI would look like with proper schema changes, rather than returning null/undefined values.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts
📚 Learning: 2024-12-03T14:17:08.016Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2143
File: apps/dashboard/app/(app)/logs/logs-page.tsx:77-83
Timestamp: 2024-12-03T14:17:08.016Z
Learning: The `<LogsTable />` component already implements virtualization to handle large datasets efficiently.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2025-06-19T13:01:55.338Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3315
File: apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/general-setup.tsx:40-50
Timestamp: 2025-06-19T13:01:55.338Z
Learning: In the create-key form's GeneralSetup component, the Controller is intentionally bound to "identityId" as the primary field while "externalId" is set explicitly via setValue. The ExternalIdField component has been designed to handle this pattern where it receives identityId as its value prop but manages both identityId and externalId through its onChange callback.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx
📚 Learning: 2025-08-25T13:46:34.441Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx:4-4
Timestamp: 2025-08-25T13:46:34.441Z
Learning: The namespace list refresh component (apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx) intentionally uses the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than a namespace-specific hook. This cross-coupling between namespace list components and overview hooks is an architectural design decision.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx
📚 Learning: 2025-10-30T15:10:52.743Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-08-25T12:56:59.310Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3834
File: apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts:59-66
Timestamp: 2025-08-25T12:56:59.310Z
Learning: In the ratelimit namespace query system (apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts), the nameQuery filter is designed as an array for future extensibility to support multiple filters, but currently only the first filter (index 0) is processed. This is intentional future-proofing.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2024-10-20T07:05:55.471Z
Learnt from: chronark
Repo: unkeyed/unkey PR: 2294
File: apps/api/src/pkg/keys/service.ts:268-271
Timestamp: 2024-10-20T07:05:55.471Z
Learning: In `apps/api/src/pkg/keys/service.ts`, `ratelimitAsync` is a table relation, not a column selection. When querying, ensure that table relations are included appropriately, not as columns.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/search.ts
🧬 Code graph analysis (11)
apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1)
  • IdentitiesClient (6-13)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (2)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (1)
  • IdentitiesListControls (4-12)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1)
  • IdentitiesList (47-320)
apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (1)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (1)
  • CreateIdentityDialog (44-155)
apps/dashboard/lib/trpc/routers/index.ts (1)
apps/dashboard/lib/trpc/routers/identity/latestVerification.ts (1)
  • identityLastVerificationTime (10-54)
apps/dashboard/lib/trpc/routers/identity/latestVerification.ts (1)
apps/dashboard/lib/trpc/trpc.ts (3)
  • t (8-8)
  • requireUser (10-21)
  • requireWorkspace (23-36)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1)
internal/ui/src/components/llm-search/index.tsx (1)
  • LLMSearch (176-176)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (7)
apps/dashboard/lib/trpc/routers/identity/query.ts (1)
  • IdentityResponseSchema (12-22)
apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)
  • dynamic (6-6)
internal/ui/src/components/buttons/copy-button.tsx (1)
  • CopyButton (27-81)
apps/dashboard/lib/shorten-id.ts (1)
  • shortenId (5-54)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/last-used.tsx (1)
  • LastUsedCell (6-60)
apps/dashboard/components/virtual-table/index.tsx (1)
  • VirtualTable (38-407)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx (5)
  • IdentityColumnSkeleton (4-14)
  • CountColumnSkeleton (20-22)
  • CreatedColumnSkeleton (24-26)
  • LastUsedColumnSkeleton (28-34)
  • ActionColumnSkeleton (36-46)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (6)
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/create-key.schema.ts (1)
  • FormValues (351-351)
internal/ui/src/components/toaster.tsx (1)
  • toast (29-29)
apps/dashboard/lib/collections/index.ts (1)
  • reset (76-84)
internal/ui/src/components/dialog/dialog-container.tsx (1)
  • DialogContainer (66-66)
internal/ui/src/components/buttons/button.tsx (1)
  • Button (439-439)
internal/ui/src/components/form/form-input.tsx (1)
  • FormInput (56-56)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (2)
apps/dashboard/components/logs/controls-container.tsx (2)
  • ControlsContainer (1-7)
  • ControlsLeft (9-11)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1)
  • IdentitiesSearch (6-31)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx (2)
apps/dashboard/lib/trpc/routers/identity/query.ts (1)
  • IdentityResponseSchema (12-22)
apps/dashboard/components/logs/table-action.popover.tsx (2)
  • MenuItem (19-29)
  • TableActionPopover (36-169)
apps/dashboard/lib/trpc/routers/identity/query.ts (1)
internal/rbac/src/queries.ts (2)
  • or (46-50)
  • and (52-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Dashboard / Test Dashboard
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (20)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (3)

12-40: LGTM: Form validation schema is well-structured.

The validation covers all requirements from the PR objectives:

  • External ID: 3-255 characters with whitespace trimming and validation
  • Metadata: Optional JSON with proper syntax validation and 1MB size limit

63-84: LGTM: Error handling correctly distinguishes between conflicts and generic errors.

The success flow properly invalidates queries to refresh the identities list, and the error handling appropriately sets inline form errors for duplicate external IDs while showing toast messages for other failures.


94-154: LGTM: Dialog structure and form submission flow are well-implemented.

The use of the form attribute on the submit button to connect it with the form ID is the correct pattern for forms with external submit buttons. Loading states and validation feedback are properly handled.

apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (1)

6-6: LGTM: CreateIdentityDialog correctly integrated into navigation.

The integration follows the established pattern for navbar actions in the dashboard.

Also applies to: 18-20

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (1)

1-12: LGTM: Clean composition following established patterns.

The component correctly reuses the existing controls container components and integrates the IdentitiesSearch component.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1)

1-31: LGTM: LLMSearch integration is correctly implemented.

The hardcoded isLoading={false} is appropriate since the search appears to be client-side filtering based on the query state management pattern. The example queries are helpful for user guidance.

apps/dashboard/lib/trpc/routers/index.ts (1)

56-56: LGTM: latestVerification endpoint correctly added to identity router.

The addition follows the established pattern for extending TRPC routers in this codebase.

Also applies to: 342-342

apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)

1-21: LGTM: Page simplification improves maintainability.

The new structure delegates identity listing concerns to the IdentitiesClient component, resulting in cleaner separation of concerns while maintaining the necessary beta feature guard.

apps/dashboard/lib/trpc/routers/identity/search.ts (1)

31-65: LGTM: Relational data fetching enhances identity responses.

The addition of related keys and ratelimits aligns with the expanded identity data model. The selective column fetching (id only) for related entities is efficient given the small result set (limit of 5).

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx (1)

10-50: LGTM: Clean implementation with proper error handling.

The component correctly uses the Clipboard API with promise-based error handling and user feedback via toasts. The memoization with appropriate dependencies prevents unnecessary re-renders.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1)

1-13: LGTM!

Clean composition component that properly combines controls and list. The "use client" directive is correctly placed, and the flex layout is appropriate for stacking these components.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/last-used.tsx (2)

22-58: Well-structured loading and error states.

The component handles all three states (loading, error, success) appropriately with good UX patterns. The controlled tooltip integration with TimestampInfo is clean.


7-18: No changes needed — the pattern is established and pragmatic.

The skipBatch: true approach with per-row queries is a standard pattern throughout the codebase and is already mitigated by virtualization. The identities table uses VirtualTable with a 50-item limit and infinite scroll, which prevents excessive concurrent queries. Given the team's stated preference to implement optimizations only when they become necessary, this suggestion constitutes premature optimization. Remove the performance concern unless there's evidence of actual performance issues.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (3)

47-105: Good implementation of search, navigation, and state management.

The query state integration with nuqs, infinite query setup, and navigation handling are well-structured. Using a Map for identity lookups is appropriate for quick access patterns.


107-230: Well-designed column configuration with proper UI patterns.

External ID truncation at 50 chars, shortenId for identity IDs, tooltips with copy functionality, and proper loading states follow the PR requirements. The dynamic import for actions with a loading placeholder is a nice touch for code splitting.


11-11: The import path @unkey/ui/src/lib/utils is the correct and intended way to use the cn utility. The @unkey/ui package does not export cn from its public entry point (@unkey/ui), keeping it as an internal utility. This import pattern is consistently used across 30+ files in the codebase and is the only available method to access the cn function. No changes are needed.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx (1)

1-14: Consistent and well-structured skeleton components.

The skeletons appropriately mirror the structure of their corresponding data cells, providing good visual loading feedback. Consistent use of grayA-3 and animate-pulse maintains UI coherence.

Also applies to: 20-34

apps/dashboard/lib/trpc/routers/identity/query.ts (3)

6-10: LGTM on schema extension.

The addition of the optional search parameter aligns with the PR objective to support search-based filtering for identities.


60-71: LGTM on relational data fetching.

The with clause correctly fetches only the id column for related keys and ratelimits, minimizing data transfer while providing the counts needed for the UI.


82-92: LGTM on response transformation.

The transformed identities correctly include the related keys and ratelimits arrays, matching the updated IdentityResponseSchema.

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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/dashboard/app/integrations/vercel/callback/client.tsx (1)

1-6: Remove the unrelated change to this deprecated file or clarify its purpose.

Line 23 (commented WorkspaceSwitcher import) appears incidental to the commit message "fix: escape LIKE" and unrelated to the PR's stated objectives (identities list styling and Create Identity flow). Since the file is explicitly marked as deprecated and "hidden for now" (lines 1-4), modifying it—even to clean up unused imports—introduces unnecessary changes. Either remove this change or include it in a separate PR focused on deprecation cleanup with clear justification in the commit message.

🧹 Nitpick comments (25)
apps/dashboard/app/(app)/[workspaceSlug]/settings/team/client.tsx (1)

85-102: Admin header block wiring looks good; consider dropping unnecessary key props

The new admin-only header (tab Select + InviteButton) is correctly gated by isAdmin, wired to the existing tab state, and safe with respect to user/organization thanks to the earlier null guard.

The key props on Select and InviteButton are redundant here because these components are not rendered via an array/map; removing them would slightly reduce noise without changing behavior.

-        <div className="flex flex-row justify-end w-full gap-4">
-          <div>
-            <Select key="tab-select" value={tab} onValueChange={(value: Tab) => setTab(value)}>
+        <div className="flex flex-row justify-end w-full gap-4">
+          <div>
+            <Select value={tab} onValueChange={(value: Tab) => setTab(value)}>
@@
-          </div>
-          <InviteButton key="invite-button" user={user} organization={organization} />
+          </div>
+          <InviteButton user={user} organization={organization} />
go/apps/api/openapi/openapi-generated.yaml (1)

5449-5450: Clarify “always returns 200” to match new 403 exception.

Great addition explaining 403 vs 200/NOT_FOUND. For consistency, adjust the earlier claim that this endpoint “always returns HTTP 200” (Line 5436) to reflect the permission exception you document here.

Suggested tweak:

-**Important**: Always returns HTTP 200. Check the `valid` field in response data to determine if the key is authorized.
+**Important**: Returns HTTP 200 for verification outcomes. Authentication/permission errors may return 4xx (e.g., 403 when the root key has no verify permissions). Check the `valid` field in response data to determine if the key is authorized.

Also applies to: 5436-5436

go/apps/api/routes/v2_keys_verify_key/403_test.go (2)

35-57: 403 scenario is well-covered; consider asserting error code and minimizing detail coupling

This subtest nicely checks the 403 path and ensures the error detail hints at both wildcard and specific verify permissions, but you might also assert the structured error code (e.g., FORBIDDEN) to make the contract stronger and rely a bit less on the exact phrasing of Error.Detail, which can be more brittle over time.


84-122: VALID-path subtests are solid; table-driven grouping could reduce repetition

Both VALID cases (wildcard vs specific API permission) are clearly expressed and assert the important bits (Status, Code, Valid), but they share almost all setup and assertions; if you touch this again, you could fold the four scenarios into a small table-driven test to cut duplication and align with the repo’s direction toward table-based tests. Based on learnings, there’s already an issue open to track that broader refactor.

apps/dashboard/app/(app)/[workspaceSlug]/settings/team/members.tsx (2)

26-28: ConfirmPopover anchoring and state wiring look good; consider clearing state on close

The shared ConfirmPopover anchored via anchorRef and driven by isConfirmPopoverOpen is a nice improvement over per-row confirms. One small cleanup would be to reset currentMembership and anchorEl when the popover is closed, so you don’t retain stale references after cancel/close:

const handleConfirmPopoverOpenChange = (open: boolean) => {
  setIsConfirmPopoverOpen(open);
  if (!open) {
    setCurrentMembership(null);
    setAnchorEl(null);
  }
};

// …

<ConfirmPopover
  isOpen={isConfirmPopoverOpen}
  onOpenChange={handleConfirmPopoverOpenChange}
  triggerRef={anchorRef}
  // ...
/>

This keeps the state model tightly aligned with the popover’s visibility and avoids holding onto an old DOM node unnecessarily.

Also applies to: 34-34, 75-97


14-14: Align loading state UI with existing Empty-based patterns (optional)

You’re importing both Empty and Loading from @unkey/ui, and already use Empty for the “no team members” state. In other parts of this repo, Empty is sometimes used as the container even for loading states (wrapping a loader) for a consistent look and feel (based on learnings). You could optionally refactor the isLoading branch to reuse <Empty> as the wrapper instead of a custom bordered div, to keep visual parity with other settings views.

This is stylistic only; current code is functionally fine.

Also applies to: 47-52, 64-71

apps/dashboard/app/api/webhooks/stripe/route.ts (2)

33-92: Automated renewal detector is conservative; watch multi‑item subs and Stripe semantics

The renewal detection logic is intentionally conservative (falls back to “manual change” unless it’s clearly just period dates + items/latest_invoice), which is a good safety bias. Two things to keep in mind:

  • It only inspects items.data[0], so if you ever support multi‑item subscriptions, this could misclassify some changes and might need to be generalized.
  • Correctness relies on how Stripe populates event.data.previous_attributes for customer.subscription.updated (especially for auto‑renew vs scheduled upgrades/downgrades). It’s worth double‑checking a few real webhook samples in staging to ensure the key set/shape matches these assumptions.

No blocking issues, but I’d validate this against live/staging events before relying on it heavily.


181-187: Early return for automated renewals looks correct; consider status code/message

Skipping DB/quota updates and notifications for detected automated renewals matches the helper’s intent and avoids noisy Slack alerts. The only minor nit is returning "Skip" with HTTP 201 (Created) — a 200 with a more conventional body (e.g. "OK") might be clearer for logs/metrics, but Stripe will treat any 2xx as success either way.

go/pkg/db/queries/identity_list.sql (1)

11-29: Cursor condition i.id >= sqlc.arg(id_cursor) can duplicate rows across pages

With AND i.id >= sqlc.arg(id_cursor), if the caller uses the last seen id as the next page’s cursor (typical pattern), that row will reappear on the following page. If the intended behavior is “start after this id”, consider switching to > (or adjusting how the cursor value is computed) to avoid duplicates:

AND i.id > sqlc.arg(id_cursor)

Please double‑check how the TRPC/API layer is populating id_cursor to ensure the pagination semantics are what you expect.

deployment/docker-compose.yaml (1)

83-99: Vault / ACME Vault config in docker-compose is coherent for dev use

The UNKEY_VAULT_* (bucket vault) and UNKEY_ACME_VAULT_* (bucket acme-vault) settings are consistent across apiv2, gw, agent, and ctrl and line up with the MinIO bucket name.

Since this compose file is dev‑only, hardcoded master keys and API keys are acceptable here, but make sure these exact values are not reused in any staging or production environment.

Also applies to: 135-143, 276-287, 377-387

go/pkg/db/identity_list.sql_generated.go (1)

8-11: Generated ListIdentities row/scan logic matches the SQL, but Ratelimits typing is very loose

The generated SQL string, params, and scan order all line up with the updated identity_list.sql, including the ratelimits JSON column and the id_cursor filter. From a generation standpoint this looks correct.

Two follow‑ups to consider (in identity_list.sql / sqlc config rather than editing this file directly):

  • Cursor semantics: As noted on the SQL file, AND i.id >= ? will re‑include the boundary row if callers pass the last seen id as the next cursor. If you intend “start after cursor”, switch to > (and re‑generate).
  • Ratelimits type: Ratelimits interface{} will typically come back as []byte from MySQL JSON. If callers always treat this as opaque JSON, consider tightening it to []byte or json.RawMessage via sqlc type overrides so you get compile‑time guarantees.

Also applies to: 13-43, 52-62, 95-119

go/k8s/manifests/dashboard.yaml (1)

26-29: Agent dependency wait and env wiring look correct; ensure token is overridden outside dev

Updating the init container to wait on both planetscale:3900 and agent:8080 is consistent with the new agent dependency, and wiring AGENT_URL / AGENT_TOKEN into the dashboard container matches that integration.

Treat "agent-auth-secret" like CTRL_API_KEY and ensure it’s overridden via Kubernetes secrets or per‑env values in non‑local deployments so real credentials aren’t baked into manifests.

Also applies to: 55-59

go/apps/ctrl/config.go (1)

128-135: Consider adding basic validation for new Vault config fields

The new Vault and ACME Vault fields on Config are wired into the struct but aren’t yet validated in Validate(). If/when Vault becomes a required dependency (for env vars or ACME certificates), it would be safer to assert at startup that:

  • VaultMasterKeys (and AcmeVaultMasterKeys, if enabled) are non‑empty.
  • VaultS3 / AcmeVaultS3 have URL, bucket, and credentials populated.

That keeps misconfiguration from surfacing only at deploy/cert runtime.

go/pkg/db/schema.sql (1)

319-332: Consider indexes for common query patterns.

The environment_variables table structure is well-designed with appropriate constraints. However, if queries frequently filter by workspace_id or environment_id alone (without key), consider adding indexes for these columns in the source schema.

Since this is a generated file, any index additions should be made in internal/db/src/schema and regenerated.

apps/dashboard/lib/trpc/routers/deploy/env-vars/update.ts (2)

66-73: Consider adding workspaceId to the UPDATE WHERE clause.

The update only filters by id, but the initial query already validated workspace ownership. While functionally safe (the query at lines 28-38 ensures authorization), adding workspaceId to the WHERE clause provides defense-in-depth against TOCTOU issues if the env var is reassigned between the check and update.

       await db
         .update(schema.environmentVariables)
         .set({
           key: input.key ?? envVar.key,
           value: encrypted,
           type: input.type,
         })
-        .where(eq(schema.environmentVariables.id, input.envVarId));
+        .where(
+          and(
+            eq(schema.environmentVariables.id, input.envVarId),
+            eq(schema.environmentVariables.workspaceId, ctx.workspace.id),
+          ),
+        );

26-84: Mutation lacks a return value.

The mutation completes without returning anything. Consider returning { success: true } or the updated env var id to confirm the operation and enable client-side optimistic updates.

apps/dashboard/lib/trpc/routers/deploy/env-vars/create.ts (2)

50-69: Duplicate key conflicts will surface as generic INTERNAL_SERVER_ERROR.

The DB has a unique index on (environmentId, key), but if a duplicate key is submitted, the insert will fail and be caught by the generic error handler at lines 70-79, returning "Failed to create environment variables" without indicating which key conflicted.

Consider catching the duplicate key error specifically and returning a more descriptive message:

} catch (error) {
  if (error instanceof TRPCError) {
    throw error;
  }
  
  // Check for duplicate key constraint violation
  if (error instanceof Error && error.message.includes("Duplicate entry")) {
    throw new TRPCError({
      code: "CONFLICT",
      message: "One or more environment variable keys already exist",
    });
  }

  throw new TRPCError({
    code: "INTERNAL_SERVER_ERROR",
    message: "Failed to create environment variables",
  });
}

31-80: Mutation lacks a return value.

Similar to updateEnvVar, this mutation doesn't return anything. Consider returning the created env var IDs to enable client-side cache updates.

       await db.insert(schema.environmentVariables).values(encryptedVars);
+
+      return { ids: encryptedVars.map((v) => v.id) };
     } catch (error) {
internal/db/src/schema/environment_variables.ts (1)

41-44: Relation named project but references environments table.

The relation name is misleading - it should be environment to match what it actually references.

-  project: one(environments, {
+  environment: one(environments, {
     fields: [environmentVariables.environmentId],
     references: [environments.id],
   }),
go/k8s/manifests/agent.yaml (1)

18-54: Add securityContext to prevent privilege escalation.

Static analysis flagged missing security hardening. Add a securityContext to run as non-root and prevent privilege escalation.

     spec:
+      securityContext:
+        runAsNonRoot: true
+        runAsUser: 1000
       containers:
         - name: agent
           image: unkey-agent:latest
           imagePullPolicy: Never
+          securityContext:
+            allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            capabilities:
+              drop:
+                - ALL
           ports:
apps/dashboard/lib/trpc/routers/deploy/env-vars/decrypt.ts (1)

27-27: Consider using .query() instead of .mutation() for read-only operation.

This procedure only reads and decrypts data without modifying state. Semantically, .query() would be more appropriate than .mutation(), and it enables caching on the client side.

apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/env-variables-section/components/env-var-form.tsx (1)

22-32: Unused projectId parameter.

The projectId prop is accepted but aliased to _projectId and never used. If it's not needed for the update mutation, consider removing it from the props interface to avoid confusion.

 type EnvVarFormProps = {
   envVarId: string;
   initialData: EnvVarFormData;
-  projectId: string;
   getExistingEnvVar: (key: string, excludeId?: string) => EnvVar | undefined;
   onSuccess: () => void;
   onCancel: () => void;
   excludeId?: string;
   autoFocus?: boolean;
   className?: string;
 };

 export function EnvVarForm({
   envVarId,
   initialData,
-  projectId: _projectId,
   getExistingEnvVar,
apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/env-variables-section/components/env-var-inputs.tsx (1)

25-35: Consider calling keyRegister.onChange to maintain react-hook-form's internal tracking.

When spreading keyRegister but overriding onChange, the original onChange from register() is not invoked. While setValue updates the form value, react-hook-form's internal event tracking (like touched state on change) may not work as expected.

             onChange={(e) => {
               if (keyDisabled) {
                 return;
               }
+              // Call the original onChange to maintain form tracking
+              keyRegister.onChange(e);
               // Auto-uppercase the key and replace spaces with underscores
-              // nothing else should be valid in an env var...
               setValue("key", e.target.value.toUpperCase().replace(/ /g, "_"), {
                 shouldValidate: true,
               });
             }}
apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/env-variables-section/add-env-vars.tsx (1)

206-209: Empty catch block silently swallows errors.

While toast.promise handles displaying the error to the user, having an empty catch block is a code smell. Consider adding a comment explaining the intent or removing the try-catch if it's not needed.

     try {
       await mutation;
       onSuccess();
-    } catch {}
+    } catch {
+      // Error already displayed via toast.promise
+    }
   };
apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/env-variables-section/env-var-row.tsx (1)

119-122: Consider clearing decryptedValue on cancel for consistency.

When canceling an edit, decryptedValue is not cleared (unlike on success). This means if the user reveals → edits → cancels → reveals again, the cached decrypted value may be stale if another user modified it.

         onCancel={() => {
           setIsEditing(false);
           setIsRevealed(false);
+          setDecryptedValue(undefined);
         }}

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)
apps/dashboard/lib/trpc/routers/identity/query.ts (1)

5-11: Search, relations, and totalCount logic looks good; consider optimizing count query

Overall this refactor hangs together well:

  • Optional search in the payload, plus the shared buildFilterConditions (workspace + not-deleted + optional LIKE on externalId/id with escapeLike), gives predictable search behavior and correctly reuses conditions for both the list and the count. Based on learnings, centralizing LIKE-escaping like this is the right move for avoiding wildcard surprises while keeping Drizzle’s parameterization intact.
  • Loading keys and ratelimits relations with only id selected, and threading them through IdentityResponseSchema, matches the new UI needs without overfetching.
  • Cursor-based pagination (lt(identity.id, cursor) and limit + 1 with hasMore/nextCursor) is implemented in the standard pattern and looks correct.

One thing to improve (previously raised) is the totalCount implementation:

  • totalCount is computed via db.query.identities.findMany(...).length, which loads all matching rows’ IDs into memory just to count them. For large workspaces this becomes unnecessarily expensive.

Consider switching to a dedicated COUNT query (or Drizzle’s count helper, if available for your version) that returns only the aggregate:

-      const countQuery = await db.query.identities.findMany({
-        where: buildFilterConditions,
-        columns: {
-          id: true,
-        },
-      });
-
-      const totalCount = countQuery.length;
+      const { identity } = await import("@unkey/db/src/schema");
+      const countResult = await db
+        .select({ count: sql<number>`count(*)` })
+        .from(identity)
+        .where((table, helpers) => buildFilterConditions(table, helpers));
+
+      const totalCount = countResult[0]?.count ?? 0;

You can adapt this to whatever count utility your Drizzle version supports.

Drizzle ORM "relational query" or "db.query.*" API: what is the recommended way to perform an efficient COUNT(*) with the same where conditions, instead of fetching all rows and using `.length`?

Also applies to: 21-23, 39-40, 61-69, 71-83, 84-95, 106-116, 120-125

🧹 Nitpick comments (3)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx (1)

17-27: Optional: DRY up clipboard copy logic

The two menu items duplicate the same navigator.clipboard.writeText + toast + error-handling flow. Consider extracting a small helper to reduce repetition and keep messages consistent:

+const copyWithToast = async (value: string, success: string) => {
+  try {
+    await navigator.clipboard.writeText(value);
+    toast.success(success);
+  } catch (error) {
+    console.error("Failed to copy to clipboard:", error);
+    toast.error("Failed to copy to clipboard");
+  }
+};
+
 export const IdentityTableActions = ({ identity }: { identity: Identity }) => {
   const menuItems: MenuItem[] = useMemo(
     () => [
       {
         id: "copy-identity-id",
         label: "Copy identity ID",
         icon: <Clone iconSize="md-medium" />,
-        onClick: () => {
-          navigator.clipboard
-            .writeText(identity.id)
-            .then(() => {
-              toast.success("Identity ID copied to clipboard");
-            })
-            .catch((error) => {
-              console.error("Failed to copy to clipboard:", error);
-              toast.error("Failed to copy to clipboard");
-            });
-        },
+        onClick: () => void copyWithToast(identity.id, "Identity ID copied to clipboard"),
       },
       {
         id: "copy-external-id",
         label: "Copy external ID",
         icon: <Clone iconSize="md-medium" />,
-        onClick: () => {
-          navigator.clipboard
-            .writeText(identity.externalId)
-            .then(() => {
-              toast.success("External ID copied to clipboard");
-            })
-            .catch((error) => {
-              console.error("Failed to copy to clipboard:", error);
-              toast.error("Failed to copy to clipboard");
-            });
-        },
+        onClick: () => void copyWithToast(identity.externalId, "External ID copied to clipboard"),
       },
     ],
     [identity.id, identity.externalId],
   );

Also applies to: 33-43

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1)

1-31: LLMSearch integration with useQueryState looks solid

Hooking LLMSearch into the "search" query param via useQueryState matches the table’s usage and should keep list + URL in sync. If you want to avoid queries that differ only by surrounding whitespace, you could optionally setSearch(query.trim()) in onSearch, but it’s not required for correctness.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1)

1-13: Simple composition of controls + list looks good

The IdentitiesClient wrapper cleanly composes controls and table into a vertical layout; there’s no functional risk here. If IdentitiesListControls / IdentitiesList are already client components and this is only used under the client Page, you could consider dropping the local "use client" directive to avoid an extra client boundary, but that’s purely optional.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22cc6e1 and 0e8124f.

📒 Files selected for processing (18)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/last-used.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx (0 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx (0 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx (0 hunks)
  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/identity/query.ts (5 hunks)
  • apps/dashboard/lib/trpc/routers/identity/search.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/index.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/utils/sql.ts (1 hunks)
💤 Files with no reviewable changes (3)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/dashboard/lib/trpc/routers/identity/search.ts
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/last-used.tsx
  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx
🧰 Additional context used
🧠 Learnings (12)
📓 Common learnings
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.
📚 Learning: 2025-07-28T20:38:53.244Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx:322-341
Timestamp: 2025-07-28T20:38:53.244Z
Learning: In apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx, mcstepp prefers to keep hardcoded endpoint logic in the getDiffType function during POC phases for demonstrating diff functionality, rather than implementing a generic diff algorithm. This follows the pattern of keeping simplified implementations for demonstration purposes.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3797
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/control-cloud/index.tsx:1-4
Timestamp: 2025-08-18T10:28:47.391Z
Learning: In Next.js App Router, components that use React hooks don't need their own "use client" directive if they are rendered within a client component that already has the directive. The client boundary propagates to child components.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2025-06-19T13:01:55.338Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3315
File: apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/general-setup.tsx:40-50
Timestamp: 2025-06-19T13:01:55.338Z
Learning: In the create-key form's GeneralSetup component, the Controller is intentionally bound to "identityId" as the primary field while "externalId" is set explicitly via setValue. The ExternalIdField component has been designed to handle this pattern where it receives identityId as its value prop but manages both identityId and externalId through its onChange callback.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx
📚 Learning: 2025-08-25T12:56:59.310Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3834
File: apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts:59-66
Timestamp: 2025-08-25T12:56:59.310Z
Learning: In the ratelimit namespace query system (apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts), the nameQuery filter is designed as an array for future extensibility to support multiple filters, but currently only the first filter (index 0) is processed. This is intentional future-proofing.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-02-27T14:35:15.251Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2918
File: apps/dashboard/lib/trpc/routers/api/overview-api-search.ts:0-0
Timestamp: 2025-02-27T14:35:15.251Z
Learning: When using Drizzle ORM with LIKE queries, use the sql`` tagged template syntax (e.g., sql`${table.name} LIKE ${`%${input.query}%`}`) instead of direct string interpolation to prevent SQL injection attacks.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
  • apps/dashboard/lib/trpc/routers/utils/sql.ts
📚 Learning: 2025-06-25T19:51:15.995Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3369
File: apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts:67-67
Timestamp: 2025-06-25T19:51:15.995Z
Learning: Drizzle ORM automatically handles SQL parameterization and escaping for queries, including LIKE queries with direct string interpolation (e.g., `like(schema.keys.name, \`%${filter.value}%\`)`), making them safe from SQL injection attacks without requiring explicit escaping or sql`` tagged template syntax.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
  • apps/dashboard/lib/trpc/routers/utils/sql.ts
📚 Learning: 2025-02-10T14:12:17.261Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2883
File: apps/dashboard/app/(app)/logs/components/controls/components/logs-search/index.tsx:29-31
Timestamp: 2025-02-10T14:12:17.261Z
Learning: In the logs search component's error handling, error messages are deliberately wrapped in single quotes within template literals for visual distinction (e.g. "Unable to process your search request 'some error message'").

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-12-05T12:05:33.143Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4484
File: go/pkg/db/queries/identity_list.sql:11-24
Timestamp: 2025-12-05T12:05:33.143Z
Learning: In go/pkg/db/queries/identity_list.sql and similar identity ratelimit queries, the RatelimitInfo struct's key_id and identity_id fields can be omitted from JSON_OBJECT construction because: (1) these are identity-level ratelimits where key_id doesn't apply, and (2) identity_id is redundant since it's already known from the parent identity context. Zero values for these fields are acceptable and intentional.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2025-09-23T17:40:44.944Z
Learnt from: perkinsjr
Repo: unkeyed/unkey PR: 4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:40:44.944Z
Learning: In the Unkey dashboard authorization section navigation components, the maintainer prefers to wrap entire navbars in Suspense rather than scoping Suspense to individual components, even if it blocks the whole navigation during loading.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
🧬 Code graph analysis (7)
apps/dashboard/lib/trpc/routers/index.ts (1)
apps/dashboard/lib/trpc/routers/identity/latestVerification.ts (1)
  • identityLastVerificationTime (10-54)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (2)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (1)
  • IdentitiesListControls (4-12)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1)
  • IdentitiesList (47-326)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (5)
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/create-key.schema.ts (1)
  • FormValues (351-351)
internal/ui/src/components/toaster.tsx (1)
  • toast (29-29)
apps/dashboard/lib/collections/index.ts (1)
  • reset (76-84)
internal/ui/src/components/dialog/dialog-container.tsx (1)
  • DialogContainer (66-66)
internal/ui/src/components/buttons/button.tsx (1)
  • Button (439-439)
apps/dashboard/lib/trpc/routers/identity/query.ts (3)
apps/dashboard/lib/trpc/routers/utils/sql.ts (1)
  • escapeLike (15-17)
internal/rbac/src/queries.ts (2)
  • or (46-50)
  • and (52-56)
apps/dashboard/lib/db.ts (1)
  • db (5-26)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1)
internal/ui/src/components/llm-search/index.tsx (1)
  • LLMSearch (176-176)
apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1)
  • IdentitiesClient (6-13)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx (2)
apps/dashboard/lib/trpc/routers/identity/query.ts (1)
  • IdentityResponseSchema (13-23)
apps/dashboard/components/logs/table-action.popover.tsx (2)
  • MenuItem (19-29)
  • TableActionPopover (36-169)
🪛 GitHub Actions: autofix.ci
apps/dashboard/lib/trpc/routers/identity/query.ts

[error] 44-44: lint/correctness/noUnusedVariables: This variable is unused.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Dashboard / Test Dashboard
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
apps/dashboard/lib/trpc/routers/utils/sql.ts (1)

1-17: escapeLike implementation and docs look correct

The escape order and example output align, and this is a good centralization of LIKE-escape logic for search. No changes needed.

apps/dashboard/lib/trpc/routers/index.ts (1)

60-60: Wiring identity.latestVerification matches existing patterns

Importing identityLastVerificationTime and exposing it as identity.latestVerification is consistent with how api.keys.latestVerification is wired and keeps the router surface coherent.

Also applies to: 340-347

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx (1)

10-49: Overall component structure and popover integration look good

Aside from the import/type fix above, the IdentityTableActions component cleanly wires MenuItems into TableActionPopover, uses useMemo with appropriate deps (identity.id, identity.externalId), and provides clear success/error toasts for both identity ID and external ID copy actions.

apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)

1-20: Identities page wiring and beta gate look correct

The page-level client boundary, identities beta feature check, and composition of Navigation + IdentitiesClient align with the described flow and should behave as expected with dynamic data.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (2)

63-92: CreateIdentity mutation flow and JSON meta handling align with requirements

The mutation wiring matches the spec: success toast, identities list invalidation via utils.identity.query.invalidate(), dialog close, and form reset, plus 409 → field error for duplicate external IDs. Parsing meta to JSON (or sending null on empty/whitespace) is consistent with the zod meta refinements that already enforce valid JSON and <1MB. Overall this flow looks good.


94-155: Dialog UX and form integration look consistent with dashboard patterns

The NavbarActionButton entry point, DialogContainer with a single primary submit button wired via form="create-identity-form", field-level error display for externalId and meta, and helper copy about associating identities with keys/ratelimits all read well and fulfill the described “Create Identity” flow. No functional issues stand out here.

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: 1

♻️ Duplicate comments (1)
apps/dashboard/lib/trpc/routers/identity/query.ts (1)

1-1: Drop unused lt helper from buildFilterConditions (and import) to fix lint

In buildFilterConditions, lt is destructured but never used, which matches the noUnusedVariables lint failure:

-      const buildFilterConditions = (identity: any, { and, eq, or, like, lt }: any) => {
+      const buildFilterConditions = (identity: any, { and, eq, or, like }: any) => {

Since you already get lt from helpers inside the where callback:

const { and, lt } = helpers;

the top‑level lt import is redundant and can be removed as well:

-import { and, count, db, eq, like, lt, or, schema } from "@/lib/db";
+import { and, count, db, eq, like, or, schema } from "@/lib/db";

This keeps the helper generic, removes the unused variable, and avoids future lint errors on the unused import.

After applying this, run the linter again to ensure all noUnusedVariables warnings around lt are cleared.

Also applies to: 66-83, 86-97

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e8124f and 6f7b92e.

📒 Files selected for processing (2)
  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/identity/query.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.
📚 Learning: 2025-08-25T12:56:59.310Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3834
File: apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts:59-66
Timestamp: 2025-08-25T12:56:59.310Z
Learning: In the ratelimit namespace query system (apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts), the nameQuery filter is designed as an array for future extensibility to support multiple filters, but currently only the first filter (index 0) is processed. This is intentional future-proofing.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-02-27T14:35:15.251Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2918
File: apps/dashboard/lib/trpc/routers/api/overview-api-search.ts:0-0
Timestamp: 2025-02-27T14:35:15.251Z
Learning: When using Drizzle ORM with LIKE queries, use the sql`` tagged template syntax (e.g., sql`${table.name} LIKE ${`%${input.query}%`}`) instead of direct string interpolation to prevent SQL injection attacks.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-06-25T19:51:15.995Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3369
File: apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts:67-67
Timestamp: 2025-06-25T19:51:15.995Z
Learning: Drizzle ORM automatically handles SQL parameterization and escaping for queries, including LIKE queries with direct string interpolation (e.g., `like(schema.keys.name, \`%${filter.value}%\`)`), making them safe from SQL injection attacks without requiring explicit escaping or sql`` tagged template syntax.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-02-10T14:12:17.261Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2883
File: apps/dashboard/app/(app)/logs/components/controls/components/logs-search/index.tsx:29-31
Timestamp: 2025-02-10T14:12:17.261Z
Learning: In the logs search component's error handling, error messages are deliberately wrapped in single quotes within template literals for visual distinction (e.g. "Unable to process your search request 'some error message'").

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-12-05T12:05:33.143Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4484
File: go/pkg/db/queries/identity_list.sql:11-24
Timestamp: 2025-12-05T12:05:33.143Z
Learning: In go/pkg/db/queries/identity_list.sql and similar identity ratelimit queries, the RatelimitInfo struct's key_id and identity_id fields can be omitted from JSON_OBJECT construction because: (1) these are identity-level ratelimits where key_id doesn't apply, and (2) identity_id is redundant since it's already known from the parent identity context. Zero values for these fields are acceptable and intentional.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-06-24T13:29:10.129Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3401
File: apps/dashboard/app/(app)/logs/filters.query-params.ts:10-0
Timestamp: 2025-06-24T13:29:10.129Z
Learning: The `buildQueryParams` function in `apps/dashboard/app/(app)/logs/filters.query-params.ts` calls `useFilters()` hook inside it, but this is valid because the function is only called from within other React hooks, maintaining the Rules of Hooks compliance.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-05-15T15:59:20.955Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3242
File: apps/dashboard/app/(app)/apis/[apiId]/api-id-navbar.tsx:230-266
Timestamp: 2025-05-15T15:59:20.955Z
Learning: Avoid using `any` type in TypeScript code as it defeats the purpose of type safety and will cause linter issues in the future. Instead, create proper interfaces or utilize existing type definitions, especially for complex nested objects.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2024-10-23T16:21:47.395Z
Learnt from: p6l-richard
Repo: unkeyed/unkey PR: 2085
File: apps/www/components/glossary/search.tsx:16-20
Timestamp: 2024-10-23T16:21:47.395Z
Learning: For the `FilterableCommand` component in `apps/www/components/glossary/search.tsx`, refactoring type definitions into an interface is not necessary at this time.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
🧬 Code graph analysis (1)
apps/dashboard/lib/trpc/routers/identity/query.ts (4)
internal/db/src/index.ts (1)
  • schema (8-8)
apps/dashboard/lib/trpc/routers/utils/sql.ts (1)
  • escapeLike (15-17)
internal/rbac/src/queries.ts (2)
  • or (46-50)
  • and (52-56)
apps/dashboard/lib/db.ts (1)
  • db (5-26)
🪛 GitHub Actions: autofix.ci
apps/dashboard/lib/trpc/routers/identity/query.ts

[error] 51-54: lint/style/noNonNullAssertion: Forbidden non-null assertion.


[error] 68-68: lint/correctness/noUnusedVariables: This variable is unused.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test Dashboard / Test Dashboard
🔇 Additional comments (1)
apps/dashboard/lib/trpc/routers/identity/query.ts (1)

13-23: Identity schema, relations, and totalCount wiring look consistent

The extensions to IdentityResponseSchema (keys, ratelimits) and IdentitiesResponse (totalCount) line up with the findMany with relations and the transformedIdentities/return shape. No obvious type or shape mismatches here.

Also applies to: 25-30, 98-108, 120-130, 134-139

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: 4

♻️ Duplicate comments (2)
apps/dashboard/lib/trpc/routers/identity/query.ts (2)

66-83: Remove unused lt parameter to fix pipeline failure.

The lt helper is destructured on Line 68 but never used within buildFilterConditions, triggering the noUnusedVariables lint error. The lt operator is only needed in the main query (line 87, 93) for cursor pagination, not in this helper function.

Apply this diff:

       // Helper function to build filter conditions for query API
       // biome-ignore lint/suspicious/noExplicitAny: Leave it as is for now
-      const buildFilterConditions = (identity: any, { and, eq, or, like, lt }: any) => {
+      const buildFilterConditions = (identity: any, { and, eq, or, like }: any) => {
         const conditions = [eq(identity.workspaceId, workspaceId), eq(identity.deleted, false)];

48-56: Remove non-null assertion to fix pipeline failure.

The or(...)! on Line 54 triggers the noNonNullAssertion lint error. The file already demonstrates the correct pattern in lines 73-79 where the result is checked before pushing.

Apply this diff to use the safe pattern consistently:

       if (search) {
         const escapedSearch = escapeLike(search);
-        baseConditions.push(
-          or(
-            like(schema.identities.externalId, `%${escapedSearch}%`),
-            like(schema.identities.id, `%${escapedSearch}%`),
-          )!,
-        );
+        const searchCondition = or(
+          like(schema.identities.externalId, `%${escapedSearch}%`),
+          like(schema.identities.id, `%${escapedSearch}%`),
+        );
+        if (searchCondition) {
+          baseConditions.push(searchCondition);
+        }
       }
🧹 Nitpick comments (3)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (2)

18-24: Example queries may mislead users.

Some example queries suggest capabilities that the backend doesn't support:

  • "Find identity with ID 'user_123'" - backend only searches externalId, not identity id
  • "Show identities created in the last week" - backend doesn't support date filtering

Consider aligning examples with actual functionality:

 exampleQueries={[
-  "Find identity with ID 'user_123'",
+  "user_123",
   "Show identities with external ID containing 'test'",
-  "Find identities with external ID 'john@example.com'",
-  "Show identities created in the last week",
+  "john@example.com",
+  "test-identity",
 ]}

24-24: Consider wiring isLoading to actual search state.

Hardcoding isLoading={false} provides no feedback during search operations. If the parent component or a query hook tracks loading state, passing it here would improve UX.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx (1)

28-34: Simplify nested animation classes.

The container on Line 29 applies animate-pulse, making the individual animate-pulse classes on Lines 30-32 redundant. The pulsing animation will cascade to children.

Apply this diff to remove redundant classes:

 export const LastUsedColumnSkeleton = () => (
   <div className="px-1.5 rounded-md flex gap-2 items-center w-[140px] h-[22px] bg-grayA-3 animate-pulse">
-    <div className="h-2 w-2 bg-grayA-3 rounded-full animate-pulse" />
-    <div className="h-2 w-12 bg-grayA-3 rounded animate-pulse" />
-    <div className="h-2 w-12 bg-grayA-3 rounded animate-pulse" />
+    <div className="h-2 w-2 bg-gray-6 rounded-full" />
+    <div className="h-2 w-12 bg-gray-6 rounded" />
+    <div className="h-2 w-12 bg-gray-6 rounded" />
   </div>
 );

Note: Changed to bg-gray-6 for visual contrast against the bg-grayA-3 container, creating a more realistic skeleton appearance.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f7b92e and b570061.

📒 Files selected for processing (18)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/last-used.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx (0 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx (0 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx (0 hunks)
  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/identity/query.ts (5 hunks)
  • apps/dashboard/lib/trpc/routers/identity/search.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/index.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/utils/sql.ts (1 hunks)
💤 Files with no reviewable changes (3)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/dashboard/lib/trpc/routers/index.ts
  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/last-used.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx
🧰 Additional context used
🧠 Learnings (22)
📓 Common learnings
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.
📚 Learning: 2025-07-28T20:38:53.244Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx:322-341
Timestamp: 2025-07-28T20:38:53.244Z
Learning: In apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx, mcstepp prefers to keep hardcoded endpoint logic in the getDiffType function during POC phases for demonstrating diff functionality, rather than implementing a generic diff algorithm. This follows the pattern of keeping simplified implementations for demonstration purposes.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3797
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/control-cloud/index.tsx:1-4
Timestamp: 2025-08-18T10:28:47.391Z
Learning: In Next.js App Router, components that use React hooks don't need their own "use client" directive if they are rendered within a client component that already has the directive. The client boundary propagates to child components.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2024-10-20T07:05:55.471Z
Learnt from: chronark
Repo: unkeyed/unkey PR: 2294
File: apps/api/src/pkg/keys/service.ts:268-271
Timestamp: 2024-10-20T07:05:55.471Z
Learning: In `apps/api/src/pkg/keys/service.ts`, `ratelimitAsync` is a table relation, not a column selection. When querying, ensure that table relations are included appropriately, not as columns.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/search.ts
📚 Learning: 2025-09-25T18:49:11.451Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 4010
File: apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts:39-44
Timestamp: 2025-09-25T18:49:11.451Z
Learning: In apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts and similar files, mcstepp prefers to keep the demo API key authentication simple without additional validation complexity, since it's temporary code that will be replaced after the demo phase.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/search.ts
📚 Learning: 2024-11-29T15:15:47.308Z
Learnt from: chronark
Repo: unkeyed/unkey PR: 2693
File: apps/api/src/routes/v1_keys_updateKey.ts:350-368
Timestamp: 2024-11-29T15:15:47.308Z
Learning: In `apps/api/src/routes/v1_keys_updateKey.ts`, the code intentionally handles `externalId` and `ownerId` separately for clarity. The `ownerId` field will be removed in the future, simplifying the code.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/search.ts
📚 Learning: 2025-06-19T11:48:05.070Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3324
File: apps/dashboard/app/(app)/authorization/roles/components/table/components/actions/keys-table-action.popover.constants.tsx:17-18
Timestamp: 2025-06-19T11:48:05.070Z
Learning: In the authorization roles refactor, the RoleBasic type uses `roleId` as the property name for the role identifier, not `id`. This is consistent throughout the codebase in apps/dashboard/lib/trpc/routers/authorization/roles/query.ts.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/search.ts
📚 Learning: 2025-10-30T15:10:52.743Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/search.ts
📚 Learning: 2025-12-05T12:05:33.143Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4484
File: go/pkg/db/queries/identity_list.sql:11-24
Timestamp: 2025-12-05T12:05:33.143Z
Learning: In go/pkg/db/queries/identity_list.sql and similar identity ratelimit queries, the RatelimitInfo struct's key_id and identity_id fields can be omitted from JSON_OBJECT construction because: (1) these are identity-level ratelimits where key_id doesn't apply, and (2) identity_id is redundant since it's already known from the parent identity context. Zero values for these fields are acceptable and intentional.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/search.ts
  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx
📚 Learning: 2025-09-23T17:40:44.944Z
Learnt from: perkinsjr
Repo: unkeyed/unkey PR: 4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:40:44.944Z
Learning: In the Unkey dashboard authorization section navigation components, the maintainer prefers to wrap entire navbars in Suspense rather than scoping Suspense to individual components, even if it blocks the whole navigation during loading.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2024-12-03T14:17:08.016Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2143
File: apps/dashboard/app/(app)/logs/logs-page.tsx:77-83
Timestamp: 2024-12-03T14:17:08.016Z
Learning: The `<LogsTable />` component already implements virtualization to handle large datasets efficiently.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2025-08-25T12:56:59.310Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3834
File: apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts:59-66
Timestamp: 2025-08-25T12:56:59.310Z
Learning: In the ratelimit namespace query system (apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts), the nameQuery filter is designed as an array for future extensibility to support multiple filters, but currently only the first filter (index 0) is processed. This is intentional future-proofing.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-02-27T14:35:15.251Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2918
File: apps/dashboard/lib/trpc/routers/api/overview-api-search.ts:0-0
Timestamp: 2025-02-27T14:35:15.251Z
Learning: When using Drizzle ORM with LIKE queries, use the sql`` tagged template syntax (e.g., sql`${table.name} LIKE ${`%${input.query}%`}`) instead of direct string interpolation to prevent SQL injection attacks.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
  • apps/dashboard/lib/trpc/routers/utils/sql.ts
📚 Learning: 2025-06-25T19:51:15.995Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3369
File: apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts:67-67
Timestamp: 2025-06-25T19:51:15.995Z
Learning: Drizzle ORM automatically handles SQL parameterization and escaping for queries, including LIKE queries with direct string interpolation (e.g., `like(schema.keys.name, \`%${filter.value}%\`)`), making them safe from SQL injection attacks without requiring explicit escaping or sql`` tagged template syntax.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
  • apps/dashboard/lib/trpc/routers/utils/sql.ts
📚 Learning: 2025-02-10T14:12:17.261Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2883
File: apps/dashboard/app/(app)/logs/components/controls/components/logs-search/index.tsx:29-31
Timestamp: 2025-02-10T14:12:17.261Z
Learning: In the logs search component's error handling, error messages are deliberately wrapped in single quotes within template literals for visual distinction (e.g. "Unable to process your search request 'some error message'").

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-06-24T13:29:10.129Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3401
File: apps/dashboard/app/(app)/logs/filters.query-params.ts:10-0
Timestamp: 2025-06-24T13:29:10.129Z
Learning: The `buildQueryParams` function in `apps/dashboard/app/(app)/logs/filters.query-params.ts` calls `useFilters()` hook inside it, but this is valid because the function is only called from within other React hooks, maintaining the Rules of Hooks compliance.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-05-15T15:59:20.955Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3242
File: apps/dashboard/app/(app)/apis/[apiId]/api-id-navbar.tsx:230-266
Timestamp: 2025-05-15T15:59:20.955Z
Learning: Avoid using `any` type in TypeScript code as it defeats the purpose of type safety and will cause linter issues in the future. Instead, create proper interfaces or utilize existing type definitions, especially for complex nested objects.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2024-10-23T16:21:47.395Z
Learnt from: p6l-richard
Repo: unkeyed/unkey PR: 2085
File: apps/www/components/glossary/search.tsx:16-20
Timestamp: 2024-10-23T16:21:47.395Z
Learning: For the `FilterableCommand` component in `apps/www/components/glossary/search.tsx`, refactoring type definitions into an interface is not necessary at this time.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-05-15T15:57:02.128Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3242
File: apps/dashboard/app/(app)/apis/[apiId]/api-id-navbar.tsx:47-50
Timestamp: 2025-05-15T15:57:02.128Z
Learning: When reviewing code for Unkey, prefer using `Boolean()` over the double negation (`!!`) operator for boolean coercion, as their linter rules favor this pattern.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2024-10-23T16:19:42.049Z
Learnt from: p6l-richard
Repo: unkeyed/unkey PR: 2085
File: apps/www/components/glossary/search.tsx:41-57
Timestamp: 2024-10-23T16:19:42.049Z
Learning: For the `FilterableCommand` component in `apps/www/components/glossary/search.tsx`, adding error handling and loading states to the results list is not necessary.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx
🧬 Code graph analysis (4)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (2)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (1)
  • IdentitiesListControls (4-12)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1)
  • IdentitiesList (47-326)
apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1)
  • IdentitiesClient (6-13)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (2)
apps/dashboard/components/logs/controls-container.tsx (1)
  • ControlsContainer (1-7)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1)
  • IdentitiesSearch (6-31)
apps/dashboard/lib/trpc/routers/identity/query.ts (4)
internal/db/src/index.ts (1)
  • schema (8-8)
apps/dashboard/lib/trpc/routers/utils/sql.ts (1)
  • escapeLike (15-17)
internal/rbac/src/queries.ts (2)
  • or (46-50)
  • and (52-56)
apps/dashboard/lib/db.ts (1)
  • db (5-26)
🪛 GitHub Actions: autofix.ci
apps/dashboard/lib/trpc/routers/identity/query.ts

[error] 51-54: lint/style/noNonNullAssertion: Forbidden non-null assertion.


[error] 66-68: lint/correctness/noUnusedVariables: This variable is unused.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Dashboard / Test Dashboard
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (7)
apps/dashboard/lib/trpc/routers/identity/search.ts (1)

39-50: Good addition of relation preloading.

Eagerly loading keys and ratelimits with minimal column projection (id only) is efficient for count display while avoiding N+1 queries.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (1)

1-12: LGTM!

Clean composition following the established patterns from the logs controls. The structure leaves room for adding right-side controls (e.g., Create Identity button) if needed.

apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)

8-20: Simplified page composition looks good.

The refactor to delegate to IdentitiesClient while preserving the beta feature gate is clean. The client component structure works well with the useWorkspaceNavigation hook requirement.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1)

1-13: LGTM! Clean composition pattern.

The component serves as an appropriate client boundary for the identities feature, composing controls and list components in a straightforward layout. The "use client" directive is correctly placed at the file level.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (3)

29-45: LGTM! Effective code splitting for actions popover.

The dynamic import with ssr: false and loading placeholder is well-implemented, optimizing initial bundle size while providing a seamless loading experience.


76-81: Verify aggressive cache strategy is intentional.

The query is configured with staleTime: Number.POSITIVE_INFINITY and disabled refetching, meaning users won't see new identities created by others (or themselves in other tabs) until they manually refresh the page or trigger a new search.

If this is intentional for performance, consider adding a manual refresh action or reducing the stale time. Otherwise, consider enabling refetch on window focus for better UX:

     {
       getNextPageParam: (lastPage) => lastPage.nextCursor,
-      staleTime: Number.POSITIVE_INFINITY,
-      refetchOnMount: false,
-      refetchOnWindowFocus: false,
+      staleTime: 5 * 60 * 1000, // 5 minutes
+      refetchOnMount: false,
+      refetchOnWindowFocus: true,
     },

236-325: LGTM! Well-structured virtual table implementation.

The VirtualTable configuration correctly handles loading states, pagination, empty states, and skeleton rendering. The countInfoText now properly displays loaded count vs. total count (addressing previous feedback), and the empty state messaging adapts appropriately to search context.

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: 0

♻️ Duplicate comments (1)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1)

157-166: Add click guard to prevent multiple navigation attempts.

The link remains clickable even when isNavigating is true, which can cause multiple handleLinkClick calls and duplicate state updates.

Add an early return in the onClick handler:

                    <Link
                      className="font-mono group-hover:underline decoration-dotted text-accent-9"
                      href={`/${workspace.slug}/identities/${identity.id}`}
                      aria-disabled={isNavigating}
-                      onClick={() => {
+                      onClick={(e) => {
+                        if (isNavigating) {
+                          e.preventDefault();
+                          return;
+                        }
                         handleLinkClick(identity.id);
                       }}
                    >

Alternatively, add pointer-events-none to the className when navigating:

                    <Link
-                      className="font-mono group-hover:underline decoration-dotted text-accent-9"
+                      className={cn(
+                        "font-mono group-hover:underline decoration-dotted text-accent-9",
+                        isNavigating && "pointer-events-none opacity-50"
+                      )}
                      href={`/${workspace.slug}/identities/${identity.id}`}
                      aria-disabled={isNavigating}
                      onClick={() => {
                         handleLinkClick(identity.id);
                       }}
                    >
🧹 Nitpick comments (1)
apps/dashboard/lib/trpc/routers/identity/query.ts (1)

42-84: Extract shared filter logic to reduce duplication.

The filter conditions (workspace, deleted, search) are duplicated in baseConditions (lines 42–57) and buildFilterConditions (lines 67–84). While these serve different Drizzle APIs, the underlying filter logic is identical and must be kept in sync manually.

Consider extracting the filter criteria into a shared helper:

// Extract filter criteria
const getFilterCriteria = (search: string | undefined) => {
  return {
    workspaceId,
    deleted: false,
    search: search ? escapeLike(search) : undefined,
  };
};

const criteria = getFilterCriteria(search);

// For SQL builder API (count query)
const baseConditions = [
  eq(schema.identities.workspaceId, criteria.workspaceId),
  eq(schema.identities.deleted, criteria.deleted),
];

if (criteria.search) {
  const searchCondition = or(
    like(schema.identities.externalId, `%${criteria.search}%`),
    like(schema.identities.id, `%${criteria.search}%`),
  );
  if (searchCondition) {
    baseConditions.push(searchCondition);
  }
}

// For query API
const buildFilterConditions = (identity: any, { and, eq, or, like }: any) => {
  const conditions = [
    eq(identity.workspaceId, criteria.workspaceId),
    eq(identity.deleted, criteria.deleted),
  ];

  if (criteria.search) {
    const searchCondition = or(
      like(identity.externalId, `%${criteria.search}%`),
      like(identity.id, `%${criteria.search}%`),
    );
    if (searchCondition) {
      conditions.push(searchCondition);
    }
  }

  return and(...conditions);
};

This ensures filter logic stays synchronized and reduces maintenance burden.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b570061 and f24b1e1.

📒 Files selected for processing (3)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx (1 hunks)
  • apps/dashboard/lib/trpc/routers/identity/query.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx
🧰 Additional context used
🧠 Learnings (14)
📓 Common learnings
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.
📚 Learning: 2024-12-03T14:07:45.173Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2143
File: apps/dashboard/components/ui/group-button.tsx:21-31
Timestamp: 2024-12-03T14:07:45.173Z
Learning: In the `ButtonGroup` component (`apps/dashboard/components/ui/group-button.tsx`), avoid suggesting the use of `role="group"` in ARIA attributes.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2025-05-15T16:26:08.666Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3242
File: apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/override-indicator.tsx:50-65
Timestamp: 2025-05-15T16:26:08.666Z
Learning: In the Unkey dashboard, Next.js router (router.push) should be used for client-side navigation instead of window.location.href to preserve client state and enable smoother transitions between pages.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2024-10-23T16:25:33.113Z
Learnt from: p6l-richard
Repo: unkeyed/unkey PR: 2085
File: apps/www/components/glossary/terms-stepper-mobile.tsx:16-20
Timestamp: 2024-10-23T16:25:33.113Z
Learning: In the `apps/www/components/glossary/terms-stepper-mobile.tsx` file, avoid suggesting to extract the term navigation logic into a custom hook, as the user prefers to keep the component straightforward.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2024-12-03T14:17:08.016Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2143
File: apps/dashboard/app/(app)/logs/logs-page.tsx:77-83
Timestamp: 2024-12-03T14:17:08.016Z
Learning: The `<LogsTable />` component already implements virtualization to handle large datasets efficiently.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2025-08-25T12:56:59.310Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3834
File: apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts:59-66
Timestamp: 2025-08-25T12:56:59.310Z
Learning: In the ratelimit namespace query system (apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts), the nameQuery filter is designed as an array for future extensibility to support multiple filters, but currently only the first filter (index 0) is processed. This is intentional future-proofing.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-02-27T14:35:15.251Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2918
File: apps/dashboard/lib/trpc/routers/api/overview-api-search.ts:0-0
Timestamp: 2025-02-27T14:35:15.251Z
Learning: When using Drizzle ORM with LIKE queries, use the sql`` tagged template syntax (e.g., sql`${table.name} LIKE ${`%${input.query}%`}`) instead of direct string interpolation to prevent SQL injection attacks.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-06-25T19:51:15.995Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3369
File: apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts:67-67
Timestamp: 2025-06-25T19:51:15.995Z
Learning: Drizzle ORM automatically handles SQL parameterization and escaping for queries, including LIKE queries with direct string interpolation (e.g., `like(schema.keys.name, \`%${filter.value}%\`)`), making them safe from SQL injection attacks without requiring explicit escaping or sql`` tagged template syntax.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-02-10T14:12:17.261Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2883
File: apps/dashboard/app/(app)/logs/components/controls/components/logs-search/index.tsx:29-31
Timestamp: 2025-02-10T14:12:17.261Z
Learning: In the logs search component's error handling, error messages are deliberately wrapped in single quotes within template literals for visual distinction (e.g. "Unable to process your search request 'some error message'").

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-12-05T12:05:33.143Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4484
File: go/pkg/db/queries/identity_list.sql:11-24
Timestamp: 2025-12-05T12:05:33.143Z
Learning: In go/pkg/db/queries/identity_list.sql and similar identity ratelimit queries, the RatelimitInfo struct's key_id and identity_id fields can be omitted from JSON_OBJECT construction because: (1) these are identity-level ratelimits where key_id doesn't apply, and (2) identity_id is redundant since it's already known from the parent identity context. Zero values for these fields are acceptable and intentional.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-06-24T13:29:10.129Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3401
File: apps/dashboard/app/(app)/logs/filters.query-params.ts:10-0
Timestamp: 2025-06-24T13:29:10.129Z
Learning: The `buildQueryParams` function in `apps/dashboard/app/(app)/logs/filters.query-params.ts` calls `useFilters()` hook inside it, but this is valid because the function is only called from within other React hooks, maintaining the Rules of Hooks compliance.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-05-15T15:57:02.128Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3242
File: apps/dashboard/app/(app)/apis/[apiId]/api-id-navbar.tsx:47-50
Timestamp: 2025-05-15T15:57:02.128Z
Learning: When reviewing code for Unkey, prefer using `Boolean()` over the double negation (`!!`) operator for boolean coercion, as their linter rules favor this pattern.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-05-15T16:09:49.243Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3242
File: apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/components/logs-search/index.tsx:7-43
Timestamp: 2025-05-15T16:09:49.243Z
Learning: For type safety issues involving `any` type assertions, the team prefers to address these systematically with linter updates rather than fixing them individually in code reviews.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
🧬 Code graph analysis (1)
apps/dashboard/lib/trpc/routers/identity/query.ts (3)
apps/dashboard/lib/trpc/routers/utils/sql.ts (1)
  • escapeLike (15-17)
internal/rbac/src/queries.ts (2)
  • or (46-50)
  • and (52-56)
apps/dashboard/lib/db.ts (1)
  • db (5-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Dashboard / Test Dashboard
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1)

78-80: Verify that aggressive caching strategy aligns with UX requirements.

The query is configured to never become stale and never refetch on mount or window focus. This means users won't see updates (new identities, deletions, metadata changes) unless they explicitly refresh the page or trigger a refetch action.

While this optimizes performance and reduces server load, it may impact user experience if they expect to see recent changes, especially in collaborative environments or after performing actions in other tabs.

Consider if one of the following adjustments better serves your use case:

  1. Allow refetch on window focus for cross-tab sync:

    -  refetchOnWindowFocus: false,
    +  refetchOnWindowFocus: true,
  2. Set a reasonable staleTime instead of infinity:

    -  staleTime: Number.POSITIVE_INFINITY,
    +  staleTime: 5 * 60 * 1000, // 5 minutes
  3. Provide a manual refresh button in the UI for users to fetch latest data.

If the current behavior is intentional for this feature, consider documenting the rationale.

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: 0

🧹 Nitpick comments (2)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (2)

65-82: Add error handling for the query.

The useInfiniteQuery result doesn't destructure or handle the error state. If the query fails, users will see an indefinite loading state or an empty state without explanation.

Add error handling:

  const {
    data: identitiesData,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage,
    isLoading: isLoadingInitial,
+   error,
+   isError,
  } = trpc.identity.query.useInfiniteQuery(

Then add an error state to your component (before the VirtualTable return) to display a user-friendly message when isError is true.


158-160: Consider using cn() for className composition.

The template literal for className works but is inconsistent with the rest of the codebase's use of the cn() utility.

Apply this diff for consistency:

                    <Link
-                     className={`font-mono group-hover:underline decoration-dotted text-accent-9 ${
-                       isNavigating ? "pointer-events-none opacity-50" : ""
-                     }`}
+                     className={cn(
+                       "font-mono group-hover:underline decoration-dotted text-accent-9",
+                       isNavigating && "pointer-events-none opacity-50"
+                     )}
                      href={`/${workspace.slug}/identities/${identity.id}`}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f24b1e1 and ee7d5b7.

📒 Files selected for processing (3)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (2 hunks)
  • apps/dashboard/lib/trpc/routers/identity/search.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/dashboard/lib/trpc/routers/identity/search.ts
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.
📚 Learning: 2024-12-03T14:07:45.173Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2143
File: apps/dashboard/components/ui/group-button.tsx:21-31
Timestamp: 2024-12-03T14:07:45.173Z
Learning: In the `ButtonGroup` component (`apps/dashboard/components/ui/group-button.tsx`), avoid suggesting the use of `role="group"` in ARIA attributes.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2025-05-15T16:26:08.666Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3242
File: apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/override-indicator.tsx:50-65
Timestamp: 2025-05-15T16:26:08.666Z
Learning: In the Unkey dashboard, Next.js router (router.push) should be used for client-side navigation instead of window.location.href to preserve client state and enable smoother transitions between pages.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2024-10-23T16:25:33.113Z
Learnt from: p6l-richard
Repo: unkeyed/unkey PR: 2085
File: apps/www/components/glossary/terms-stepper-mobile.tsx:16-20
Timestamp: 2024-10-23T16:25:33.113Z
Learning: In the `apps/www/components/glossary/terms-stepper-mobile.tsx` file, avoid suggesting to extract the term navigation logic into a custom hook, as the user prefers to keep the component straightforward.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2024-12-03T14:17:08.016Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2143
File: apps/dashboard/app/(app)/logs/logs-page.tsx:77-83
Timestamp: 2024-12-03T14:17:08.016Z
Learning: The `<LogsTable />` component already implements virtualization to handle large datasets efficiently.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3797
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/control-cloud/index.tsx:1-4
Timestamp: 2025-08-18T10:28:47.391Z
Learning: In Next.js App Router, components that use React hooks don't need their own "use client" directive if they are rendered within a client component that already has the directive. The client boundary propagates to child components.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2025-06-10T14:21:42.413Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3311
File: apps/dashboard/components/logs/llm-search/components/search-input.tsx:14-14
Timestamp: 2025-06-10T14:21:42.413Z
Learning: In Next.js applications, importing backend modules into frontend components causes bundling issues and can expose server-side code to the client bundle. For simple constants like limits, it's acceptable to duplicate the value rather than compromise the frontend/backend architectural separation.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2025-09-23T17:40:44.944Z
Learnt from: perkinsjr
Repo: unkeyed/unkey PR: 4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:40:44.944Z
Learning: In the Unkey dashboard authorization section navigation components, the maintainer prefers to wrap entire navbars in Suspense rather than scoping Suspense to individual components, even if it blocks the whole navigation during loading.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
🧬 Code graph analysis (1)
apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1)
  • IdentitiesClient (6-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Dashboard / Test Dashboard
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (5)
apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)

1-20: LGTM! Clean simplification.

The page component has been appropriately simplified by delegating the complex table logic to IdentitiesClient. The beta feature gate is preserved, and the previous issue with the dynamic export has been resolved.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (4)

29-45: Good use of dynamic import for code splitting.

The lazy-loaded IdentityTableActionPopover with a matching skeleton reduces the initial bundle size. The loading fallback UI is consistent with the final rendered state.


157-173: Navigation guard properly implemented.

The link now includes both a visual/interaction lock (pointer-events-none and opacity-50) and an explicit guard in the onClick handler (lines 164-167) that prevents navigation during loading. This addresses the previous review concern.


262-272: Correct use of totalCount for pagination display.

The count info text now properly displays identitiesList.length (items loaded) vs totalCount (total available) instead of showing the same value twice. This addresses the previous review feedback.


78-81: This query is properly invalidated on identity mutations.

The query configuration with staleTime: Number.POSITIVE_INFINITY and manual refetch prevention is safe because all identity mutations that affect this list (currently only identity.create) properly invalidate identity.query through their onSuccess callbacks. Both creation flows call invalidateQueries for this query.

Copy link
Collaborator

@MichaelUnkey MichaelUnkey left a comment

Choose a reason for hiding this comment

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

Long id makes text centered? only thing i noticed

Image

@mcstepp
Copy link
Collaborator Author

mcstepp commented Dec 10, 2025

Long id makes text centered? only thing i noticed
Image

Interesting! I dug into it and its because its a button under the hood. The text is always centered in the button, and the button width is the size of the cell. I can fix it though. Good catch!

@mcstepp
Copy link
Collaborator Author

mcstepp commented Dec 10, 2025

Long id makes text centered? only thing i noticed
Image

Fixed.
Screenshot 2025-12-10 at 12 38 55 PM

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: 0

🧹 Nitpick comments (7)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx (1)

36-46: Consider using a div instead of button for the skeleton.

The ActionColumnSkeleton renders a non-interactive loading placeholder. Using a <button> element is semantically incorrect since it's not an actionable control during loading. This could also cause accessibility issues as screen readers will announce it as a button.

-export const ActionColumnSkeleton = () => (
-  <button
-    type="button"
-    className={cn(
-      "group size-5 p-0 rounded m-0 items-center flex justify-center animate-pulse",
-      "border border-gray-6",
-    )}
-  >
-    <Dots className="text-gray-11" iconSize="sm-regular" />
-  </button>
-);
+export const ActionColumnSkeleton = () => (
+  <div
+    className={cn(
+      "size-5 rounded flex items-center justify-center animate-pulse",
+      "border border-gray-6",
+    )}
+  >
+    <Dots className="text-gray-11" iconSize="sm-regular" />
+  </div>
+);
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (2)

16-18: Redundant refinement check.

The third refine (trimmed !== "") on line 18 is unreachable—if trimmed.length >= 3 passes, the string cannot be empty. This check was likely left over from an earlier iteration.

  externalId: z
    .string()
    .transform((s) => s.trim())
    .refine((trimmed) => trimmed.length >= 3, "External ID must be at least 3 characters")
-   .refine((trimmed) => trimmed.length <= 255, "External ID must be 255 characters or fewer")
-   .refine((trimmed) => trimmed !== "", "External ID cannot be only whitespace"),
+   .refine((trimmed) => trimmed.length <= 255, "External ID must be 255 characters or fewer"),

22-39: Consider separating JSON validity and size validation for clearer error messages.

The current refine combines two distinct validations (valid JSON and size limit) with a single error message. Users won't know which constraint they violated. However, this is acceptable for an initial implementation.

  meta: z
    .string()
    .optional()
+   .refine(
+     (val) => {
+       if (!val || val.trim() === "") return true;
+       try {
+         JSON.parse(val);
+         return true;
+       } catch {
+         return false;
+       }
+     },
+     { message: "Must be valid JSON" },
+   )
    .refine(
      (val) => {
-       if (!val || val.trim() === "") {
-         return true;
-       }
-       try {
-         JSON.parse(val);
-         // Check size limit (1MB)
-         const size = new Blob([val]).size;
-         return size < 1024 * 1024;
-       } catch {
-         return false;
-       }
-     },
-     {
-       message: "Must be valid JSON and less than 1MB",
+       if (!val || val.trim() === "") return true;
+       const size = new Blob([val]).size;
+       return size < 1024 * 1024;
      },
+     { message: "Metadata must be less than 1MB" },
    ),
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1)

24-24: isLoading is hardcoded to false.

The LLMSearch component accepts an isLoading prop to display loading feedback during searches, but it's currently hardcoded. The loading state from the identities query exists in the IdentitiesList component, separate from this search control. Consider restructuring to pass the loading state from the list query to this search component.

apps/dashboard/lib/trpc/routers/identity/query.ts (1)

39-85: Deduplicate filter construction between count and main query

Right now the workspace/deleted/search filters are implemented twice (baseConditions for the count query and buildFilterConditions for the main query). They’re logically equivalent but will be easy to drift and re-run escapeLike unnecessarily.

Consider building the conditions in a single helper and reusing it for both queries, e.g. by:

  • Defining buildFilterConditions above the count query, and
  • Using it for the count as well, something like:
.where(buildFilterConditions(schema.identities, { and, eq, or, like }))

This keeps the search behaviour and workspace scoping in one place.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (2)

58-61: selectedIdentity is never set; selection styling is effectively dead code

selectedIdentity is only reset to null in handleLinkClick and never set to a real Identity, but it’s passed to VirtualTable and used in rowClassName. That means the “selected row” styling can never actually activate.

Either:

  • Wire it up in handleRowClick (or similar) if you want a selected-row affordance:
const handleRowClick = useCallback(
  (identity: Identity) => {
    setSelectedIdentity(identity);
    router.push(`/${workspace.slug}/identities/${identity.id}`);
  },
  [router, workspace.slug],
);

or

  • Drop selectedIdentity and the related props if row selection isn’t part of the UX.

This will simplify the component and avoid confusing unused state.

Also applies to: 250-256


274-297: Consider adding a “Create Identity” CTA directly in the empty state

The empty state currently only surfaces a “Learn about Identities” docs button. Given the PR/issue goals call out a Create Identity CTA in the empty state, you may want to add a secondary action here (e.g. a button that opens the create-identity dialog) so users have an obvious primary action when the table is empty, not just in the header/controls.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee7d5b7 and 30945a4.

📒 Files selected for processing (18)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/last-used.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/skeletons.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx (0 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx (0 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx (0 hunks)
  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/identity/query.ts (5 hunks)
  • apps/dashboard/lib/trpc/routers/identity/search.ts (3 hunks)
  • apps/dashboard/lib/trpc/routers/index.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/utils/sql.ts (1 hunks)
💤 Files with no reviewable changes (3)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/dashboard/lib/trpc/routers/identity/search.ts
  • apps/dashboard/lib/trpc/routers/identity/latestVerification.ts
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identity-table-actions.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/last-used.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/index.tsx
🧰 Additional context used
🧠 Learnings (25)
📓 Common learnings
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.
📚 Learning: 2025-08-25T12:56:59.310Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3834
File: apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts:59-66
Timestamp: 2025-08-25T12:56:59.310Z
Learning: In the ratelimit namespace query system (apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts), the nameQuery filter is designed as an array for future extensibility to support multiple filters, but currently only the first filter (index 0) is processed. This is intentional future-proofing.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-02-27T14:35:15.251Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2918
File: apps/dashboard/lib/trpc/routers/api/overview-api-search.ts:0-0
Timestamp: 2025-02-27T14:35:15.251Z
Learning: When using Drizzle ORM with LIKE queries, use the sql`` tagged template syntax (e.g., sql`${table.name} LIKE ${`%${input.query}%`}`) instead of direct string interpolation to prevent SQL injection attacks.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
  • apps/dashboard/lib/trpc/routers/utils/sql.ts
📚 Learning: 2025-06-25T19:51:15.995Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3369
File: apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts:67-67
Timestamp: 2025-06-25T19:51:15.995Z
Learning: Drizzle ORM automatically handles SQL parameterization and escaping for queries, including LIKE queries with direct string interpolation (e.g., `like(schema.keys.name, \`%${filter.value}%\`)`), making them safe from SQL injection attacks without requiring explicit escaping or sql`` tagged template syntax.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
  • apps/dashboard/lib/trpc/routers/utils/sql.ts
📚 Learning: 2025-02-10T14:12:17.261Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2883
File: apps/dashboard/app/(app)/logs/components/controls/components/logs-search/index.tsx:29-31
Timestamp: 2025-02-10T14:12:17.261Z
Learning: In the logs search component's error handling, error messages are deliberately wrapped in single quotes within template literals for visual distinction (e.g. "Unable to process your search request 'some error message'").

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-12-05T12:05:33.143Z
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4484
File: go/pkg/db/queries/identity_list.sql:11-24
Timestamp: 2025-12-05T12:05:33.143Z
Learning: In go/pkg/db/queries/identity_list.sql and similar identity ratelimit queries, the RatelimitInfo struct's key_id and identity_id fields can be omitted from JSON_OBJECT construction because: (1) these are identity-level ratelimits where key_id doesn't apply, and (2) identity_id is redundant since it's already known from the parent identity context. Zero values for these fields are acceptable and intentional.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-06-24T13:29:10.129Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3401
File: apps/dashboard/app/(app)/logs/filters.query-params.ts:10-0
Timestamp: 2025-06-24T13:29:10.129Z
Learning: The `buildQueryParams` function in `apps/dashboard/app/(app)/logs/filters.query-params.ts` calls `useFilters()` hook inside it, but this is valid because the function is only called from within other React hooks, maintaining the Rules of Hooks compliance.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2024-10-23T16:21:47.395Z
Learnt from: p6l-richard
Repo: unkeyed/unkey PR: 2085
File: apps/www/components/glossary/search.tsx:16-20
Timestamp: 2024-10-23T16:21:47.395Z
Learning: For the `FilterableCommand` component in `apps/www/components/glossary/search.tsx`, refactoring type definitions into an interface is not necessary at this time.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-05-15T15:57:02.128Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3242
File: apps/dashboard/app/(app)/apis/[apiId]/api-id-navbar.tsx:47-50
Timestamp: 2025-05-15T15:57:02.128Z
Learning: When reviewing code for Unkey, prefer using `Boolean()` over the double negation (`!!`) operator for boolean coercion, as their linter rules favor this pattern.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-05-15T16:09:49.243Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3242
File: apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/components/logs-search/index.tsx:7-43
Timestamp: 2025-05-15T16:09:49.243Z
Learning: For type safety issues involving `any` type assertions, the team prefers to address these systematically with linter updates rather than fixing them individually in code reviews.

Applied to files:

  • apps/dashboard/lib/trpc/routers/identity/query.ts
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3797
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/control-cloud/index.tsx:1-4
Timestamp: 2025-08-18T10:28:47.391Z
Learning: In Next.js App Router, components that use React hooks don't need their own "use client" directive if they are rendered within a client component that already has the directive. The client boundary propagates to child components.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2025-07-28T20:38:53.244Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx:322-341
Timestamp: 2025-07-28T20:38:53.244Z
Learning: In apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx, mcstepp prefers to keep hardcoded endpoint logic in the getDiffType function during POC phases for demonstrating diff functionality, rather than implementing a generic diff algorithm. This follows the pattern of keeping simplified implementations for demonstration purposes.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2025-06-10T14:21:42.413Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3311
File: apps/dashboard/components/logs/llm-search/components/search-input.tsx:14-14
Timestamp: 2025-06-10T14:21:42.413Z
Learning: In Next.js applications, importing backend modules into frontend components causes bundling issues and can expose server-side code to the client bundle. For simple constants like limits, it's acceptable to duplicate the value rather than compromise the frontend/backend architectural separation.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
📚 Learning: 2025-05-15T16:26:08.666Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3242
File: apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/override-indicator.tsx:50-65
Timestamp: 2025-05-15T16:26:08.666Z
Learning: In the Unkey dashboard, Next.js router (router.push) should be used for client-side navigation instead of window.location.href to preserve client state and enable smoother transitions between pages.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2025-09-23T17:40:44.944Z
Learnt from: perkinsjr
Repo: unkeyed/unkey PR: 4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:40:44.944Z
Learning: In the Unkey dashboard authorization section navigation components, the maintainer prefers to wrap entire navbars in Suspense rather than scoping Suspense to individual components, even if it blocks the whole navigation during loading.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx
📚 Learning: 2024-12-03T14:07:45.173Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2143
File: apps/dashboard/components/ui/group-button.tsx:21-31
Timestamp: 2024-12-03T14:07:45.173Z
Learning: In the `ButtonGroup` component (`apps/dashboard/components/ui/group-button.tsx`), avoid suggesting the use of `role="group"` in ARIA attributes.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2024-12-03T14:17:08.016Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 2143
File: apps/dashboard/app/(app)/logs/logs-page.tsx:77-83
Timestamp: 2024-12-03T14:17:08.016Z
Learning: The `<LogsTable />` component already implements virtualization to handle large datasets efficiently.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/table/identities-list.tsx
📚 Learning: 2025-09-23T17:39:59.820Z
Learnt from: perkinsjr
Repo: unkeyed/unkey PR: 4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx:88-97
Timestamp: 2025-09-23T17:39:59.820Z
Learning: The useWorkspaceNavigation hook in the Unkey dashboard guarantees that a workspace exists. If no workspace is found, the hook redirects the user to create a new workspace. Users cannot be logged in without a workspace, and new users must create one to continue. Therefore, workspace will never be null when using this hook.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx
📚 Learning: 2025-05-16T16:16:02.286Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3258
File: apps/dashboard/components/dashboard/feedback-component.tsx:28-35
Timestamp: 2025-05-16T16:16:02.286Z
Learning: When validating string inputs in forms using Zod, it's best practice to use `.trim()` before length checks to prevent submissions consisting only of whitespace characters, particularly for feedback forms where meaningful content is expected.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx
📚 Learning: 2025-07-16T17:51:57.297Z
Learnt from: chronark
Repo: unkeyed/unkey PR: 3617
File: go/apps/api/openapi/openapi.yaml:3309-3312
Timestamp: 2025-07-16T17:51:57.297Z
Learning: In the Unkey API OpenAPI schema, the permissions query regex for the verifyKey endpoint intentionally allows all whitespace characters (including tabs and newlines) via `\s`. Do not flag this as an error in future reviews.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx
📚 Learning: 2025-04-22T11:47:24.733Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3156
File: apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/ratelimit-setup.tsx:48-59
Timestamp: 2025-04-22T11:47:24.733Z
Learning: For form inputs in the Unkey dashboard, validation constraints are handled at the schema level using Zod rather than using HTML attributes like min/step on numeric inputs.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx
📚 Learning: 2025-04-22T11:48:39.670Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3156
File: apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/ratelimit-setup.tsx:36-47
Timestamp: 2025-04-22T11:48:39.670Z
Learning: The Unkey dashboard's form validation for numeric values like rate limits is handled through the Zod schema validation (with `.positive()` validators and additional checks in `superRefine`), rather than HTML input attributes like `min`.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx
📚 Learning: 2025-06-19T13:01:55.338Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3315
File: apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/general-setup.tsx:40-50
Timestamp: 2025-06-19T13:01:55.338Z
Learning: In the create-key form's GeneralSetup component, the Controller is intentionally bound to "identityId" as the primary field while "externalId" is set explicitly via setValue. The ExternalIdField component has been designed to handle this pattern where it receives identityId as its value prop but manages both identityId and externalId through its onChange callback.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx
🧬 Code graph analysis (6)
apps/dashboard/lib/trpc/routers/index.ts (1)
apps/dashboard/lib/trpc/routers/identity/latestVerification.ts (1)
  • identityLastVerificationTime (10-54)
apps/dashboard/lib/trpc/routers/identity/query.ts (3)
apps/dashboard/lib/trpc/routers/utils/sql.ts (1)
  • escapeLike (15-17)
internal/rbac/src/queries.ts (2)
  • or (46-50)
  • and (52-56)
apps/dashboard/lib/db.ts (1)
  • db (5-26)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/controls/identities-search.tsx (1)
internal/ui/src/components/llm-search/index.tsx (1)
  • LLMSearch (176-176)
apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1)
  • IdentitiesClient (6-13)
apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (1)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (1)
  • CreateIdentityDialog (44-155)
apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (3)
internal/ui/src/components/toaster.tsx (1)
  • toast (29-29)
internal/ui/src/components/dialog/dialog-container.tsx (1)
  • DialogContainer (66-66)
internal/ui/src/components/buttons/button.tsx (1)
  • Button (439-439)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Dashboard / Test Dashboard
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
apps/dashboard/lib/trpc/routers/index.ts (1)

60-60: LGTM!

The new latestVerification endpoint is correctly wired and follows the established pattern used for api.keys.latestVerification (line 211). The import and router structure are consistent with the codebase conventions.

Also applies to: 346-346

apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (1)

18-20: LGTM!

The CreateIdentityDialog is appropriately placed within Navbar.Actions, following the established navigation patterns. Based on learnings, useWorkspaceNavigation guarantees the workspace exists, so there are no null-safety concerns.

apps/dashboard/lib/trpc/routers/utils/sql.ts (1)

15-17: LGTM!

The escape sequence order is correct—backslashes must be escaped first to prevent double-escaping from subsequent replacements. The documentation is clear with a practical example.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/identities-client.tsx (1)

6-13: LGTM!

Clean composition component that establishes the client boundary for the identities feature. The child components will inherit the client context as per Next.js App Router conventions.

apps/dashboard/app/(app)/[workspaceSlug]/identities/_components/create-identity-dialog.tsx (1)

63-84: LGTM!

The mutation handlers are well-implemented:

  • Success path correctly invalidates queries, shows a toast, closes the dialog, and resets the form.
  • Error handling appropriately distinguishes between CONFLICT (duplicate external ID) and other errors.
apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)

4-18: IdentitiesClient-based page composition looks correct

The page now cleanly delegates all list UI to IdentitiesClient while preserving the identities beta gate and existing Navigation. No behavioural or routing issues stand out here.

@mcstepp mcstepp requested a review from MichaelUnkey December 10, 2025 18:10
Copy link
Collaborator

@MichaelUnkey MichaelUnkey left a comment

Choose a reason for hiding this comment

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

LGTM

@mcstepp mcstepp merged commit 3ef549c into main Dec 10, 2025
26 checks passed
@mcstepp mcstepp deleted the ENG-2268-identities-list-view branch December 10, 2025 18:57
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.

Identity List View with Basic Actions

3 participants