Skip to content

feat(dashboard): Slug routing#4009

Merged
perkinsjr merged 17 commits intomainfrom
slug-routing
Sep 26, 2025
Merged

feat(dashboard): Slug routing#4009
perkinsjr merged 17 commits intomainfrom
slug-routing

Conversation

@perkinsjr
Copy link
Member

@perkinsjr perkinsjr commented Sep 22, 2025

What does this PR do?

Core Architecture Changes

URL Structure Migration: URL pattern across all dashboard routes, enabling workspace-scoped navigation

Component Migration: Relocated and updated 564 files including components, pages,
and utilities to work within the new workspace-scoped file structure:

  • API management pages and components
  • Key creation and management flows
  • Logs viewing and filtering systems
  • Settings and billing pages
  • Identity and permission management
  • Authorization (roles & permissions) system

Workspace Context Integration:

  • Added workspace slug parameter extraction and validation
  • Implemented workspace-aware navigation and breadcrumbs
  • Updated all tRPC routes to include workspace context
  • Added workspace redirect hooks for navigation

Performance Improvements

  • Modified database queries to be workspace-aware
  • Updated API endpoints to handle workspace parameters
  • Enhanced caching strategies for workspace-scoped data

Reliability Enhancements:

  • Added retry logic for workspace loading failures
  • Implemented exponential backoff delays for API retries for workspaces
  • Optimized workspace switching performance

UI/UX & Developer Experience

Interface Improvements:

  • Fixed accessibility issues in navigation components
  • Improved loading states and error handling across all pages
  • Enhanced workspace switching experience
  • Added proper workspace context to all major features

Code Quality:

  • Fixed sidebar navigation issues
  • Corrected various linting and TypeScript errors
  • Addressed button nesting and HTML structure issues

Fixes # (issue)

If there is not an issue for this, please create one first. This is used to tracking purposes and also helps use understand why this PR exists

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?

Test 1 (Regular User):
- Login as a user who has a workspace, select the workspace, navigate around and interact with all pieces of the dashboard making sure nothing breaks.

Test 2 (new User):
- Sign up for an account, make sure you get navigated to /new and that the dashboard functions after the fact.

Test 3 (Dumb Developer):

  • Sign in and accidentally select a local organization that doesn't have a DB attached.
  • See the workspace loader for (15s) -> This is retries with a minor back off for worst case scenarios. This would only happen IF our tRPC returned Not Found on every attempt, which would never happen in the real world.
  • Redirect to new

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

perkinsjr and others added 2 commits September 21, 2025 21:05
This commit introduces a comprehensive refactoring of the dashboard routing system
to support workspace-based URLs, enabling multi-workspace functionality with
improved navigation patterns and enhanced error handling.

## Core Architecture Changes

• **URL Structure Migration**: Migrated from `/apis/[apiId]` to `/[workspace]/apis/[apiId]`
  URL pattern across all dashboard routes, enabling workspace-scoped navigation

• **Component Migration**: Relocated and updated 564 files including components, pages,
  and utilities to work within the new workspace-scoped file structure:
  - API management pages and components
  - Key creation and management flows
  - Logs viewing and filtering systems
  - Settings and billing pages
  - Identity and permission management
  - Authorization (roles & permissions) system

• **Workspace Context Integration**:
  - Added workspace slug parameter extraction and validation
  - Implemented workspace-aware navigation and breadcrumbs
  - Updated all tRPC routes to include workspace context
  - Added workspace redirect hooks for seamless navigation

## Data Layer & Performance Improvements

• **Enhanced Database Integration**:
  - Modified database queries to be workspace-aware
  - Updated API endpoints to handle workspace parameters
  - Enhanced caching strategies for workspace-scoped data
  - Implemented robust workspace loading with retry mechanisms

• **Reliability Enhancements**:
  - Added intelligent retry logic for workspace loading failures
  - Implemented exponential backoff delays for API retries
  - Fixed workspace provider error handling and recovery
  - Optimized workspace switching performance

## UI/UX & Developer Experience

• **Interface Improvements**:
  - Fixed accessibility issues in navigation components
  - Improved loading states and error handling across all pages
  - Enhanced workspace switching experience
  - Added proper workspace context to all major features

• **Code Quality**:
  - Resolved merge conflicts from main branch integration
  - Fixed sidebar navigation issues
  - Corrected various linting and TypeScript errors
  - Addressed button nesting and HTML structure issues
@changeset-bot
Copy link

changeset-bot bot commented Sep 22, 2025

⚠️ No Changeset found

Latest commit: c3545ff

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 Sep 22, 2025

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

Project Deployment Preview Comments Updated (UTC)
dashboard Ready Ready Preview Comment Sep 25, 2025 6:19pm
engineering Ready Ready Preview Comment Sep 25, 2025 6:19pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 22, 2025

📝 Walkthrough

Walkthrough

Adds workspace-aware routing and imports across dashboard pages. Introduces a new ApisNavbar, identities pages/components, and a workspace layout with Suspense. Updates many links to prefix workspace slug, adds Suspense/Loading in select components, refines settings invalidation after API deletion, and switches some selection logic to use IDs.

Changes

Cohort / File(s) Summary
Workspace-scoped imports: APIs/Keys
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/external-id-field/index.tsx, .../key-secret-section.tsx, .../_overview/components/table/components/log-details/index.tsx, .../components/override-indicator.tsx, .../keys/[keyAuthId]/[keyId]/components/charts/bar-chart/utils.ts, .../components/controls/components/logs-filters/outcome-filter.tsx, .../components/controls/index.tsx, .../components/table/components/log-details/index.tsx, .../keys/[keyAuthId]/_components/components/controls/index.tsx, .../actions/components/edit-credits/index.tsx, .../actions/components/edit-credits/utils.ts, .../actions/components/edit-expiration/index.tsx, .../actions/components/edit-expiration/utils.ts, .../actions/components/edit-external-id/index.tsx, .../actions/components/edit-key-name.tsx, .../actions/components/edit-metadata/index.tsx, .../actions/components/edit-ratelimits/index.tsx, .../actions/components/edit-ratelimits/utils.ts, .../actions/components/edit-rbac/components/assign-permission/permissions-field.tsx, .../actions/components/edit-rbac/components/assign-role/create-key-options.tsx, .../actions/components/hooks/use-edit-key.tsx, .../actions/keys-table-action.popover.constants.tsx, .../selection-controls/components/batch-edit-external-id.tsx
Updated imports to workspaceSlug-scoped modules. No behavioral changes.
Workspace-aware navigation and links
.../create-key/components/key-created-success-dialog.tsx, .../_overview/components/table/components/log-details/index.tsx, .../components/override-indicator.tsx, .../keys/[keyAuthId]/[keyId]/page.tsx, .../keys/[keyAuthId]/keys-list.tsx, .../keys/[keyAuthId]/page.tsx, .../[apiId]/page.tsx, .../settings/page.tsx, .../settings/components/default-bytes.tsx, .../settings/components/default-prefix.tsx, .../settings/components/delete-api.tsx, apps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsx, apps/dashboard/app/(app)/[workspaceSlug]/audit/page.tsx, apps/dashboard/app/(app)/[workspaceSlug]/logs/navigation.tsx, apps/dashboard/app/(app)/[workspaceSlug]/logs/page.tsx, apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/page.tsx
Introduced useWorkspaceNavigation and prefixed routes/hrefs with workspace slug. Adjusted revalidate and router.push targets accordingly.
Suspense/Loading integration
.../create-key/index.tsx, .../_overview/components/table/components/log-details/index.tsx, .../components/override-indicator.tsx, apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/api-list-card.tsx, apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx
Wrapped components/sections in React.Suspense with Loading fallbacks. No prop changes to wrapped children.
New components/pages: APIs and Identities
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/api-id-navbar.tsx, apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/api-list-card.tsx, apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx, .../identities/navigation.tsx, .../identities/components/results.tsx, .../identities/row.tsx, .../identities/[identityId]/navigation.tsx, apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx
Added ApisNavbar, ApiListCard, identities listing/detail navigation, results and row components, and a workspace layout. Implement data fetching, breadcrumbing, and Suspense wrappers.
Settings invalidation and renames
.../settings/components/key-settings-form-helper.ts, .../settings/components/settings-client.tsx
Added invalidateAfterApiDeletion and used it in mutation handlers; renamed local workspace binding to workspaceData.
Styling tweaks
.../settings/components/delete-protection.tsx, .../settings/components/skeleton.tsx, .../settings/components/update-api-name.tsx
Replaced border-b-1 with border-b. No logic changes.
Selection logic uses IDs
apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/components/table/permissions-list.tsx, .../authorization/roles/components/table/roles-list.tsx
Toggle selection now keyed by permissionId/roleId instead of names. Rendering updated accordingly.
Delete hooks: result-based counts
.../authorization/permissions/components/table/components/actions/components/hooks/use-delete-permission.ts, .../authorization/roles/components/table/components/actions/components/hooks/use-delete-role.ts
onSuccess now derives deleted count from mutation result (data.deletedCount) instead of variables length.
Authorization: workspace imports and nav
apps/dashboard/app/(app)/[workspaceSlug]/authorization/constants.ts, .../permissions/navigation.tsx, .../roles/navigation.tsx, .../roles/components/.../use-fetch-connected-keys-and-perms.ts, .../roles/components/.../keys-table-action.popover.constants.tsx, .../permissions/components/table/components/selection-controls/index.tsx, .../roles/components/table/components/selection-controls/index.tsx, .../roles/components/upsert-role/components/assign-key/create-key-options.tsx, .../roles/components/upsert-role/components/assign-permission/create-permission-options.tsx
Added navigation(workspaceSlug) export; switched to workspace-aware breadcrumbs; updated imports and introduced a LoadingTrigger wrapper for dynamic popover trigger.
Logs/Audit: workspace-scoped imports
apps/dashboard/app/(app)/[workspaceSlug]/audit/components/.../bucket-filter.tsx, .../events-filter.tsx, .../root-keys-filter.tsx, .../users-filter.tsx, .../logs-filters/index.tsx, .../logs-queries/utils.ts, apps/dashboard/app/(app)/[workspaceSlug]/logs/components/.../display-popover.tsx, .../logs-filters/components/methods-filter.tsx, .../paths-filter.tsx, .../status-filter.tsx, .../logs-filters/index.tsx, .../logs-queries/utils.ts, .../logs-search/index.tsx, apps/dashboard/app/(app)/[workspaceSlug]/logs/navigation.tsx
Redirected hooks/types/util imports to workspaceSlug paths. Minor typings/formatting adjustments.
APIs list: prop changes and usage
apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/api-list-client.tsx, apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/create-api-button.tsx, apps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsx
Added workspaceSlug prop to ApiListClient and CreateApiButton; updated page to pass slug and to use breadcrumb pointing to workspace-scoped path.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant V as View: ApisNavbar
  participant T as TRPC: queryApiKeyDetails
  Note over V: Mount with {apiId, keyspaceId, keyId, activePage}
  V->>T: fetch layoutData (enabled gating, retry)
  alt loading or error
    V-->>U: Render LoadingNavbar
  else success
    V-->>U: Render NavbarContent with items (Requests, Keys?, Settings)
    U->>V: Click Create new key
    alt keyAuth exists
      V-->>U: Open CreateKeyDialog
    else
      V-->>U: Disabled action
    end
  end
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant S as Settings: DeleteApi
  participant M as Mutation: deleteApi
  participant H as Helper: invalidateAfterApiDeletion
  participant R as Router
  U->>S: Confirm delete
  S->>M: mutate({ apiId })
  M-->>S: onSuccess(data)
  S->>H: invalidateAfterApiDeletion()
  S->>R: push `/{workspace.slug}/apis`
Loading
sequenceDiagram
  autonumber
  participant P as Page: /[workspaceSlug]/identities
  participant Z as Zod: parse searchParams
  participant W as useWorkspaceNavigation
  participant R as Results (Suspense)
  participant Q as TRPC: identities.search
  P->>Z: validate {search?, limit?}
  P->>W: get workspace (feature flags)
  alt identities disabled
    P-->>P: Render OptIn
  else enabled
    P-->>P: Render Navigation, SearchField
    P->>R: Suspense mount
    R->>Q: fetch({search, limit})
    alt success
      R-->>P: Render table rows
    else error/empty
      R-->>P: Show fallback/empty state
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

:joystick: 300 points, UI

Suggested reviewers

  • perkinsjr
  • mcstepp
  • ogzhanolguncu
  • chronark
  • Flo4604

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The description omits content under the required “## What does this PR do?” heading and leaves the “Fixes # (issue)” placeholder unresolved, failing to provide the summary and issue reference mandated by the template. Please fill in the “## What does this PR do?” section with a concise summary of changes, replace the placeholder in “Fixes # (issue)” with the actual issue number, and ensure all required template sections are completed.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title “feat(dashboard): Slug routing” is concise and accurately highlights the primary change of adding workspace‐scoped slug routing in the dashboard, following conventional commit style and providing clear context for reviewers.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch slug-routing

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3a5dbb and c3545ff.

📒 Files selected for processing (3)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-api.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/paths-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/page.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: perkinsjr
PR: unkeyed/unkey#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.
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.279Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.
Learnt from: perkinsjr
PR: unkeyed/unkey#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.
📚 Learning: 2025-09-23T17:39:59.820Z
Learnt from: perkinsjr
PR: unkeyed/unkey#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]/logs/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-api.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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]/logs/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-api.tsx
📚 Learning: 2025-09-22T18:44:56.279Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.279Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-api.tsx
📚 Learning: 2025-06-24T13:29:10.129Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/paths-filter.tsx
📚 Learning: 2025-08-25T13:46:34.441Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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]/logs/components/controls/components/logs-filters/components/paths-filter.tsx
📚 Learning: 2025-08-25T13:46:08.303Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx:1-1
Timestamp: 2025-08-25T13:46:08.303Z
Learning: The NamespaceListDateTime component in apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx is intentionally designed to use the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than the namespace list hook, as clarified by ogzhanolguncu. This coupling is by design, not an architectural issue.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/paths-filter.tsx
🧬 Code graph analysis (2)
apps/dashboard/app/(app)/[workspaceSlug]/logs/page.tsx (1)
apps/dashboard/hooks/use-workspace-navigation.tsx (1)
  • useWorkspaceNavigation (16-35)
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-api.tsx (1)
apps/dashboard/hooks/use-workspace-navigation.tsx (1)
  • useWorkspaceNavigation (16-35)
⏰ 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). (4)
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Build / Build
  • GitHub Check: Test Packages / Test
🔇 Additional comments (3)
apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/paths-filter.tsx (1)

1-1: Import swap aligns with workspace-scoped hooks

Thanks for pointing the filter at the [workspaceSlug]-aware hook; this keeps the logs controls consistent with the new routing layout.

apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-api.tsx (1)

2-2: Redirect now honors the workspace slug

Pulling the slug from useWorkspaceNavigation ensures the post-delete redirect lands back on the correct workspace-scoped APIs index. Nicely resolves the earlier UUID redirect bug.

Also applies to: 25-25, 58-58

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

2-12: Navigation uses workspace context correctly

Wiring the logs page navigation through useWorkspaceNavigation keeps the breadcrumb in sync with the active workspace slug. Looks good.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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.

@perkinsjr
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 22, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

vercel[bot]

This comment was marked as duplicate.

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

Caution

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

⚠️ Outside diff range comments (20)
apps/dashboard/app/(app)/[workspace]/authorization/permissions/components/table/components/selection-controls/index.tsx (1)

1-6: Add 'use client' directive; hooks and framer-motion require a Client Component.

This module uses useState/useRef and framer-motion; without 'use client' it will fail in App Router.

Apply this diff at the top:

+'use client';
+
 import { AnimatedCounter } from "@/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls";
 import { ConfirmPopover } from "@/components/confirmation-popover";
 import { Trash, XMark } from "@unkey/icons";
 import { Button } from "@unkey/ui";
 import { AnimatePresence, motion } from "framer-motion";
 import { useRef, useState } from "react";
apps/dashboard/app/(app)/[workspace]/logs/components/table/log-details/components/log-footer.tsx (1)

13-15: Outcome: default should be "N/A" and UI currently renders the wrong value.

  • Per prior learning, default to "N/A" (not "VALID").
  • Bug: you compute contentCopy but render {content}, so null outcomes render empty despite styling based on the fallback.

Apply:

-const DEFAULT_OUTCOME = "VALID";
+const DEFAULT_OUTCOME = "N/A";

-          description: (content) => {
-            let contentCopy = content;
-            if (contentCopy == null) {
-              contentCopy = DEFAULT_OUTCOME;
-            }
-            return (
-              <Badge
-                className={cn(
-                  {
-                    "text-amber-11 bg-amber-3 hover:bg-amber-3 font-medium":
-                      YELLOW_STATES.includes(contentCopy),
-                    "text-red-11 bg-red-3 hover:bg-red-3 font-medium":
-                      RED_STATES.includes(contentCopy),
-                  },
-                  "uppercase",
-                )}
-              >
-                {content}
-              </Badge>
-            );
-          },
-          content: extractResponseField(log, "code"),
+          description: (content) => {
+            const display = content ?? DEFAULT_OUTCOME;
+            return (
+              <Badge
+                className={cn(
+                  {
+                    "text-amber-11 bg-amber-3 hover:bg-amber-3 font-medium":
+                      YELLOW_STATES.includes(display),
+                    "text-red-11 bg-red-3 hover:bg-red-3 font-medium":
+                      RED_STATES.includes(display),
+                  },
+                  "uppercase",
+                )}
+              >
+                {display}
+              </Badge>
+            );
+          },
+          content: extractResponseField(log, "code") ?? DEFAULT_OUTCOME,

Note: Change informed by retrieved learning for this component’s Outcome default.

Also applies to: 57-76, 79-82

apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/methods-filter.tsx (1)

1-36: Add "use client" — this file uses hooks.

This component calls useFilters(), so it must be a client component in Next.js App Router. Add the directive at the top to avoid RSC/hook runtime errors.

+'use client';
+
 import { useFilters } from "@/app/(app)/[workspace]/logs/hooks/use-filters";
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/index.tsx (1)

1-62: Add "use client" — this file also uses hooks.

useFilters() is used directly; mark this component as client to prevent RSC violations.

+'use client';
+
 import { useFilters } from "@/app/(app)/[workspace]/logs/hooks/use-filters";
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/paths-filter.tsx (1)

1-37: Add "use client" — hooks and crypto.randomUUID() require client context.

This module uses useFilters() and browser crypto; mark as client to avoid server-runtime errors.

+'use client';
+
 import { logsFilterFieldConfig } from "@/app/(app)/[workspace]/logs/filters.schema";
 import { useFilters } from "@/app/(app)/[workspace]/logs/hooks/use-filters";
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsx (1)

35-57: Spurious error toasts on initial load and stale suppression across selections.

  • You toast when !log, which is true during initial fetch → user sees “Log Data Unavailable” before loading finishes.
  • errorShown resets only when selectedLog becomes null, so switching to a new log may suppress valid errors.

Gate on requestId, ignore the loading state (undefined), and reset per-requestId.

Apply:

   const [errorShown, setErrorShown] = useState(false);
 
-  useEffect(() => {
-    if (!errorShown && selectedLog) {
-      if (error) {
-        toast.error("Error Loading Log Details", {
-          description: `${
-            error.message ||
-            "An unexpected error occurred while fetching log data. Please try again."
-          }`,
-        });
-        setErrorShown(true);
-      } else if (!log) {
-        toast.error("Log Data Unavailable", {
-          description:
-            "Could not retrieve log information for this key. The log may have been deleted or is still processing.",
-        });
-        setErrorShown(true);
-      }
-    }
-
-    if (!selectedLog) {
-      setErrorShown(false);
-    }
-  }, [error, log, selectedLog, errorShown]);
+  const requestId = selectedLog?.request_id;
+
+  // Reset toast state whenever a new log is selected or the drawer closes
+  useEffect(() => {
+    setErrorShown(false);
+  }, [requestId]);
+
+  useEffect(() => {
+    if (!requestId || errorShown) return;
+    if (error) {
+      toast.error("Error Loading Log Details", {
+        description:
+          error.message ||
+          "An unexpected error occurred while fetching log data. Please try again.",
+      });
+      setErrorShown(true);
+      return;
+    }
+    // Only treat explicit null as "unavailable"; undefined is still loading
+    if (log === null) {
+      toast.error("Log Data Unavailable", {
+        description:
+          "Could not retrieve log information for this key. The log may have been deleted or is still processing.",
+      });
+      setErrorShown(true);
+    }
+  }, [requestId, error, log, errorShown]);
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-secret-section.tsx (1)

32-40: Copy button leaks the real secret when the snippet is hidden.
CopyButton always uses the unmasked snippet, so users can copy secrets even while the UI shows a masked value.

Apply to copy exactly what’s visible:

   const snippet = `curl -XPOST '${
     process.env.NEXT_PUBLIC_UNKEY_API_URL ?? "https://api.unkey.com"
   }/v2/keys.verifyKey' \\
   -H 'Authorization: Bearer <UNKEY_ROOT_KEY>' \\
   -H 'Content-Type: application/json' \\
   -d '{
     "key": "${keyValue}"
   }'`;
+
+  const displaySnippet = showKeyInSnippet
+    ? snippet
+    : snippet.replace(keyValue, maskedKey);

@@
         <Code
           className={codeClassName}
           visibleButton={
             <VisibleButton isVisible={showKeyInSnippet} setIsVisible={setShowKeyInSnippet} />
           }
-          copyButton={<CopyButton value={snippet} />}
+          copyButton={<CopyButton value={displaySnippet} />}
         >
-          {showKeyInSnippet ? snippet : snippet.replace(keyValue, maskedKey)}
+          {displaySnippet}
         </Code>

Also applies to: 65-71

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/delete-api.tsx (1)

61-63: Prevent double‑submit: await the mutation.

react-hook-form’s isSubmitting flips back to false immediately because onSubmit doesn’t await the mutation, enabling rapid double clicks. Use mutateAsync and await it.

-  async function onSubmit(_values: z.infer<typeof formSchema>) {
-    deleteApi.mutate({ apiId: api.id });
-  }
+  async function onSubmit(_values: z.infer<typeof formSchema>) {
+    await deleteApi.mutateAsync({ apiId: api.id });
+  }
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/keys-table-action.popover.constants.tsx (1)

195-199: Add "use client" — this module uses React hooks and browser APIs.

This file calls trpc.useUtils() and references navigator.clipboard via handlers, so it must be a client component. Without the directive, Next.js will treat it as a server component and error at build/runtime.

+ "use client";
+
 import { MAX_KEYS_FETCH_LIMIT } from "@/app/(app)/[workspace]/authorization/roles/components/upsert-role/components/assign-key/hooks/use-fetch-keys";
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/components/assign-role/create-key-options.tsx (1)

41-45: Tooltip trigger is a non‑focusable div — keyboard users can’t access tooltip

Wrap the trigger in a focusable element or add tabIndex=0 (and ideally aria-describedby linked to content).

-            <div className="flex w-full text-accent-8 text-xs gap-4 py-0.5 items-center group flex-row">
+            <div tabIndex={0} className="flex w-full text-accent-8 text-xs gap-4 py-0.5 items-center group flex-row">

Optional: add role="button" if semantically appropriate.

apps/dashboard/app/(app)/[workspace]/authorization/roles/components/upsert-role/components/assign-key/create-key-options.tsx (1)

34-38: Tooltip trigger needs to be focusable for a11y

Use a focusable element or add tabIndex=0 on the div so keyboard users can access the tooltip.

-            <div className="flex w-full text-accent-8 text-xs gap-4 py-0.5 items-center group flex-row">
+            <div tabIndex={0} className="flex w-full text-accent-8 text-xs gap-4 py-0.5 items-center group flex-row">

Also applies to: 40-55

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx (1)

83-95: Add slug check before navigating to details.

Right now, navigation can render "/undefined/apis/..." if the workspace is not ready.

       case "go-to-details":
-          if (!keyspaceId) {
+          if (!keyspaceId || !workspace?.slug) {
             toast.error("Failed to Navigate", {
-              description: "Keyspace ID is required to view key details.",
+              description: !workspace?.slug
+                ? "Workspace is not ready yet. Please try again."
+                : "Keyspace ID is required to view key details.",
               action: {
                 label: "Contact Support",
                 onClick: () => window.open("mailto:support@unkey.dev", "_blank"),
               },
             });
             return;
           }
-          router.push(`/${workspace?.slug}/apis/${apiId}/keys/${keyspaceId}/${keyData.id}`);
+          router.push(`/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${keyData.id}`);
           break;
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (1)

48-67: Fix workspace slug nullability, preserve new‑tab behavior, and precompute navigation path

workspace?.slug is used directly in href/router.push and in the useCallback deps — when workspace is not loaded the link becomes "/undefined/…"; preventing default on every click blocks Cmd/Ctrl/middle‑click. Fix the override-indicator component to guard the slug, precompute detailsPath, allow modified clicks, and use detailsPath/router in the deps.

Location: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx — router.push template (≈lines 62–64), Link href (≈line 89), deps (≈lines 66–67).

Apply:

 export const KeyIdentifierColumn = ({ log, apiId, onNavigate }: KeyIdentifierColumnProps) => {
   const { workspace } = useWorkspace();
   const router = useRouter();
   const errorPercentage = getErrorPercentage(log);
   const severity = getErrorSeverity(log);
   const hasErrors = severity !== "none";
   const [isNavigating, setIsNavigating] = useState(false);

+  // Guard and precompute path
+  const slug = workspace?.slug;
+  if (!slug) return null;
+  const detailsPath =
+    log.key_details?.key_auth_id
+      ? `/${slug}/apis/${apiId}/keys/${log.key_details.key_auth_id}/${log.key_id}`
+      : undefined;
 
   const handleLinkClick = useCallback(
     (e: React.MouseEvent) => {
-      e.preventDefault();
-      setIsNavigating(true);
-      onNavigate?.();
-
-      router.push(
-        `/${workspace?.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`,
-      );
+      // Let modified clicks open new tabs/windows
+      const isModified = e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0;
+      if (isModified || !detailsPath) return;
+      e.preventDefault();
+      setIsNavigating(true);
+      onNavigate?.();
+      router.push(detailsPath);
     },
-    [apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router.push, workspace?.slug],
+    [detailsPath, onNavigate, router],
   );
 ...
       <Link
         title={`View details for ${log.key_id}`}
         className="font-mono group-hover:underline decoration-dotted"
-        href={`/${workspace?.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
+        href={detailsPath ?? "#"}
         onClick={handleLinkClick}
       >

Multiple other files use workspace?.slug (see grep output); apply the same guard/precompute pattern where those hrefs or router.push calls can run before workspace is available.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (3)

176-183: Guard key-details link against undefined workspace slug.

Using workspace?.slug directly can render "/undefined/..." during loading. Disable link until slug exists.

-                  <Link
+                  <Link
                     title={`View details for ${key.id}`}
                     className="font-mono group-hover:underline decoration-dotted"
-                    href={`/${workspace?.slug}/apis/${apiId}/keys/${keyspaceId}/${key.id}`}
-                    aria-disabled={isNavigating}
+                    href={workspace ? `/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${key.id}` : "#"}
+                    aria-disabled={!workspace || isNavigating}
                     onClick={() => {
                       handleLinkClick(key.id);
                     }}
                   >

151-159: Workspace-scoped identities link is missing slug.

Link currently points to /identities/{id}; should include workspace slug and be disabled until available.

-                          <Link
+                          <Link
                             title={"View details for identity"}
                             className="font-mono group-hover:underline decoration-dotted"
-                            href={`/identities/${key.identity_id}`}
+                            href={
+                              workspace
+                                ? `/${workspace.slug}/identities/${key.identity_id}`
+                                : "#"
+                            }
                             target="_blank"
                             rel="noopener noreferrer"
-                            aria-disabled={isNavigating}
+                            aria-disabled={!workspace || isNavigating}
                           >

1-399: Fix '/undefined' links from optional workspace.slug interpolation

Multiple files construct hrefs like /${workspace?.slug}/... — when workspace?.slug is undefined this renders /undefined/.... Guard the slug or avoid interpolating the optional chain directly in template literals; conditionally render Link or set href only when workspace?.slug is present.

Key locations (repo-wide — fix all similar occurrences):

  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx — href at ~line 180
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx — activePage.href ~line 19
  • apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx — linkPath ~line 25
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx — base/nav hrefs ~lines 120–137
  • apps/dashboard/components/navigation/sidebar/workspace-navigations.tsx — basePath ~line 43
  • apps/dashboard/components/navigation/sidebar/usage-banner.tsx — billing href ~line 45
  • apps/dashboard/app/(app)/[workspace]/identities/navigation.tsx — href ~line 13
  • apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx — href ~line 14
  • apps/dashboard/app/(app)/[workspace]/settings/team/client.tsx — href ~line 93
  • apps/dashboard/app/new/components/onboarding-success-step.tsx — router.push at ~line 61

Suggested quick fix pattern:

  • href={workspace?.slug ? /${workspace.slug}/apis/${apiId} : undefined}
  • or render only when workspace?.slug exists.
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-external-id/index.tsx (2)

49-58: Bug: onOpenChange guard checks prop instead of event arg.

Use the open parameter, not isOpen, or the dialog can close incorrectly when the confirm popover is active.

Apply:

-  const handleDialogOpenChange = (open: boolean) => {
-    if (isConfirmPopoverOpen && !isOpen) {
+  const handleDialogOpenChange = (open: boolean) => {
+    if (isConfirmPopoverOpen && !open) {
       // If confirm popover is active don't let this trigger outer popover
       return;
     }

60-69: Clear action should also null externalId (consistency + server intent).

Batch clear sets both id and externalId to null; single clear sends only id: null. Align behavior to avoid stale external IDs.

Apply:

   const clearSelection = async () => {
     setSelectedIdentityId(null);
     await updateKeyOwner.mutateAsync({
       keyIds: keyDetails.id,
       ownerType: "v2",
       identity: {
-        id: null,
+        id: null,
+        externalId: null,
       },
     });
   };

Note: Past learning indicates externalId and ownerId are handled separately server-side and ownerId will be removed; keeping externalId explicit avoids ambiguity.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls/components/batch-edit-external-id.tsx (1)

45-56: Same onOpenChange bug as single-edit.

Check open arg instead of isOpen.

Apply:

-  const handleDialogOpenChange = (open: boolean) => {
-    if (isConfirmPopoverOpen && !isOpen) {
+  const handleDialogOpenChange = (open: boolean) => {
+    if (isConfirmPopoverOpen && !open) {
       // If confirm popover is active don't let this trigger outer popover
       return;
     }
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-expiration/utils.ts (1)

5-13: Treat expiration timestamps as milliseconds (fix places that treat them as seconds).

  • Why: schemas and API use UNIX timestamps in milliseconds (see create-key.schema.ts and query-api-keys/schema.ts); the form expects Date objects (expiration.data?: Date). getKeyExpirationDefaults (new Date(keyDetails.expires)) is correct for ms.
  • Action: fix code that treats expires as seconds — e.g. apps/.../status-cell/use-key-status.ts currently does (keyData.expires * 1000 - Date.now()) / (...) — remove the 1000 or normalize expires to milliseconds first (use (keyData.expires - Date.now()) / (100060*60) or convert explicitly).
  • Check and correct any other usages (normalize or document unit):
    • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema.ts (expires: number — ms)
    • apps/dashboard/lib/trpc/routers/api/keys/query-api-keys/schema.ts (expires: z.number().nullable())
    • apps/dashboard/lib/trpc/routers/api/keys/query-api-keys/get-all-keys.ts (serializes expires with getTime())
    • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/.../edit-expiration/utils.ts (file under review) — OK, but ensure keyDetails.expires is ms before new Date(...)
  • Run a repo-wide search for "expires * 1000" / "expires *1000" and for arithmetic using expires to find remaining mismatches.

@perkinsjr
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

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/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (1)

193-202: Guard against undefined workspace before building the key details link.

workspace.slug is dereferenced unconditionally. If the hook returns null/undefined during loading, this will throw at render time.

Apply this diff to make the link safe while preserving UX:

-                  <Link
-                    title={`View details for ${key.id}`}
-                    className="font-mono group-hover:underline decoration-dotted"
-                    href={`/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${key.id}`}
-                    aria-disabled={isNavigating}
-                    onClick={() => {
-                      handleLinkClick(key.id);
-                    }}
-                  >
+                  <Link
+                    title={`View details for ${key.id}`}
+                    className="font-mono group-hover:underline decoration-dotted"
+                    href={
+                      workspace
+                        ? `/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${key.id}`
+                        : "#"
+                    }
+                    aria-disabled={isNavigating || !workspace}
+                    onClick={(e) => {
+                      if (!workspace) {
+                        e.preventDefault();
+                        e.stopPropagation();
+                        return;
+                      }
+                      handleLinkClick(key.id);
+                    }}
+                  >
🧹 Nitpick comments (23)
apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx (3)

9-16: Handle loading/null workspace to avoid a runtime crash

If useWorkspaceNavigation() returns null/undefined while loading, workspace.slug will throw. Render a loading state until the workspace is ready.

 export function Navigation() {
   const workspace = useWorkspaceNavigation();
 
+  if (!workspace) {
+    return <Loading />;
+  }
+
   return (
     <Navbar>
       <Navbar.Breadcrumbs icon={<Layers3 />}>
-        <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/logs`}>
+        <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/logs`}>
           Logs
         </Navbar.Breadcrumbs.Link>
       </Navbar.Breadcrumbs>
     </Navbar>
   );
 }

15-17: URL-encode the slug and mark the current crumb (a11y)

Encode the slug to be safe against unexpected characters and optionally mark the active page.

-        <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/logs`}>
+        <Navbar.Breadcrumbs.Link href={`/${encodeURIComponent(workspace.slug)}/logs`} aria-current="page">

7-7: Avoid server‑only redirect() in client components

next/navigation’s redirect() is server-only. If you intended to redirect from this client component, use useRouter().push(...) instead; otherwise remove the import.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx (3)

28-28: Handle workspace loading/null before usage.

Depending on the hook’s lifecycle, workspace could be transiently undefined. Add a guard or loading fallback to avoid workspace.slug access before ready.

Example:

const workspace = useWorkspaceNavigation();
if (!workspace) {
  return <Loading />;
}

41-43: beforeunload handler won’t reliably prompt without returnValue.

Set e.returnValue = "" for cross‑browser confirmation on tab/window close.

Apply this diff:

-    const handleBeforeUnload = (e: BeforeUnloadEvent) => {
-      e.preventDefault();
-    };
+    const handleBeforeUnload = (e: BeforeUnloadEvent) => {
+      e.preventDefault();
+      // Required in some browsers for the prompt to show
+      e.returnValue = "";
+    };

97-99: Guard workspace presence and encode path segments.

  • Add a defensive check for workspace.slug.
  • Use encodeURIComponent for all dynamic path parts.

Apply this diff:

-          router.push(
-            `/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${keyData.id}`
-          );
+          if (!workspace?.slug) {
+            toast.error("Failed to Navigate", {
+              description: "Workspace context is not ready. Please try again.",
+            });
+            return;
+          }
+          router.push(
+            `/${encodeURIComponent(workspace.slug)}/apis/${encodeURIComponent(apiId)}/keys/${encodeURIComponent(keyspaceId)}/${encodeURIComponent(keyData.id)}`
+          );

Longer-term, consider centralized route builders (e.g., a helper from useWorkspaceNavigation or a paths util) to avoid manual string templates.

apps/dashboard/app/(app)/[workspace]/apis/page.tsx (1)

6-7: Remove unused import
redirect is unused.

-import { useSearchParams, redirect } from "next/navigation";
+import { useSearchParams } from "next/navigation";
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (3)

183-187: Remove redundant cloneElement/class merge.

iconContainer already has cursor-pointer; cloning to re-append it is unnecessary noise.

Apply this diff:

-                    {React.cloneElement(iconContainer, {
-                      className: cn(
-                        iconContainer.props.className,
-                        "cursor-pointer"
-                      ),
-                    })}
+                    {iconContainer}

291-293: Narrow the dependency to a stable primitive.

Depending on the whole workspace object can cause unnecessary recomputes. Use workspace?.slug.

Apply this diff:

-      hoveredKeyId,
-      workspace,
+      hoveredKeyId,
+      workspace?.slug,

355-361: Wrap totalCount in an element; flex gap doesn’t apply to text nodes.

Currently {totalCount} is a bare text node, so spacing may be off compared to adjacent spans.

Apply this diff:

-            <div className="flex gap-2">
-              <span>Showing</span>{" "}
-              <span className="text-accent-12">{keys.length}</span>
-              <span>of</span>
-              {totalCount}
-              <span>keys</span>
-            </div>
+            <div className="flex gap-2">
+              <span>Showing</span>
+              <span className="text-accent-12">{keys.length}</span>
+              <span>of</span>
+              <span className="text-accent-12">{totalCount}</span>
+              <span>keys</span>
+            </div>
apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (3)

183-185: URL‑encode the workspace slug in href

Avoids malformed URLs if slugs contain reserved characters.

-                      <Link
-                        href={`/${identity.workspace.slug}/apis/${key.keyAuth.api.id}/keys/${key.keyAuth.id}/${key.id}`}
-                      >
+                      <Link
+                        href={`/${encodeURIComponent(identity.workspace.slug)}/apis/${key.keyAuth.api.id}/keys/${key.keyAuth.id}/${key.id}`}
+                      >

202-206: Don’t type async server components as React.FC

React.FC implies a sync render; server components can be async. Remove React.FC.

-const LastUsed: React.FC<{
-  workspaceId: string;
-  keySpaceId: string;
-  keyId: string;
-}> = async (props) => {
+async function LastUsed(props: {
+  workspaceId: string;
+  keySpaceId: string;
+  keyId: string;
+}) {

209-213: Fetch only what you need: limit 1

You only read the first row; reduce ClickHouse load.

-      limit: 50,
+      limit: 1,
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx (2)

32-33: Initialize router for client-side navigation.

Apply:

-  const workspace = useWorkspaceNavigation();
+  const workspace = useWorkspaceNavigation();
+  const router = useRouter();

98-100: Handle empty input gracefully and improve numeric UX.

Avoid coercing empty input to 0; set undefined to let zod validation work, and hint numeric keypad on mobile.

Apply:

-              type="text"
-              onChange={(e) =>
-                field.onChange(Number(e.target.value.replace(/\D/g, "")))
-              }
+              type="text"
+              inputMode="numeric"
+              onChange={(e) => {
+                const v = e.target.value.replace(/\D/g, "");
+                field.onChange(v ? Number(v) : undefined);
+              }}
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx (3)

9-12: Remove unused imports (Loading, redirect).

They’re not used and will fail linting.

Apply:

-import { Loading } from "@unkey/ui";
-import { redirect } from "next/navigation";

209-216: Guard against a transient null workspace before building hrefs.

If the workspace hook yields a brief null, using workspace.slug will throw. Early-return a minimal skeleton.

Apply:

   export const ApisNavbar = ({
@@
   }: ApisNavbarProps) => {
-  const workspace = useWorkspaceNavigation();
+  const workspace = useWorkspaceNavigation();
+  if (!workspace) {
+    return <Navbar />;
+  }

127-149: Minor: reuse base to avoid string duplication.

Not required, but reduces risk of path typos.

For example:

-  const navigationItems = [
+  const navigationItems = [
     {
       id: "requests",
       label: "Requests",
-      href: `/${workspace.slug}/apis/${currentApi.id}`,
+      href: base,
     },
   ];
@@
-    navigationItems.push({
+    navigationItems.push({
       id: "keys",
       label: "Keys",
-      href: `/${workspace.slug}/apis/${currentApi.id}/keys/${currentApi.keyAuthId}`,
+      href: `${base}/keys/${currentApi.keyAuthId}`,
     });
@@
-  navigationItems.push({
+  navigationItems.push({
     id: "settings",
     label: "Settings",
-    href: `/${workspace.slug}/apis/${currentApi.id}/settings`,
+    href: `${base}/settings`,
   });
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/page.tsx (2)

5-6: Remove unused imports (Loading, redirect).

Apply:

-import { Loading } from "@unkey/ui";
-import { redirect } from "next/navigation";

10-20: Guard render until workspace is available.

Prevents crashes if the hook is momentarily null before hydration.

Apply:

   const workspace = useWorkspaceNavigation();
 
   return (
+    !workspace ? null : (
     <div className="min-h-screen">
       <ApisNavbar
         apiId={apiId}
         activePage={{
           href: `/${workspace.slug}/apis/${apiId}`,
           text: "Requests",
         }}
       />
       <LogsClient apiId={apiId} />
     </div>
+    )
   );
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx (2)

3-3: Remove unused imports and keep the workspace hook (don’t switch to params).

Loading and redirect are unused. Continue using the workspace hook for security; don’t replace with route params.

Apply:

-import { Loading } from "@unkey/ui";
-import { redirect } from "next/navigation";

Also applies to: 6-7


16-24: Gate rendering until workspace is ready to avoid /undefined/... hrefs or crashes.

Keeps security of the hook while preventing transient issues.

Apply:

   const workspace = useWorkspaceNavigation();
 
   return (
-    <div>
+    !workspace ? null : (
+    <div>
       <ApisNavbar
         apiId={apiId}
         activePage={{
           href: `/${workspace.slug}/apis/${apiId}/settings`,
           text: "Settings",
         }}
       />
       <SettingsClient apiId={apiId} />
     </div>
+    )
   );
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx (1)

14-16: Guard against undefined or non-array filters to avoid runtime errors.

If data is truthy but filters is undefined or not an array, data?.filters.length can throw. Harden the check.

Apply:

-      if (data?.filters.length === 0 || !data) {
+      if (!data || !Array.isArray(data.filters) || data.filters.length === 0) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d72dbaa and 5c7bca5.

📒 Files selected for processing (26)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx (6 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx (7 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (13 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/settings-client.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspace]/authorization/permissions/navigation.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspace]/authorization/permissions/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspace]/authorization/roles/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (9 hunks)
  • apps/dashboard/app/(app)/[workspace]/identities/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/identities/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/logs/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/page.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (13)
  • apps/dashboard/app/(app)/[workspace]/logs/page.tsx
  • apps/dashboard/app/(app)/[workspace]/authorization/permissions/navigation.tsx
  • apps/dashboard/app/(app)/[workspace]/identities/page.tsx
  • apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx
  • apps/dashboard/app/(app)/[workspace]/authorization/permissions/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/page.tsx
  • apps/dashboard/app/(app)/[workspace]/authorization/roles/page.tsx
  • apps/dashboard/app/(app)/[workspace]/identities/navigation.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/settings-client.tsx
🧰 Additional context used
🧠 Learnings (13)
📓 Common learnings
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.238Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.

Applied to files:

  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx
📚 Learning: 2025-07-28T20:38:53.244Z
Learnt from: mcstepp
PR: unkeyed/unkey#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)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx
  • apps/dashboard/app/(app)/[workspace]/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx
  • apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx
  • apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx
  • apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
📚 Learning: 2025-09-22T18:44:56.238Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.238Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.

Applied to files:

  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/page.tsx
  • apps/dashboard/app/(app)/[workspace]/page.tsx
  • apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
PR: unkeyed/unkey#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)/[workspace]/apis/[apiId]/settings/page.tsx
  • apps/dashboard/app/(app)/[workspace]/page.tsx
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/page.tsx
📚 Learning: 2024-10-04T20:44:38.489Z
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.

Applied to files:

  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-04-24T14:34:30.621Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3115
File: apps/dashboard/components/logs/checkbox/filters-popover.tsx:33-55
Timestamp: 2025-04-24T14:34:30.621Z
Learning: In the ShortcutActivator component within filters-popover.tsx, the purpose is to track keys separately for each filter item, providing a registration mechanism for shortcuts passed to it rather than enforcing specific key combinations like option+shift+key.

Applied to files:

  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-05-15T16:26:08.666Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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)/[workspace]/page.tsx
  • apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx
  • apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx
📚 Learning: 2025-06-24T13:29:10.129Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
📚 Learning: 2025-08-25T13:46:08.303Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx:1-1
Timestamp: 2025-08-25T13:46:08.303Z
Learning: The NamespaceListDateTime component in apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx is intentionally designed to use the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than the namespace list hook, as clarified by ogzhanolguncu. This coupling is by design, not an architectural issue.

Applied to files:

  • apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
📚 Learning: 2025-08-25T13:46:34.441Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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)/[workspace]/logs/components/controls/components/logs-search/index.tsx
🔇 Additional comments (33)
apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx (1)

12-20: LGTM: Workspace-aware breadcrumbs

Good use of the workspace hook and slug-scoped pathing, consistent with the slug routing migration.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx (8)

5-5: Good: adopting workspace-aware navigation.

Using useWorkspaceNavigation aligns with slug routing and the security guidance to avoid URL params.


7-8: No action needed on imports.

Leaving as-is is fine.


56-58: LGTM: close-attempt handler default action is clear.


91-93: LGTM: support contact action is reasonable.


153-156: LGTM: anchoring the confirm popover via ref is fine.


166-168: LGTM: copy and structure.


175-177: LGTM: showing non-secret key ID is appropriate.


208-209: LGTM: copy tweak reads well.

apps/dashboard/app/(app)/[workspace]/apis/page.tsx (4)

4-13: Good switch to the workspace hook; verify loading guarantees

Nice move to use useWorkspaceNavigation for security and caching. Ensure it never yields undefined or that you guard UI while loading.


22-25: Guard breadcrumb until workspace is ready to avoid crash/bad URL
If workspace is undefined during initial render, accessing workspace.slug will crash; previously it emitted "/undefined/apis". Render a non-link crumb until ready.

-          <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/apis`} active>
-            APIs
-          </Navbar.Breadcrumbs.Link>
+          {workspace ? (
+            <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/apis`} active>
+              APIs
+            </Navbar.Breadcrumbs.Link>
+          ) : (
+            <Navbar.Breadcrumbs.Item active>APIs</Navbar.Breadcrumbs.Item>
+          )}

27-31: Avoid accessing workspace.slug before ready; drop unnecessary key prop
Key is meaningless outside a list. Also guard until workspace exists.

-          <CreateApiButton
-            key="createApi"
-            defaultOpen={isNewApi}
-            workspaceSlug={workspace.slug}
-          />
+          {workspace && (
+            <CreateApiButton
+              defaultOpen={isNewApi}
+              workspaceSlug={workspace.slug}
+            />
+          )}

34-34: Gate ApiListClient by workspace; show a loading fallback
Prevents runtime error and avoids rendering with an invalid slug.

-      <ApiListClient workspaceSlug={workspace.slug} />
+      {workspace ? (
+        <ApiListClient workspaceSlug={workspace.slug} />
+      ) : (
+        <Loading />
+      )}
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (7)

6-6: Verify workspace hook uses the auth-validated provider (not URL params).

Per retrieved learnings for this repo, the workspace must come from the secure workspace hook and not from URL params. Please confirm useWorkspaceNavigation() is backed by the authorized workspace context, not useParams.

If it’s not, switch to the secure workspace hook and plumb slug via that. This preserves the access checks and caching guarantees.


30-35: Dynamic import mapping LGTM.

Mapping to mod.KeysTableActions with an inline loader looks good.


41-49: UI polish LGTM.

Button/ICON classes and loading state tweaks are fine.


63-66: Keys query call LGTM.

Passing keyAuthId into useKeysListQuery aligns with the route segment.


160-177: Good: identity link is gated behind workspace presence.

This avoids constructing URLs from unvalidated params and aligns with the security guidance in retrieved learnings.


226-230: Confirm HiddenValueCell expects key.start.

Double-check KeyDetails includes start (and not e.g. prefix, preview, or maskedValue) and that HiddenValueCell renders it as intended.


411-422: Fallback skeleton for unknown columns LGTM.

Good generic guard; protects against layout regressions during column changes.

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

14-27: Guard slug to avoid “/undefined”; URL‑encode slug and reuse an encoded base path

Prevents transient /undefined/identities and ensures safe routing.

 export function Navigation({ identityId }: NavigationProps): JSX.Element {
-  const workspace = useWorkspaceNavigation();
+  const workspace = useWorkspaceNavigation();
+  const slug = workspace?.slug;
+  if (!slug) {
+    // Render nothing or a skeleton while workspace loads
+    return null;
+  }
+  const base = `/${encodeURIComponent(slug)}/identities`;
+  const encodedId = encodeURIComponent(identityId);
 
   return (
     <Navbar>
       <Navbar.Breadcrumbs
         icon={<Fingerprint aria-hidden="true" focusable={false} />}
       >
-        <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/identities`}>
+        <Navbar.Breadcrumbs.Link href={base}>
           Identities
         </Navbar.Breadcrumbs.Link>
         <Navbar.Breadcrumbs.Link
-          href={`/${workspace.slug}/identities/${encodeURIComponent(
-            identityId
-          )}`}
+          href={`${base}/${encodedId}`}
           className="w-[200px] truncate"
           active
           isIdentifier
         >
           {identityId}
         </Navbar.Breadcrumbs.Link>
apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (3)

161-172: Render key.meta inside

; avoid returning raw objects and preserve formatting

Prevents React from receiving a raw object and keeps whitespace.

-                        (() => {
-                          try {
-                            return JSON.stringify(
-                              JSON.parse(key.meta),
-                              null,
-                              2
-                            );
-                          } catch {
-                            return key.meta;
-                          }
-                        })()
+                        (() => {
+                          if (typeof key.meta === "string") {
+                            try {
+                              return (
+                                <pre className="whitespace-pre-wrap break-words">
+                                  {JSON.stringify(JSON.parse(key.meta), null, 2)}
+                                </pre>
+                              );
+                            } catch {
+                              return (
+                                <pre className="whitespace-pre-wrap break-words">{key.meta}</pre>
+                              );
+                            }
+                          }
+                          return (
+                            <pre className="whitespace-pre-wrap break-words">
+                              {JSON.stringify(key.meta, null, 2)}
+                            </pre>
+                          );
+                        })()

178-181: Good fix: non‑optional workspaceId

Using identity.workspace.id addresses the earlier nullability concern.


220-225: Ensure time delta arithmetic is numeric

If lastUsed is an ISO string, subtraction yields NaN. Use getTime().

-          <span className="text-content">
-            ({ms(Date.now() - lastUsed)} ago)
-          </span>
+          <span className="text-content">
+            ({ms(Date.now() - new Date(lastUsed).getTime())} ago)
+          </span>

Please confirm the type returned by clickhouse.verifications.latest().val?.at(0)?.time. If it’s not a number in ms, the above change is required.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx (1)

67-68: Revalidate path looks correct with workspace scoping.

Looks good assuming the route is /${workspace.slug}/apis/${apiId}/settings.

If you want, I can scan the repo for the canonical path of this page to double-check the revalidate target.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx (2)

31-66: Workspace-aware guard and revalidate path look solid.

Early-return prevents using an undefined slug, and the revalidate path is correctly workspace-scoped.

Consider aligning behavior with DefaultBytes (either both push to /new or both noop) for consistency. I can update both to a common pattern if desired.


104-106: LGTM on button props and layout tweaks.

apps/dashboard/app/(app)/[workspace]/page.tsx (2)

5-11: Blocker: render‑phase redirect and possible "/undefined/apis"; move to useEffect + guard slug

Call the redirect in an effect, guard until slug exists, and encode the segment.

Apply this diff within the function:

 export default function WorkspacePage() {
   const router = useRouter();
   const workspace = useWorkspaceNavigation();

-  router.replace(`/${workspace.slug}/apis`);
+  useEffect(() => {
+    if (!workspace?.slug) return;
+    router.replace(`/${encodeURIComponent(workspace.slug)}/apis`);
+  }, [router, workspace?.slug]);
 
   return null;
 }

Additional changes outside the selected range:

+"use client";
+import { useEffect } from "react";
 import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation";
-import { useRouter, redirect } from "next/navigation";
+import { useRouter } from "next/navigation";

7-7: Verify useWorkspaceNavigation sources slug from the validated workspace hook (not URL params).

Per retrieved learning, the slug must come from the secured workspace hook (useWorkspace) to enforce authorization/caching.

File: apps/dashboard/app/(app)/[workspace]/page.tsx — const workspace = useWorkspaceNavigation();

Confirm useWorkspaceNavigation reads the slug from useWorkspace (not useParams). Implementation was not found in the repository scan; if it uses useParams, replace/refactor to consume the validated workspace context.

apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx (4)

1-1: Good fix: Client component boundary added.

Adding "use client" resolves prior feedback and ensures hooks run on the client.


4-8: LGTM: import consolidation and readability tweaks.

Import grouping and multiline call improve readability without behavior changes.

Also applies to: 27-30


23-23: LGTM: trailing comma removed in toast options.

No functional impact; consistent with formatter rules.


2-2: Workspace-scoped imports confirmed — no stale old imports found

Repository search found no imports from "@/app/(app)/logs/hooks/use-filters". All occurrences use the workspace-scoped path "@/app/(app)/[workspace]/logs/hooks/use-filters" in:

  • apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/methods-filter.tsx
  • apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/paths-filter.tsx
  • apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/status-filter.tsx
  • apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/index.tsx
  • apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx

@perkinsjr
Copy link
Member Author

@coderabbitai resolve

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

✅ Actions performed

Comments resolved.

@perkinsjr
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

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/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (1)

56-68: Preserve link semantics; don’t blanket preventDefault() (breaks new‑tab/keyboard access).

Respect modifier/middle clicks and only intercept plain left‑clicks. Also guard against missing slug/key_auth_id and encode path segments.

Apply:

-  const handleLinkClick = useCallback(
-    (e: React.MouseEvent) => {
-      e.preventDefault();
-      setIsNavigating(true);
-
-      onNavigate?.();
-
-      router.push(
-        `/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`,
-      );
-    },
-    [apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router.push, workspace.slug],
-  );
+  const handleLinkClick = useCallback(
+    (e: React.MouseEvent<HTMLAnchorElement>) => {
+      // Let the browser handle new tab/window and middle/modified clicks.
+      if (
+        e.defaultPrevented ||
+        e.button !== 0 ||
+        e.metaKey ||
+        e.ctrlKey ||
+        e.altKey ||
+        e.shiftKey
+      ) {
+        return;
+      }
+      e.preventDefault();
+      if (!workspace?.slug || !log.key_details?.key_auth_id) return;
+      setIsNavigating(true);
+      onNavigate?.();
+      router.push(
+        `/${workspace.slug}/apis/${apiId}/keys/${encodeURIComponent(
+          log.key_details.key_auth_id,
+        )}/${encodeURIComponent(log.key_id)}`
+      );
+    },
+    [apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router, workspace?.slug],
+  );
🧹 Nitpick comments (22)
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/utils.ts (1)

5-9: Avoid inline typeof import(...) + biome-ignore; export and import the Refill type instead

This works and fixes the prior "type-only schema import" issue, but it’s brittle (deep path) and requires a format suppression. Prefer exporting Refill from the schema module and importing it here.

Apply in this file:

-// biome-ignore format: the comma after z.infer is incorrect syntax
-type Refill = z.infer<
-  typeof import("@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema").refillSchema
->;
+// Refill type comes from the schema module

Add this import (outside the selected lines):

+import type { Refill } from "@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema";

And in apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema.ts, export the type:

// inside create-key.schema.ts
import { z } from "zod";

export const refillSchema = /* existing schema */;

export type Refill = z.infer<typeof refillSchema>;
apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx (1)

3-8: Optional: Drop Suspense if nothing here suspends

If no child of Navbar suspends, the Suspense boundary and its import add noise. The early guard already renders Loading.

-import { Suspense } from "react";
+// import { Suspense } from "react"; // not needed if nothing below suspends
@@
-  return (
-    <Suspense fallback={<Loading type="spinner" />}>
-      <Navbar>
+  return (
+      <Navbar>
@@
-      </Navbar>
-    </Suspense>
+      </Navbar>

If other children in this tree do suspend, keep Suspense (or move it to the smallest subtree that actually suspends).

Also applies to: 17-18, 32-33

apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx (5)

24-66: Suspense boundary is ineffective here

The data-fetching hook runs above the boundary and isLoading is hardcoded false, so the fallback never shows. Remove the boundary (and related imports) to reduce complexity.

-    <Suspense fallback={<Loading type="spinner" />}>
-      <StatsCard
+    <StatsCard
         name={api.name}
         secondaryId={api.id}
         linkPath={`/${workspace.slug}/apis/${api.id}`}
         chart={
           <StatsTimeseriesBarChart
             data={timeseries}
             // INFO: Causing too much lag when there are too many Charts. We'll try to optimize this in the future.
             isLoading={false}
             isError={isError}
             config={{
               success: {
                 label: "Valid",
                 color: "hsl(var(--accent-4))",
               },
               error: {
                 label: "Invalid",
                 color: "hsl(var(--orange-9))",
               },
             }}
           />
         }
         stats={
           <>
             <MetricStats
               successCount={passed}
               errorCount={blocked}
               successLabel="VALID"
               errorLabel="INVALID"
             />
             <div className="flex items-center gap-2 min-w-0 max-w-[40%]">
               <Key className="text-accent-11 flex-shrink-0" />
               <div className="text-xs text-accent-9 truncate">
                 {`${formatNumber(keyCount)} ${keyCount === 1 ? "Key" : "Keys"}`}
               </div>
             </div>
           </>
         }
         icon={<ProgressBar className="text-accent-11" />}
-      />
-    </Suspense>
+      />

9-10: Remove unused imports if Suspense is removed

Clean up imports after removing the Suspense boundary.

-import { Loading } from "@unkey/ui";
-import { Suspense } from "react";

19-20: Compute success/error in a single pass

Avoid two reductions over the same array.

-  const passed = timeseries?.reduce((acc, crr) => acc + crr.success, 0) ?? 0;
-  const blocked = timeseries?.reduce((acc, crr) => acc + crr.error, 0) ?? 0;
+  const { passed, blocked } =
+    (timeseries ?? []).reduce(
+      (acc, { success, error }) => {
+        acc.passed += success;
+        acc.blocked += error;
+        return acc;
+      },
+      { passed: 0, blocked: 0 },
+    );

33-34: Consider minimal loading state for first paint

Hardcoding isLoading={false} removes helpful skeletons. A cheap compromise: only show loading until first data/error.

-            isLoading={false}
+            isLoading={!timeseries && !isError}

36-54: Label casing consistency and i18n

"Valid/Invalid" vs "VALID/INVALID" are inconsistent. Consider consistent casing and routing labels through i18n.

apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (2)

185-191: Request only the needed row: change limit from 50 to 1.

You only use the first element; reduce ClickHouse work and latency.

Apply this diff:

-      limit: 50,
+      limit: 1,

180-206: Avoid N+1 ClickHouse queries; batch by keyIds (and drop React.FC on async server component).

  • Batch last-used lookups for all keys in one query and map by keyId to eliminate per-row requests.
  • Minor: for server components, prefer const LastUsed = async (...) => {} without React.FC typing.
apps/dashboard/app/(app)/[workspace]/authorization/permissions/navigation.tsx (2)

27-39: Remove the outer Suspense; it’s redundant and hides the entire navbar

The dynamic import already renders a disabled trigger as its loading UI. Wrapping the whole Navbar in Suspense swaps the entire navigation for a spinner, degrading UX without benefit here.

Apply this diff:

-    <Suspense fallback={<Loading type="spinner" />}>
-      <Navbar className="w-full flex justify-between">
+    <Navbar className="w-full flex justify-between">
         <Navbar.Breadcrumbs icon={<ShieldKey />} className="flex-1 w-full">
           <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/authorization/roles`}>
             Authorization
           </Navbar.Breadcrumbs.Link>
           <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/authorization/permissions`} active>
             Permissions
           </Navbar.Breadcrumbs.Link>
         </Navbar.Breadcrumbs>
         <UpsertPermissionDialog triggerButton />
-      </Navbar>
-    </Suspense>
+      </Navbar>

6-6: Drop unused import after removing Suspense

Loading becomes unused with the above change.

Apply this diff:

-import { Loading } from "@unkey/ui";
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (1)

10-10: Remove Suspense wrapper; it’s a no‑op here.

Link does not suspend; the fallback never renders. Simplify and drop the import/wrapper.

Apply:

-import { Suspense, useCallback, useState } from "react";
+import { useCallback, useState } from "react";
-      <Suspense fallback={<Loading type="spinner" />}>
-        <Link
-          title={`View details for ${log.key_id}`}
-          className="font-mono group-hover:underline decoration-dotted"
-          href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
-          onClick={handleLinkClick}
-        >
-          <div className="font-mono font-medium truncate flex items-center">
-            {shortenId(log.key_id)}
-          </div>
-        </Link>
-      </Suspense>
+      {/* Link rendered directly; no suspense needed */}
+      <Link
+        title={`View details for ${log.key_id}`}
+        className="font-mono group-hover:underline decoration-dotted"
+        href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
+        onClick={handleLinkClick}
+      >
+        <div className="font-mono font-medium truncate flex items-center">
+          {shortenId(log.key_id)}
+        </div>
+      </Link>

Also applies to: 87-99

apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx (2)

10-21: Switch to Suspense-based dynamic import; drop ssr:false + loading

The outer Suspense won’t handle this dynamic import because suspense: true isn’t enabled. Prefer Suspense-driven loading and avoid duplicative fallbacks.

Apply this diff:

-const UpsertRoleDialog = dynamic(
-  () => import("./components/upsert-role").then((mod) => mod.UpsertRoleDialog),
-  {
-    ssr: false,
-    loading: () => (
-      <NavbarActionButton disabled>
-        <Plus />
-        Create new role
-      </NavbarActionButton>
-    ),
-  },
-);
+const UpsertRoleDialog = dynamic(
+  () => import("./components/upsert-role").then((mod) => mod.UpsertRoleDialog),
+  { suspense: true },
+);

6-6: Remove unused Loading import after scoping Suspense

If you adopt the scoped Suspense, Loading becomes unused.

-import { Loading } from "@unkey/ui";
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx (5)

212-221: Reorder guards so error handling isn’t shadowed.

Currently !layoutData is handled before error, making the error branch mostly unreachable.

Apply this diff:

-  // Show loading state while fetching data
-  if (isLoading || !layoutData) {
+  // Show loading state while fetching data
+  if (isLoading) {
     return <LoadingNavbar workspace={workspace} />;
   }
 
-  // Handle error state
-  if (error) {
-    console.error("Failed to fetch API layout data:", error);
-    return <LoadingNavbar workspace={workspace} />;
-  }
+  // Handle error or missing data
+  if (error || !layoutData) {
+    console.error("Failed to fetch API layout data:", error);
+    return <LoadingNavbar workspace={workspace} />;
+  }

111-116: Remove noisy console.warn or gate it to dev.

This warning triggers every time key params are present and adds noise.

Apply this diff:

-  // If we expected to find a key but this component doesn't handle key details,
-  // we should handle this at a higher level or in a different component
-  if (shouldFetchKey) {
-    console.warn("Key fetching logic should be handled at a higher level");
-  }

123-146: DRY: reuse base for nav hrefs.

Reduces duplication and risk of mismatched paths.

Apply this diff:

   const navigationItems = [
     {
       id: "requests",
       label: "Requests",
-      href: `/${workspace.slug}/apis/${currentApi.id}`,
+      href: base,
     },
   ];

   // Add Keys navigation if keyAuthId exists
   if (currentApi.keyAuthId) {
     navigationItems.push({
       id: "keys",
       label: "Keys",
-      href: `/${workspace.slug}/apis/${currentApi.id}/keys/${currentApi.keyAuthId}`,
+      href: `${base}/keys/${currentApi.keyAuthId}`,
     });
   }

   // Add Settings navigation
   navigationItems.push({
     id: "settings",
     label: "Settings",
-    href: `/${workspace.slug}/apis/${currentApi.id}/settings`,
+    href: `${base}/settings`,
   });

13-41: Derive ApiLayoutData from tRPC types to avoid drift.

Manual mirrors of API shapes tend to rot. Prefer deriving the type from your tRPC router/client exports.

Example (adapt to your trpc exports):

// e.g., if you export RouterOutputs
// import type { RouterOutputs } from "@/lib/trpc/types";
type ApiLayoutData = /* RouterOutputs["api"]["queryApiKeyDetails"] */ any;

// or, if available, from the client helper:
// type ApiLayoutData = NonNullable<Awaited<ReturnType<(typeof trpc.api.queryApiKeyDetails)["fetch"]>>>;

53-56: Narrow workspace prop to Pick<Workspace, "slug">

Only workspace.slug is used for URLs—avoid coupling props to the full DB type; change LoadingNavbarProps and NavbarContentProps to workspace: Pick<Workspace, "slug">.

-interface LoadingNavbarProps {
-  workspace: Workspace;
-}
+interface LoadingNavbarProps {
+  workspace: Pick<Workspace, "slug">;
+}
 
-interface NavbarContentProps {
+interface NavbarContentProps {
   apiId: string;
   keyspaceId?: string;
   keyId?: string;
   activePage?: {
     href: string;
     text: string;
   };
-  workspace: Workspace;
+  workspace: Pick<Workspace, "slug">;
   isMobile: boolean;
   layoutData: ApiLayoutData;
 }
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/page.tsx (2)

14-22: Suspense wrapper is a no-op here; remove it or enable suspense in the query.

ApisNavbar uses useQuery without suspense: true, so this Suspense never shows. Simplest: remove Suspense and its imports.

Apply this diff:

-import { Loading } from "@unkey/ui";
-import { Suspense } from "react";
+// Suspense not needed unless the child suspends

@@
-      <Suspense fallback={<Loading type="spinner" />}>
-        <ApisNavbar
-          apiId={apiId}
-          activePage={{
-            href: `/${workspace.slug}/apis/${apiId}`,
-            text: "Requests",
-          }}
-        />
-      </Suspense>
+      <ApisNavbar
+        apiId={apiId}
+        activePage={{
+          href: `/${workspace.slug}/apis/${apiId}`,
+          text: "Requests",
+        }}
+      />

Alternatively, keep Suspense and set suspense: true on the TRPC query.

Also applies to: 4-6


1-1: Consider server component page with a small client child.

Making the entire page client-side increases bundle size and foregoes SSR. If only navigation needs client hooks, keep the page server and move hooks into client children.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/index.tsx (1)

212-221: Gate the Suspense-mounted dialog to avoid page-level spinner/flicker.

Without gating, if the dialog suspends on mount, a spinner may render inline on initial load. Mount only when open.

-      <Suspense fallback={<Loading type="spinner" />}>
-        <KeyCreatedSuccessDialog
-          apiId={apiId}
-          keyspaceId={keyspaceId}
-          isOpen={successDialogOpen}
-          onClose={handleSuccessDialogClose}
-          keyData={createdKeyData}
-          onCreateAnother={openNewKeyDialog}
-        />
-      </Suspense>
+      {successDialogOpen && (
+        <Suspense fallback={<Loading type="spinner" />}>
+          <KeyCreatedSuccessDialog
+            apiId={apiId}
+            keyspaceId={keyspaceId}
+            isOpen={successDialogOpen}
+            onClose={handleSuccessDialogClose}
+            keyData={createdKeyData}
+            onCreateAnother={openNewKeyDialog}
+          />
+        </Suspense>
+      )}

Optional: code-split the dialog to reduce initial bundle.

import dynamic from "next/dynamic";
const KeyCreatedSuccessDialog = dynamic(
  () => import("./components/key-created-success-dialog").then(m => m.KeyCreatedSuccessDialog),
  { loading: () => <Loading type="spinner" />, ssr: false },
);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c7bca5 and 1a086c6.

📒 Files selected for processing (28)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/index.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/utils.ts (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-metadata/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (5 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/settings-client.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/apis/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/authorization/permissions/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspace]/identities/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/identities/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/logs/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspace]/page.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (18)
  • apps/dashboard/app/(app)/[workspace]/identities/navigation.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx
  • apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
  • apps/dashboard/app/(app)/[workspace]/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx
  • apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx
  • apps/dashboard/app/(app)/[workspace]/logs/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-metadata/index.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/index.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
  • apps/dashboard/app/(app)/[workspace]/identities/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/settings-client.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/page.tsx
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.
📚 Learning: 2025-09-22T18:44:56.238Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.238Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.

Applied to files:

  • apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx
📚 Learning: 2024-10-08T15:33:04.290Z
Learnt from: AkshayBandi027
PR: unkeyed/unkey#2215
File: apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx:28-29
Timestamp: 2024-10-08T15:33:04.290Z
Learning: In `authorization/roles/[roleId]/update-role.tsx`, the tag `role-${role.id}` is revalidated after updating a role to ensure that the caching mechanism is properly handled for roles.

Applied to files:

  • apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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)/[workspace]/identities/[identityId]/navigation.tsx
🔇 Additional comments (23)
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/utils.ts (1)

1-1: LGTM: workspace-scoped import path

The updated import to the workspace-scoped utils path looks correct.

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

13-25: Guard and encode slug to prevent crashes and “/undefined” links

workspace.slug is used without a guard and without encoding. If the slug isn’t ready, this can crash or render invalid hrefs. Precompute an encoded base once and early‑return a loader if missing.

 export function Navigation({ identityId }: NavigationProps): JSX.Element {
   const workspace = useWorkspaceNavigation();
 
+  const slug = workspace?.slug;
+  if (!slug) {
+    return <Loading type="spinner" />;
+  }
+  const base = `/${encodeURIComponent(slug)}/identities`;
+  const encodedId = encodeURIComponent(identityId);
+
   return (
     <Suspense fallback={<Loading type="spinner" />}>
       <Navbar>
         <Navbar.Breadcrumbs icon={<Fingerprint aria-hidden="true" focusable={false} />}>
-          <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/identities`}>
+          <Navbar.Breadcrumbs.Link href={base}>
             Identities
           </Navbar.Breadcrumbs.Link>
           <Navbar.Breadcrumbs.Link
-            href={`/${workspace.slug}/identities/${encodeURIComponent(identityId)}`}
+            href={`${base}/${encodedId}`}
             className="w-[200px] truncate"
             active
             isIdentifier
           >
             {identityId}
           </Navbar.Breadcrumbs.Link>
apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx (2)

22-22: Defensive guard around api.keys

If api.keys can be absent, this will throw. Safe-fallback to 0.

-  const keyCount = api.keys.reduce((acc, crr) => acc + crr.count, 0);
+  const keyCount = api.keys?.reduce((acc, crr) => acc + crr.count, 0) ?? 0;

Can you confirm ApiOverview['keys'] is always a defined array?


28-28: Verify workspace slug availability and route shape

Ensure workspace.slug is always defined here and the route pattern matches /{workspace}/apis/{apiId} after slug routing changes.

apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (6)

19-19: Auth guard via notFound() import looks good.

Import is used correctly to gate unauthorized access.


35-38: Including workspace.id and workspace.slug is correct for downstream usage.

Needed for LastUsed and slug-prefixed links.


156-156: Non-optional workspaceId fix looks correct.

Passing identity.workspace.id satisfies the string prop and matches the prior guard.


161-163: Slug‑prefixed link is correct and avoids protocol‑relative // URLs.

Matches the new workspace‑scoped routing.


198-200: Verify timestamp units (ms vs s).

If lastUsed is seconds, both new Date(lastUsed) and Date.now() - lastUsed will be wrong. Confirm it’s milliseconds; if seconds, multiply by 1000.

Apply this diff if it’s seconds:

-          <span className="text-content-subtle">{new Date(lastUsed).toUTCString()}</span>
-          <span className="text-content">({ms(Date.now() - lastUsed)} ago)</span>
+          <span className="text-content-subtle">{new Date(lastUsed * 1000).toUTCString()}</span>
+          <span className="text-content">({ms(Date.now() - lastUsed * 1000)} ago)</span>

144-150: Fix meta rendering: avoid returning objects to React and preserve whitespace.

Current IIFE can return a raw object (React crash) and collapses JSON formatting in a table cell. Render inside a

 and handle string vs object explicitly.

Apply this diff:

-                    <TableCell className="font-mono text-xs">
-                      {key.meta ? (
-                        (() => {
-                          try {
-                            return JSON.stringify(JSON.parse(key.meta), null, 2);
-                          } catch {
-                            return key.meta;
-                          }
-                        })()
-                      ) : (
-                        <Minus className="text-content-subtle w-4 h-4" />
-                      )}
-                    </TableCell>
+                    <TableCell className="font-mono text-xs">
+                      {key.meta ? (
+                        typeof key.meta === "string" ? (
+                          (() => {
+                            let text = key.meta;
+                            try {
+                              text = JSON.stringify(JSON.parse(key.meta), null, 2);
+                            } catch {}
+                            return <pre className="whitespace-pre-wrap break-words">{text}</pre>;
+                          })()
+                        ) : (
+                          <pre className="whitespace-pre-wrap break-words">
+                            {JSON.stringify(key.meta, null, 2)}
+                          </pre>
+                        )
+                      ) : (
+                        <Minus className="text-content-subtle w-4 h-4" />
+                      )}
+                    </TableCell>
apps/dashboard/app/(app)/[workspace]/authorization/permissions/navigation.tsx (3)

23-41: LGTM overall

Solid client-side nav with workspace-scoped breadcrumbs and a lazy-loaded dialog trigger. Pattern aligns with the PR’s workspace routing changes.


10-12: UpsertPermissionDialog export verified — dynamic import OK
Named export found at apps/dashboard/app/(app)/[workspace]/authorization/permissions/components/upsert-permission/index.tsx:48.


30-33: Keep breadcrumb pointing to /authorization/roles — no /authorization index found

Search returned only apps/dashboard/app/(app)/[workspace]/authorization/roles/page.tsx and no apps/dashboard/app/(app)/[workspace]/authorization/page.*. Leave the link as-is; if you want the root crumb to go to /authorization, add an index page at apps/dashboard/app/(app)/[workspace]/authorization/page.tsx and then update the href.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (1)

2-2: Confirmed: useWorkspaceNavigation uses useWorkspace (no useParams)

Verified apps/dashboard/hooks/use-workspace-navigation.tsx imports and uses useWorkspace and contains no useParams references — workspace slug is sourced from the secured workspace context.

apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx (3)

1-5: LGTM: Solid client-side nav composition

Client component + workspace-aware breadcrumbs setup looks good.


37-38: Ensure dialog triggers role cache revalidation on success

Per prior learning, revalidate role-${role.id} after updates to keep lists/breadcrumbs fresh. Verify UpsertRoleDialog does this.


24-25: Confirm workspace.slug is always defined (or guard hrefs)
useWorkspaceNavigation throws while loading and redirects to /new when no workspace (apps/dashboard/hooks/use-workspace-navigation.tsx), so it won’t return null — verify the Workspace returned by the trpc/backend always includes a non-empty slug; if not, guard link creation or fall back to workspace.id.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx (3)

70-99: Skeleton loading state reads well and matches the final layout.


174-186: Create key action gating is correct.

Correctly disables creation when no keyspace/keyAuth is present.


198-210: Good use of guarded query with retry/backoff.

Enabled flag prevents spurious calls; retry settings are sensible defaults.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/page.tsx (1)

8-25: Overall structure LGTM.

Workspace-scoped href and navbar wiring look correct.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/index.tsx (2)

20-20: LGTM on adding Suspense import.


11-11: Confirm Loading export and prop API.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/index.tsx imports Loading from @unkey/ui and uses as the Suspense fallback; repo search did not locate the @unkey/ui source or a Loading export — verify that @unkey/ui exports Loading and that the correct prop is type="spinner" (not variant/size).

@perkinsjr
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

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/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx (1)

10-20: Fix workspace hook usage before building slug hrefs.

Line 10: useWorkspaceNavigation() returns an object { workspace, ... }, not the workspace itself. Assigning it directly makes workspace the wrapper object, so Line 19 ends up reading workspace.slug, which resolves to undefined. That breaks the navbar link (/undefined/apis/...), blocking navigation on this page. Please destructure the workspace property before using it.

-  const workspace = useWorkspaceNavigation();
+  const { workspace } = useWorkspaceNavigation();
🧹 Nitpick comments (14)
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (2)

151-156: Remove redundant workspace null-check.

useWorkspaceNavigation guarantees a workspace; keep only the identity_id guard.

-                        {key.identity_id && workspace ? (
+                        {key.identity_id ? (
                           <Link
                             title={"View details for identity"}
                             className="font-mono group-hover:underline decoration-dotted"
-                            href={`/${workspace.slug}/identities/${key.identity_id}`}
+                            href={`/${workspace.slug}/identities/${key.identity_id}`}
                             target="_blank"
                             rel="noopener noreferrer"
                             aria-disabled={isNavigating}
                           >

271-272: Narrow useMemo deps to workspace.slug.

Avoid invalidating columns when the workspace object identity changes.

-      workspace,
+      workspace.slug,
apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/create-api-button.tsx (1)

50-56: Parallelize cache invalidation and encode path segment in push

Run invalidation and revalidation concurrently and encode the id in the push URL.

   const create = trpc.api.create.useMutation({
     async onSuccess(res) {
       toast.success("Your API has been created");
-      await revalidate(`/${workspaceSlug}/apis`);
-      api.overview.query.invalidate();
-      router.push(`/${workspaceSlug}/apis/${res.id}`);
+      await Promise.all([
+        revalidate(`/${workspaceSlug}/apis`),
+        api.overview.query.invalidate(),
+      ]);
+      router.push(`/${workspaceSlug}/apis/${encodeURIComponent(res.id)}`);
       setIsOpen(false);
     },
apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx (1)

1-3: Avoid client layout; use segment loading.tsx for fallback

Marking the layout as a Client Component forces the entire subtree client-side. Prefer a Server Component layout and a route-segment loading.tsx for the spinner.

Proposed change in this file:

-"use client";
-
-import { Loading } from "@unkey/ui";
-import { Suspense } from "react";
+// Server layout; no client imports here

Move the fallback to a new loading.tsx:

// apps/dashboard/app/(app)/[workspaceSlug]/loading.tsx
"use client";
import { Loading } from "@unkey/ui";

export default function WorkspaceLoading() {
  return (
    <div className="flex items-center justify-center w-full h-full min-h-[200px]">
      <div className="flex flex-col items-center gap-4">
        <Loading size={24} />
        <p className="text-sm text-gray-600 dark:text-gray-400">Loading workspace...</p>
      </div>
    </div>
  );
}

Also applies to: 21-23

apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (2)

67-68: Fix useCallback deps: depend on router, not router.push

router.push is a method reference; depend on the router object for stability.

-    [apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router.push, workspace.slug],
+    [apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router, workspace.slug],

87-99: Remove unnecessary Suspense around a plain Link

No async boundary here; Suspense adds overhead without benefit.

-      <Suspense fallback={<Loading type="spinner" />}>
-        <Link
-          title={`View details for ${log.key_id}`}
-          className="font-mono group-hover:underline decoration-dotted"
-          href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
-          onClick={handleLinkClick}
-        >
-          <div className="font-mono font-medium truncate flex items-center">
-            {shortenId(log.key_id)}
-          </div>
-        </Link>
-      </Suspense>
+      <Link
+        title={`View details for ${log.key_id}`}
+        className="font-mono group-hover:underline decoration-dotted"
+        href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
+        onClick={handleLinkClick}
+      >
+        <div className="font-mono font-medium truncate flex items-center">
+          {shortenId(log.key_id)}
+        </div>
+      </Link>
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx (2)

73-76: Encode dynamic URL segments

Ensure IDs are safely encoded.

-        href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
+        href={`/${workspace.slug}/apis/${encodeURIComponent(apiId)}/keys/${encodeURIComponent(
+          log.key_details?.key_auth_id ?? "",
+        )}/${encodeURIComponent(log.key_id)}`}

117-119: Remove Suspense around static content

No async children; wrap higher-level panels if needed, not this static section.

-      <Suspense fallback={<Loading type="spinner" />}>
-        <LogSection title="Identifiers" details={identifiers} />
-      </Suspense>
+      <LogSection title="Identifiers" details={identifiers} />
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsx (1)

20-27: Encode route params in href

Avoid accidental path issues if ids contain special characters.

       <ApisNavbar
         apiId={apiId}
         activePage={{
-          href: `/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}`,
+          href: `/${workspace.slug}/apis/${encodeURIComponent(apiId)}/keys/${encodeURIComponent(
+            keyspaceId,
+          )}`,
           text: "Keys",
         }}
       />
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/api-id-navbar.tsx (3)

113-115: Silence dev-only console.warn in production

Guard the warning to avoid noisy logs in production.

-  if (shouldFetchKey) {
-    console.warn("Key fetching logic should be handled at a higher level");
-  }
+  if (shouldFetchKey && process.env.NODE_ENV !== "production") {
+    console.warn("Key fetching logic should be handled at a higher level");
+  }

200-210: Use exponential backoff for retries

Aligns with the PR’s reliability goals and avoids thundering-herd retry bursts.

   } = trpc.api.queryApiKeyDetails.useQuery(
     { apiId },
     {
       enabled: Boolean(apiId), // Only run query if apiId exists
       retry: 3,
-      retryDelay: 1000,
+      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 8000),
     },
   );

166-169: Default breadcrumb label when activePage is absent

Avoid empty text for the breadcrumb trigger.

-              <div className="hover:bg-gray-3 rounded-lg flex items-center gap-1 p-1">
-                {activePage?.text ?? ""}
+              <div className="hover:bg-gray-3 rounded-lg flex items-center gap-1 p-1">
+                {activePage?.text ?? "Requests"}
                 <ChevronExpandY className="size-4" />
               </div>
apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/page.tsx (1)

180-206: Avoid typing async components as React.FC.

React.FC doesn’t model async server components. Prefer a named async function with an explicit props type.

Apply this diff:

-const LastUsed: React.FC<{
-  workspaceId: string;
-  keySpaceId: string;
-  keyId: string;
-}> = async (props) => {
+type LastUsedProps = {
+  workspaceId: string;
+  keySpaceId: string;
+  keyId: string;
+};
+
+async function LastUsed(props: LastUsedProps) {
@@
-};
+}
apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx (1)

33-49: Prefetch is a no-op and timeout type is brittle; prefetch via router and clean up.

Currently the timer sets a flag but never prefetches. Also prefer ReturnType<typeof setTimeout> for client safety and clear timers on unmount.

Apply this diff:

-  const prefetchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
+  const prefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@
-  const handlePrefetch = useCallback(() => {
-    if (hasPrefetchedRef.current) {
-      return;
-    }
-
-    if (prefetchTimeoutRef.current) {
-      clearTimeout(prefetchTimeoutRef.current);
-    }
-
-    prefetchTimeoutRef.current = setTimeout(() => {
-      // Use Link's built-in prefetch behavior instead of manual router.prefetch
-      hasPrefetchedRef.current = true;
-    }, 100); // 100ms debounce
-  }, []);
+  const handlePrefetch = useCallback(() => {
+    if (hasPrefetchedRef.current) return;
+    if (prefetchTimeoutRef.current) clearTimeout(prefetchTimeoutRef.current);
+    prefetchTimeoutRef.current = setTimeout(() => {
+      router.prefetch(detailsUrl);
+      hasPrefetchedRef.current = true;
+    }, 100); // 100ms debounce
+  }, [router, detailsUrl]);

Additionally, add unmount cleanup and an accessibility role:

+  // Cleanup pending timer on unmount
+  useEffect(() => {
+    return () => {
+      if (prefetchTimeoutRef.current) clearTimeout(prefetchTimeoutRef.current);
+    };
+  }, []);
@@
-    <TableRow
+    <TableRow
+      role="link"

And import useEffect:

-import { memo, useCallback, useMemo, useRef } from "react";
+import { memo, useCallback, useMemo, useRef, useEffect } from "react";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d1805f and c3a5dbb.

📒 Files selected for processing (81)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/external-id-field/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/key-secret-section.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/index.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/api-id-navbar.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/charts/bar-chart/utils.ts (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/components/logs-filters/outcome-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/controls/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/utils.ts (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-expiration/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-expiration/utils.ts (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-external-id/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-key-name.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-metadata/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-ratelimits/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-ratelimits/utils.ts (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/components/assign-permission/permissions-field.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/components/assign-role/create-key-options.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/hooks/use-edit-key.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/keys-table-action.popover.constants.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls/components/batch-edit-external-id.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (5 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsx (4 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-api.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-protection.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/key-settings-form-helper.ts (3 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/settings-client.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/skeleton.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/update-api-name.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/api-list-card.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/api-list-client.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/create-api-button.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/bucket-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/events-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/users-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-queries/utils.ts (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/constants.ts (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/components/table/components/actions/components/hooks/use-delete-permission.ts (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/components/table/components/selection-controls/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/components/table/permissions-list.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/components/hooks/use-delete-role.ts (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/components/hooks/use-fetch-connected-keys-and-perms.ts (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/keys-table-action.popover.constants.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/selection-controls/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/roles-list.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/upsert-role/components/assign-key/create-key-options.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/upsert-role/components/assign-permission/create-permission-options.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/navigation.tsx (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/page.tsx (3 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-display/components/display-popover.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/methods-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/paths-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/status-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-queries/utils.ts (2 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-search/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/table/log-details/components/log-footer.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/navigation.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/page.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (9)
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/events-filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-protection.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/update-api-name.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-display/components/display-popover.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/components/logs-filters/outcome-filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/external-id-field/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-external-id/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/hooks/use-edit-key.tsx
🧰 Additional context used
🧠 Learnings (19)
📓 Common learnings
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.279Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.
Learnt from: perkinsjr
PR: unkeyed/unkey#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.
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:46:49.043Z
Learning: In the Unkey dashboard, there is no overview page at /${workspace.slug}/authorization. The roles page at /${workspace.slug}/authorization/roles serves as the default/primary page for the authorization section, so breadcrumb navigation appropriately points both "Authorization" and "Roles" breadcrumbs to the roles page.
Learnt from: perkinsjr
PR: unkeyed/unkey#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.
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.
📚 Learning: 2025-09-22T18:44:56.279Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.279Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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]/apis/[apiId]/settings/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx
📚 Learning: 2025-09-23T17:39:59.820Z
Learnt from: perkinsjr
PR: unkeyed/unkey#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]/apis/[apiId]/settings/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-06-24T13:29:10.129Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/status-filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-queries/utils.ts
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/key-settings-form-helper.ts
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-search/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/methods-filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-queries/utils.ts
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/users-filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/bucket-filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/paths-filter.tsx
📚 Learning: 2025-06-19T11:48:05.070Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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/app/(app)/[workspaceSlug]/authorization/roles/components/table/roles-list.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/components/hooks/use-delete-role.ts
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/components/hooks/use-fetch-connected-keys-and-perms.ts
📚 Learning: 2024-10-08T15:33:04.290Z
Learnt from: AkshayBandi027
PR: unkeyed/unkey#2215
File: apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx:28-29
Timestamp: 2024-10-08T15:33:04.290Z
Learning: In `authorization/roles/[roleId]/update-role.tsx`, the tag `role-${role.id}` is revalidated after updating a role to ensure that the caching mechanism is properly handled for roles.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/roles-list.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/components/hooks/use-delete-role.ts
📚 Learning: 2025-09-23T17:46:49.043Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:46:49.043Z
Learning: In the Unkey dashboard, there is no overview page at /${workspace.slug}/authorization. The roles page at /${workspace.slug}/authorization/roles serves as the default/primary page for the authorization section, so breadcrumb navigation appropriately points both "Authorization" and "Roles" breadcrumbs to the roles page.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/constants.ts
  • apps/dashboard/app/(app)/[workspaceSlug]/audit/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/upsert-role/components/assign-key/create-key-options.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-08-25T13:46:34.441Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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]/logs/components/controls/components/logs-search/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/index.tsx
📚 Learning: 2025-08-25T13:46:08.303Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx:1-1
Timestamp: 2025-08-25T13:46:08.303Z
Learning: The NamespaceListDateTime component in apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx is intentionally designed to use the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than the namespace list hook, as clarified by ogzhanolguncu. This coupling is by design, not an architectural issue.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-search/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/controls/index.tsx
📚 Learning: 2025-09-23T17:40:44.944Z
Learnt from: perkinsjr
PR: unkeyed/unkey#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]/apis/[apiId]/_components/create-key/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/api-id-navbar.tsx
📚 Learning: 2024-10-20T07:05:55.471Z
Learnt from: chronark
PR: unkeyed/unkey#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/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-ratelimits/index.tsx
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx
📚 Learning: 2024-12-03T14:23:07.189Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2143
File: apps/dashboard/app/(app)/logs/components/log-details/resizable-panel.tsx:37-49
Timestamp: 2024-12-03T14:23:07.189Z
Learning: In `apps/dashboard/app/(app)/logs/components/log-details/resizable-panel.tsx`, the resize handler is already debounced.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx
📚 Learning: 2024-12-03T14:17:08.016Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsx
📚 Learning: 2024-11-29T15:15:47.308Z
Learnt from: chronark
PR: unkeyed/unkey#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/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls/components/batch-edit-external-id.tsx
📚 Learning: 2025-06-19T13:01:55.338Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls/components/batch-edit-external-id.tsx
📚 Learning: 2024-10-04T20:44:38.489Z
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#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/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
🧬 Code graph analysis (3)
apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/navigation.tsx (1)
apps/dashboard/app/(app)/identities/[identityId]/navigation.tsx (1)
  • Navigation (10-26)
apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (2)
apps/dashboard/app/(app)/identities/navigation.tsx (1)
  • Navigation (6-16)
apps/dashboard/app/(app)/identities/[identityId]/navigation.tsx (1)
  • Navigation (10-26)
apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)
apps/dashboard/app/(app)/identities/page.tsx (1)
  • Page (26-66)
⏰ 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: Analyze (javascript-typescript)

@graphite-app
Copy link

graphite-app bot commented Sep 26, 2025

Digital art gif. A giant green hand with legs smiles happily and bounces, giving a big thumbs up. (Added via Giphy)

@graphite-app
Copy link

graphite-app bot commented Sep 26, 2025

Graphite Automations

"Post a GIF when PR approved" took an action on this PR • (09/26/25)

1 gif was posted to this PR based on Andreas Thomas's automation.

@perkinsjr perkinsjr merged commit 55c3dd1 into main Sep 26, 2025
17 checks passed
@perkinsjr perkinsjr deleted the slug-routing branch September 26, 2025 12:20
@coderabbitai coderabbitai bot mentioned this pull request Sep 26, 2025
18 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:dashboard Unkey dashboard related Feature New feature or request 🕹️ 300 points UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants