Skip to content

feat: WIP DON'T REVIEW WIP WIP WIP implement workspace-based URL routing architecture#4008

Closed
perkinsjr wants to merge 7 commits intomainfrom
slug-routing
Closed

feat: WIP DON'T REVIEW WIP WIP WIP implement workspace-based URL routing architecture#4008
perkinsjr wants to merge 7 commits intomainfrom
slug-routing

Conversation

@perkinsjr
Copy link
Member

@perkinsjr perkinsjr commented Sep 21, 2025

What does this PR do?

This commit introduces a comprehensive refactoring of the dashboard routing system to support workspace-based URLs, enabling multi-workspace functionality and improved navigation patterns.

Key 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 200+ components, pages, and utilities
to work within the new workspace-scoped file structure, including:

  • API management pages and components
  • Key creation and management flows
  • Logs viewing and filtering
  • Settings and billing pages
  • Identity and permission management

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

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

UI/UX Improvements:

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

Bug Fixes:

  • Resolved merge conflicts from main branch integration
  • Fixed sidebar navigation issues
  • Corrected workspace loading performance issues
  • Fixed various linting and TypeScript errors
  • Addressed button nesting and HTML structure issues

This refactoring enables users to work seamlessly across multiple workspaces while maintaining clean, SEO-friendly URLs and improving overall application architecture. All existing functionality is preserved while adding robust multi-workspace support.

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?

WILL FILLL SOON

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

Summary by CodeRabbit

  • New Features

    • Workspace-scoped navigation and pages for APIs, Logs, Authorization, and Identities (including new nav bars and redirects).
    • Identities: searchable list with results table and detail pages.
    • API page gains a contextual navbar and workspace-aware Create API flow.
  • Improvements

    • All dashboard links/routes now include the workspace slug for correct scoping.
    • Keys and settings pages use workspace-aware paths and cache revalidation.
    • Selection in Roles/Permissions lists now operates by IDs, improving accuracy.
  • Bug Fixes

    • Deletion toasts/counts use actual server deletedCount for Roles/Permissions.
  • Style

    • Minor UI polish (border class adjustments).

This commit introduces a comprehensive refactoring of the dashboard routing system
to support workspace-based URLs, enabling multi-workspace functionality and
improved navigation patterns.

Key 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 200+ components, pages, and utilities
  to work within the new workspace-scoped file structure, including:
  - API management pages and components
  - Key creation and management flows
  - Logs viewing and filtering
  - Settings and billing pages
  - Identity and permission management

• **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 Updates**:
  - Modified database queries to be workspace-aware
  - Updated API endpoints to handle workspace parameters
  - Enhanced caching strategies for workspace-scoped data
  - Fixed workspace loading and switching functionality

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

• **Bug Fixes**:
  - Resolved merge conflicts from main branch integration
  - Fixed sidebar navigation issues
  - Corrected workspace loading performance issues
  - Fixed various linting and TypeScript errors
  - Addressed button nesting and HTML structure issues

This refactoring enables users to work seamlessly across multiple workspaces while
maintaining clean, SEO-friendly URLs and improving overall application architecture.
All existing functionality is preserved while adding robust multi-workspace support.
@changeset-bot
Copy link

changeset-bot bot commented Sep 21, 2025

⚠️ No Changeset found

Latest commit: bb6d555

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 21, 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 22, 2025 0:58am
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
engineering Ignored Ignored Preview Sep 22, 2025 0:58am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 21, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

Introduces workspace-scoped routing across dashboard pages by adding useWorkspace and dynamic hrefs. Adds new workspace-aware navigation components and pages (logs, identities, authorization). Updates numerous imports to include [workspace] path segment. Adjusts some component props (CreateApiButton, ApiListClient). Minor logic tweaks in delete handlers and selections; minor styling updates.

Changes

Cohort / File(s) Summary
Workspace-scoped routing for APIs
apps/dashboard/app/(app)/[workspace]/apis/page.tsx, .../apis/[apiId]/page.tsx, .../apis/[apiId]/settings/page.tsx, .../apis/[apiId]/keys/[keyAuthId]/page.tsx, .../apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx, .../apis/_components/api-list-card.tsx, .../apis/[apiId]/api-id-navbar.tsx
Added useWorkspace; converted static paths to /${workspace.slug}/...; introduced/updated ApisNavbar; dynamic breadcrumb/activePage hrefs.
Keys: routing and imports
.../apis/[apiId]/_overview/components/table/components/log-details/index.tsx, .../_overview/components/table/components/override-indicator.tsx, .../create-key/components/*, .../keys/[keyAuthId]/[keyId]/components/**/*, .../keys/[keyAuthId]/_components/**/*, .../keys/[keyAuthId]/_components/.../keys-list.tsx
Prefixed key links with workspace slug; updated many imports to [workspace] paths; no logic changes except dependency arrays updated to include workspace slug.
Logs: workspace-aware pages and hooks
.../[workspace]/logs/page.tsx, .../[workspace]/logs/navigation.tsx, .../[workspace]/logs/components/**/*
New workspace logs page and navigation; redirected when workspace missing; updated imports to [workspace]/logs/*; type imports adjusted.
Audit: workspace filters and page link
.../[workspace]/audit/components/**/*, .../[workspace]/audit/page.tsx
Updated useFilters and related imports to [workspace]; navigation link uses /${workspace.slug}/audit.
Authorization: navigation, selection, delete handlers
.../[workspace]/authorization/constants.ts, .../permissions/navigation.tsx, .../permissions/page.tsx, .../permissions/components/table/**/*, .../roles/navigation.tsx, .../roles/page.tsx, .../roles/components/table/**/*, .../roles/components/upsert-role/**/*
Added workspace-scoped navigation utility and nav components; guarded rendering by workspace; toggles now use IDs (roleId/permissionId); delete hooks now read data.deletedCount; import paths adjusted to [workspace].
Identities: new feature pages and navigation
.../[workspace]/identities/page.tsx, .../identities/navigation.tsx, .../identities/filter.tsx, .../identities/components/results.tsx, .../identities/row.tsx, .../identities/[identityId]/navigation.tsx, .../identities/[identityId]/page.tsx
Added workspace-scoped identities listing and details; Suspense + TRPC query; URL building uses workspace slug; safer JSON render; keyboard-accessible rows.
API creation/list props
.../[workspace]/apis/_components/create-api-button.tsx, .../[workspace]/apis/_components/api-list-client.tsx, .../[workspace]/apis/page.tsx
CreateApiButton now requires workspaceSlug; routes/revalidate use it; ApiListClient accepts/passes workspaceSlug; conditional rendering by workspace.
Workspace entry redirect
apps/dashboard/app/(app)/[workspace]/page.tsx
New redirect component sending to /${workspace.slug}/apis on load.
Misc import path updates to [workspace]
.../authorization/*/selection-controls/index.tsx, .../keys/*/utils.ts, .../charts/bar-chart/utils.ts, .../controls/index.tsx, and similar
Adjusted module paths to include [workspace]; no behavioral changes.
Settings: workspace-aware actions and minor renames
.../[workspace]/apis/[apiId]/settings/components/{default-bytes.tsx,default-prefix.tsx,delete-api.tsx,settings-client.tsx}
Added useWorkspace; early returns when no workspace; revalidate and navigation paths now workspace-scoped; renamed internal workspaceworkspaceData usage.
Styling-only changes
.../settings/components/{delete-protection.tsx,skeleton.tsx,update-api-name.tsx}
Replaced border-b-1 with border-b; no logic changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Page as Workspace-scoped Page
  participant WS as useWorkspace
  participant Router as Next Router
  participant UI as Navbar/Links

  User->>Page: Navigate to page
  Page->>WS: Get current workspace
  alt Workspace available
    WS-->>Page: { slug }
    Page->>UI: Build hrefs using "/{slug}/..."
    User->>UI: Click link
    UI->>Router: push("/{slug}/apis/...") 
    Router-->>User: Navigate to destination
  else No workspace
    Page->>Router: redirect("/new") or return null
  end
Loading
sequenceDiagram
  autonumber
  participant Client as Delete Hook (Roles/Permissions)
  participant Server as API
  participant Cache as Revalidate/Invalidate
  participant Toast as UI Toast

  Client->>Server: mutate(delete {ids})
  Server-->>Client: { deletedCount }
  Client->>Cache: invalidate queries
  Client->>Toast: show "Deleted {deletedCount} item(s)"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Rbac keys #3321 — Both touch apps/dashboard/app/(app)/apis/[apiId]/api-id-navbar.tsx, altering navbar behavior and scope.
  • feat: logs page v2 #2701 — Both modify logs modules/pages; this PR scopes logs to workspace similarly to the logs v2 changes.
  • feat: onboarding success step #3499 — Both update key creation success flow/components; this PR adjusts routing and imports around that area.

Suggested labels

Feature, Dashboard, :joystick: 300 points

Suggested reviewers

  • chronark
  • mcstepp
  • ogzhanolguncu

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description contains a comprehensive high-level summary and marks the change as a new feature, but the required "How should this be tested?" section is blank ("WILL FILLL SOON") and the Required checklist items remain unchecked, so the description is incomplete for review and verification. Because the repo template mandates explicit testing steps and checklist confirmation, the current description does not provide sufficient information to validate or merge this large, cross-cutting refactor. The claim of migrating 200+ components increases the need for concrete testing and rollout/migration notes. Fill the "How should this be tested?" section with clear, reproducible steps (local setup, commands to run such as build/lint/tests, critical pages to exercise, migration steps, and any required environment flags), run and record pnpm build/pnpm fmt/tests, and then mark the Required checklist items as completed or explain any exceptions; also include any breaking-change notes, a list of high-risk areas to manually verify, and link the related issue(s) before requesting a full review.
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 clearly identifies the primary intent—implementing workspace-based URL routing—so it is related to the changeset, but it is noisy and non-conforming: repeated "WIP" markers, an uppercase "DON'T REVIEW", and extra words make it long and unsuitable for commit history or release notes. The core subject is present, however the title should be concise, single-sentence, and free of draft/status markers.

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 21, 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: 15

Caution

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

⚠️ Outside diff range comments (24)
apps/dashboard/app/(app)/[workspace]/authorization/roles/components/table/components/actions/keys-table-action.popover.constants.tsx (1)

3-6: Static import from the same module defeats the dynamic code‑split.

Importing TableActionPopoverDefaultTrigger statically from the same file you’re dynamically importing (@/components/logs/table-action.popover) pulls that module into the main chunk, negating the split. Net result: larger initial bundle and little gain from dynamic().

Recommended fixes (pick one):

  • Split the trigger into its own lightweight module and import it statically, while keeping the heavy popover in the dynamically imported module.
  • Alternatively, inline a tiny placeholder trigger for the loading fallback so nothing from the popover module is eagerly imported.

Example (illustrative paths):

- import { type MenuItem, TableActionPopoverDefaultTrigger } from "@/components/logs/table-action.popover";
+ import { type MenuItem } from "@/components/logs/table-action.popover";
+ import { TableActionPopoverDefaultTrigger } from "@/components/logs/table-action.popover/trigger";

Also applies to: 20-25

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/components/logs-filters/outcome-filter.tsx (1)

13-15: Missing "use client" — this component uses a hook.

useFilters() indicates a React hook; without the "use client" directive this will fail in the App Router. Add the directive at the very top of the file.

Apply:

+ 'use client';
+
 import {
   getOutcomeColor,
   getOutcomeOptions,
 } from "@/app/(app)/[workspace]/apis/[apiId]/_overview/utils";
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/charts/bar-chart/utils.ts (1)

21-49: Config key mismatch and inclusion semantics: don’t force-add VALID; use 'valid' key, not 'success'.

Currently VALID is always included as key 'success', which can desync with timeseries keys (likely 'valid') and ignores an explicit subset passed via includedOutcomes. Build the config from the requested outcomes and map VALID→'valid' for the key.

Apply:

 export function createOutcomeChartConfig(includedOutcomes?: string[]) {
-  const config: Record<string, { label: string; color: string }> = {
-    success: {
-      label: formatOutcomeName("VALID"),
-      color: cssVarToChartColor("accent-4"),
-    },
-  };
-
-  // Default to all non-valid outcomes if none specified
-  const outcomesToInclude =
-    includedOutcomes ||
-    KEY_VERIFICATION_OUTCOMES.filter((outcome) => outcome !== "VALID" && outcome !== "");
-
-  // Add each outcome as a chart series option
-  outcomesToInclude.forEach((outcome) => {
-    if (outcome === "VALID" || outcome === "") {
-      return; // Skip VALID (already added) and empty string
-    }
-
-    // Convert to the format used in our timeseries data (snake_case)
-    const key = outcome.toLowerCase();
-    const colorClass = OUTCOME_BACKGROUND_COLORS[outcome] || "bg-accent-4";
-
-    config[key] = {
-      label: formatOutcomeName(outcome),
-      color: cssVarToChartColor(colorClass),
-    };
-  });
+  const config: Record<string, { label: string; color: string }> = {};
+
+  // If a subset is provided, honor it; otherwise include all non-empty outcomes
+  const outcomes =
+    (includedOutcomes ?? KEY_VERIFICATION_OUTCOMES).filter((o) => o !== "");
+
+  outcomes.forEach((outcome) => {
+    const key = outcome === "VALID" ? "valid" : outcome.toLowerCase(); // lowercase snake_case
+    const colorClass = OUTCOME_BACKGROUND_COLORS[outcome] ?? "bg-accent-4";
+    config[key] = {
+      label: formatOutcomeName(outcome),
+      color: cssVarToChartColor(colorClass),
+    };
+  });
 
   return config;
 }
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/methods-filter.tsx (1)

10-16: TypeScript compile error: as const conflicts with MethodOption[].

as const makes the array readonly and not assignable to MethodOption[]. This will fail type-checking.

Apply one of these (pick 1):

-] as const;
+];

Or, if you want a readonly literal, change the type accordingly (ensure FilterCheckbox accepts readonly arrays):

-const options: MethodOption[] = [
+const options: ReadonlyArray<MethodOption> = [
   { id: 1, method: "GET", checked: false },
   { id: 2, method: "POST", checked: false },
   { id: 3, method: "PUT", checked: false },
   { id: 4, method: "DELETE", checked: false },
   { id: 5, method: "PATCH", checked: false },
-] as const;
+];
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-ratelimits/utils.ts (1)

5-14: Defensive clone to avoid shared-state mutations

If downstream code mutates ratelimit.data, you’ll be mutating either keyDetails.key.ratelimits.items or the array returned by getDefaultValues(). Clone before returning.

Apply:

-  const defaultRatelimits =
-    keyDetails.key.ratelimits.items.length > 0
-      ? keyDetails.key.ratelimits.items
-      : (getDefaultValues().ratelimit?.data ?? [
-          {
-            name: "Default",
-            limit: 10,
-            refillInterval: 1000,
-          },
-        ]);
+  const sourceRatelimits =
+    keyDetails.key.ratelimits.items.length > 0
+      ? keyDetails.key.ratelimits.items
+      : (getDefaultValues().ratelimit?.data ?? [
+          { name: "Default", limit: 10, refillInterval: 1000 },
+        ]);
+  const defaultRatelimits = sourceRatelimits.map((r) => ({ ...r }));
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-key-name.tsx (2)

92-99: Fix onOpenChange to only act on close.

Current handler fires on any state change and will immediately re-close on open for Radix-style APIs.

-          onOpenChange={() => {
-            saveCurrentValues();
-            onClose();
-          }}
+          onOpenChange={(open) => {
+            if (!open) {
+              saveCurrentValues();
+              onClose();
+            }
+          }}

70-77: Reset form to the new name after successful mutation.

You reset to the pre-update name from props, which can be stale until parent revalidates.

-  const key = useEditKeyName(() => {
-    clearPersistedData();
-    reset({
-      name: keyDetails.name || "",
-      originalName: keyDetails.name || "",
-    });
-    onClose();
-  });
+  const key = useEditKeyName(() => {
+    onClose();
+  });

And update submit to clear + reset with the submitted value:

   const onSubmit = async (data: EditNameFormValues) => {
     try {
-      await key.mutateAsync({ ...data, keyId: keyDetails.id });
+      await key.mutateAsync({ ...data, keyId: keyDetails.id });
+      clearPersistedData();
+      const newName = data.name?.trim() ?? "";
+      reset({ name: newName, originalName: newName });
+      onClose();
     } catch {
       // `useEditKeyName` already shows a toast, but we still need to
       // prevent unhandled‐rejection noise in the console.
     }
   };
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-ratelimits/index.tsx (1)

76-83: Bug: onOpenChange ignores next state and will close on open.

DialogContainer.onOpenChange signature is (value: boolean) => void (internal/ui/src/components/dialog/dialog-container.tsx); the current handler ignores the boolean and unconditionally calls saveCurrentValues()/onClose(), causing the dialog to immediately close when opened. Use a guard to only run on close.

-          onOpenChange={() => {
-            saveCurrentValues();
-            onClose();
-          }}
+          onOpenChange={(open) => {
+            if (!open) {
+              saveCurrentValues();
+              onClose();
+            }
+          }}

Occurrences of the same unguarded pattern were found in other files (examples): edit-expiration, edit-key-name, edit-metadata, edit-credits, edit-ratelimits.

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

8-16: Use the route [workspace] param (or ctx.workspace.id) to resolve workspace — don’t fall back to org defaults

  • apps/dashboard/app/(app)/[workspace]/audit/page.tsx (lines ~9–11) calls getWorkspace(orgId); change the caller to pass the workspace slug/id from the route params (or derive it from ctx).
  • apps/dashboard/app/(app)/[workspace]/audit/actions.ts (getWorkspace(orgId) at ~line 5) must accept a workspace identifier (slug/id) or read ctx.workspace.id and use that when querying — otherwise an org default workspace may be returned and cause cross-workspace leakage.
  • After change, verify LogsClient / members / rootKeys remain scoped to the resolved workspace.
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/paths-filter.tsx (1)

5-6: Add "use client" — this component uses hooks.

This component calls useFilters(); it must be a Client Component.

Apply:

+ "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]/settings/components/settings-client.tsx (1)

36-41: Guard missing workspace in layoutData before destructuring.
If layoutData.workspace is absent, workspaceData.ipWhitelist access below will throw.

Apply this diff:

-  if (!layoutData || !layoutData.keyAuth) {
-    throw new Error("KeyAuth configuration not found");
-  }
+  if (!layoutData?.keyAuth || !layoutData?.workspace) {
+    throw new Error("Missing keyAuth or workspace in API details payload");
+  }
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsx (1)

35-57: Avoid premature “Log Data Unavailable” toast during loading

else if (!log) fires while the request is still in-flight (log undefined), causing spurious error toasts. Gate on a settled state.

Apply:

   useEffect(() => {
-    if (!errorShown && selectedLog) {
-      if (error) {
+    if (!selectedLog) {
+      setErrorShown(false);
+      return;
+    }
+    if (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);
-      } else if (!log) {
+      } else 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);
-      }
-    }
-
-    if (!selectedLog) {
-      setErrorShown(false);
-    }
+      }
     }
   }, [error, log, selectedLog, errorShown]);

If the hook cannot distinguish loading vs. not-found, consider returning isLoading or status and guard on that instead.

apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-metadata/index.tsx (1)

88-91: onOpenChange always closes the dialog (ignores boolean arg).

This will also fire on open events in many dialog libs and can immediately re-close. Act only on close.

Apply:

-          onOpenChange={() => {
-            saveCurrentValues();
-            onClose();
-          }}
+          onOpenChange={(open) => {
+            if (!open) {
+              saveCurrentValues();
+              onClose();
+            }
+          }}
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 hooks and browser APIs.

This file calls trpc.useUtils() and navigator.clipboard; it must be a Client Component.

Apply:

+"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]/authorization/permissions/components/table/permissions-list.tsx (1)

55-66: Make selection toggle accessible (click + keyboard + touch) and prevent double toggles.

Currently the checkbox appears only on hover/selected, which is problematic on touch and for keyboard users. Add click/keyboard handlers on the icon container and stop propagation from the Checkbox to avoid double toggles.

Apply this diff:

           const iconContainer = (
             <div
               className={cn(
                 "size-5 rounded flex items-center justify-center cursor-pointer border border-grayA-3 transition-all duration-100",
                 "bg-grayA-3",
                 isSelected && "bg-grayA-5",
               )}
+              role="button"
+              aria-pressed={isSelected}
+              aria-label={`Select permission ${permission.name}`}
+              tabIndex={0}
+              onClick={(e) => {
+                e.stopPropagation();
+                toggleSelection(permission.permissionId);
+              }}
+              onKeyDown={(e) => {
+                if (e.key === "Enter" || e.key === " ") {
+                  e.preventDefault();
+                  e.stopPropagation();
+                  toggleSelection(permission.permissionId);
+                }
+              }}
-              onMouseEnter={() => setHoveredPermissionName(permission.name)}
-              onMouseLeave={() => setHoveredPermissionName(null)}
+              onMouseEnter={() => setHoveredPermissionId(permission.permissionId)}
+              onMouseLeave={() => setHoveredPermissionId(null)}
             >
               {!isSelected && !isHovered && <Page2 size="sm-regular" className="text-gray-12" />}
               {(isSelected || isHovered) && (
                 <Checkbox
                   checked={isSelected}
                   className="size-4 [&_svg]:size-3"
-                  onCheckedChange={() => toggleSelection(permission.permissionId)}
+                  onCheckedChange={() => toggleSelection(permission.permissionId)}
+                  onClick={(e) => e.stopPropagation()}
+                  onKeyDown={(e) => {
+                    if (e.key === "Enter" || e.key === " ") e.stopPropagation();
+                  }}
                 />
               )}
             </div>
           );

Also applies to: 69-70

apps/dashboard/app/(app)/[workspace]/logs/components/table/log-details/components/log-footer.tsx (1)

84-102: Guard against non-array permissions to avoid runtime crash

content.map assumes an array; if permissions is missing or not an array, this throws. Guard and use a stable key.

Apply:

         {
           label: "Permissions",
-          description: (content) => (
-            <div className="flex flex-wrap gap-2 justify-end">
-              {content.map((permission, index) => (
+          description: (content) => {
+            const permissions = Array.isArray(content) ? content : [];
+            return (
+              <div className="flex flex-wrap gap-2 justify-end">
+                {permissions.map((permission, index) => (
                   <Badge
                     variant="secondary"
                     // biome-ignore lint/suspicious/noArrayIndexKey: its okay to use it as a key
-                    key={index}
+                    key={`${permission}-${index}`}
                     className="px-2 py-1 text-xs font-mono rounded-md"
                   >
                     {permission}
                   </Badge>
-              ))}
-            </div>
-          ),
+                ))}
+              </div>
+            );
+          },
           content: extractResponseField(log, "permissions"),
           tooltipContent: "Copy Permissions",
           tooltipSuccessMessage: "Permissions copied to clipboard",
         },
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/update-api-name.tsx (1)

15-19: Don’t accept client‑supplied workspaceId — update server input first

The TRPC procedure still declares workspaceId in its input; remove it server‑side, then apply the client changes.

  • Server: apps/dashboard/lib/trpc/routers/api/updateName.ts — remove workspaceId from the z.object input (currently around lines 14–17).
  • Client: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/update-api-name.tsx — after the server change, remove workspaceId from formSchema/defaultValues/hidden input and the mutate payload (use api.id and name only; the original client diff is valid once server input is updated).
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-queries/utils.ts (1)

12-23: Status bucketing is inconsistent and misses non-200/400/500 codes.

Use prefix bucketing for 2xx/4xx/5xx and keep consistent with the audit utils.

Apply:

-      case "status":
-        return {
-          value:
-            value === "200" ? "2xx" : value === "400" ? "4xx" : value === "500" ? "5xx" : value,
-          color: value.startsWith("2")
-            ? "bg-success-9"
-            : value.startsWith("4")
-              ? "bg-warning-9"
-              : value.startsWith("5")
-                ? "bg-error-9"
-                : null,
-        };
+      case "status": {
+        const code = String(value);
+        const bucket = /^[245]\d{2}$/.test(code) ? `${code[0]}xx` : code;
+        const color = code.startsWith("2")
+          ? "bg-success-9"
+          : code.startsWith("4")
+            ? "bg-warning-9"
+            : code.startsWith("5")
+              ? "bg-error-9"
+              : null;
+        return { value: bucket, color };
+      }
apps/dashboard/app/(app)/[workspace]/audit/components/controls/components/logs-queries/utils.ts (1)

14-26: Fix status color logic to handle all 2xx/4xx/5xx codes (not just 200/400/500).

Currently 404/500+ won’t colorize correctly; align with logs utils.

Apply:

-      case "status":
-        return {
-          value:
-            value === "200" ? "2xx" : value === "400" ? "4xx" : value === "500" ? "5xx" : value,
-          color:
-            value === "200"
-              ? "bg-success-9"
-              : value === "400"
-                ? "bg-warning-9"
-                : value === "500"
-                  ? "bg-error-9"
-                  : null,
-        };
+      case "status": {
+        const code = String(value);
+        const bucket = /^[245]\d{2}$/.test(code) ? `${code[0]}xx` : code;
+        const color = code.startsWith("2")
+          ? "bg-success-9"
+          : code.startsWith("4")
+            ? "bg-warning-9"
+            : code.startsWith("5")
+              ? "bg-error-9"
+              : null;
+        return { value: bucket, color };
+      }
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-secret-section.tsx (1)

32-39: Don't copy the real secret when the snippet is hidden.

CopyButton always copies the unmasked snippet even when the UI hides it, which can leak the key. Make copy respect visibility (or disable copy until revealed).

Apply this diff:

   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 maskedSnippet = snippet.replace(keyValue, maskedKey);

   return (
     <div className={className}>
       <div className="flex flex-col gap-2 items-start w-full">
@@
         <Code
           className={codeClassName}
           visibleButton={
             <VisibleButton isVisible={showKeyInSnippet} setIsVisible={setShowKeyInSnippet} />
           }
-          copyButton={<CopyButton value={snippet} />}
+          copyButton={<CopyButton value={showKeyInSnippet ? snippet : maskedSnippet} />}
         >
-          {showKeyInSnippet ? snippet : snippet.replace(keyValue, maskedKey)}
+          {showKeyInSnippet ? snippet : maskedSnippet}
         </Code>

Also applies to: 68-71

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

8-19: Avoid rendering a Link with "/undefined/identities"

Gate rendering on a resolved workspace, like other navigation files in this PR.

-export function Navigation() {
-  const { workspace } = useWorkspace();
-
-  return (
-    <Navbar>
-      <Navbar.Breadcrumbs icon={<Fingerprint aria-hidden="true" focusable={false} />}>
-        <Navbar.Breadcrumbs.Link href={`/${workspace?.slug}/identities`} active>
-          Identities
-        </Navbar.Breadcrumbs.Link>
-      </Navbar.Breadcrumbs>
-    </Navbar>
-  );
-}
+export function Navigation() {
+  const { workspace } = useWorkspace();
+  return workspace ? (
+    <Navbar>
+      <Navbar.Breadcrumbs icon={<Fingerprint aria-hidden="true" focusable={false} />}>
+        <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/identities`} active>
+          Identities
+        </Navbar.Breadcrumbs.Link>
+      </Navbar.Breadcrumbs>
+    </Navbar>
+  ) : null;
+}
apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (1)

161-168: Avoid double slashes and invalid links when slug is missing

Using ?? "" can produce //apis/.... Require a slug (or don’t render).

-                      <Link
-                        href={`/${identity.workspace?.slug ?? ""}/apis/${key.keyAuth.api.id}/keys/${key.keyAuth.id}/${key.id}`}
-                      >
+                      <Link
+                        href={`/${identity.workspace!.slug}/apis/${key.keyAuth.api.id}/keys/${key.keyAuth.id}/${key.id}`}
+                      >
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (1)

151-160: Identity details link must include workspace slug

Link in apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (line ~154) is workspace-agnostic and will 404 after the workspace routing change — update to include the workspace slug.

Apply:

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

Scan results: only this file missed the workspace prefix; other identity links already include it:

  • apps/dashboard/app/(app)/[workspace]/identities/row.tsx (returns /${encodedWorkspaceId}/identities/${encodedId}).
  • apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx (uses /${workspace?.slug}/identities/${encodeURIComponent(identityId)}).
apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx (1)

16-26: Disable link until workspace slug is available

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

Guard the linkPath to avoid producing "/undefined/apis/...". Apply this patch:

   const { timeseries, isError } = useFetchVerificationTimeseries(api.keyspaceId);
   const { workspace } = useWorkspace();
+  const linkPath = workspace ? `/${workspace.slug}/apis/${api.id}` : "#";
...
       secondaryId={api.id}
-      linkPath={`/${workspace?.slug}/apis/${api.id}`}
+      linkPath={linkPath}

StatsCard currently has no disabled/noop prop — prefer adding one to StatsCard and using it instead of "#" to avoid focusable non-navigable elements.


import { ConfirmPopover } from "@/components/confirmation-popover";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { useWorkspace } from "@/providers/workspace-provider";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Guard against missing workspace slug to avoid navigating to /undefined/...

router.push builds /${workspace?.slug}/...; if the provider isn't ready, this becomes /undefined/.... Add a fallback from route params and fail gracefully.

Apply this diff:

 import { useEffect, useRef, useState } from "react";
+import { useParams } from "next/navigation";
@@
-  const { workspace } = useWorkspace();
+  const { workspace } = useWorkspace();
+  const params = useParams<{ workspace: string }>();
@@
-          router.push(`/${workspace?.slug}/apis/${apiId}/keys/${keyspaceId}/${keyData.id}`);
+          {
+            const slug = workspace?.slug ?? params?.workspace;
+            if (!slug) {
+              toast.error("Failed to Navigate", {
+                description: "Workspace is not loaded yet. Please try again.",
+              });
+              return;
+            }
+            router.push(
+              `/${encodeURIComponent(slug)}/apis/${encodeURIComponent(apiId)}/keys/${encodeURIComponent(
+                keyspaceId,
+              )}/${encodeURIComponent(keyData.id)}`,
+            );
+          }

Run a quick scan for other potentially unsafe patterns:

Also applies to: 28-28, 94-95


🏁 Script executed:

#!/bin/bash
# Find href/router.push that interpolate workspace?.slug directly
rg -nP --type=tsx --type=ts '/\$\{workspace\?\.\s*slug\}/' -S

Length of output: 27


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf "Repo root: %s\n\n" "$(pwd)"

GLOB='**/*.{ts,tsx,js,jsx,mdx}'
IGNORE=(--glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' --glob '!.next/**' --glob '!**/vendor/**')

echo "Checking specific file from the review..."
FILE='apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx'
if [ -f "$FILE" ]; then
  echo "FOUND: $FILE"
  sed -n '1,240p' "$FILE" || true
else
  echo "NOT FOUND: $FILE"
fi
echo

echo "1) Searching for literal '${workspace?.slug'..."
rg -nF --hidden -S '${workspace?.slug' "${IGNORE[@]}" --glob "$GLOB" || true
echo

echo "2) Searching for literal '${workspace.slug'..."
rg -nF --hidden -S '${workspace.slug' "${IGNORE[@]}" --glob "$GLOB" || true
echo

echo "3) Searching for '/${workspace?.slug' and '/${workspace.slug' (template starts)..."
rg -nF --hidden -S '/${workspace?.slug' "${IGNORE[@]}" --glob "$GLOB" || true
rg -nF --hidden -S '/${workspace.slug' "${IGNORE[@]}" --glob "$GLOB" || true
echo

echo "4) Searching for workspace?.slug (anywhere)..."
rg -nF --hidden -S 'workspace?.slug' "${IGNORE[@]}" --glob "$GLOB" || true
echo

echo "5) Searching for router.push occurrences (showing context)..."
rg -n -C3 --hidden -S 'router.push' "${IGNORE[@]}" --glob "$GLOB" || true
echo

echo "6) Searching for href={`/${ occurrences (common pattern)..."
rg -nF --hidden -S 'href={`/${' "${IGNORE[@]}" --glob "$GLOB" || true
echo

echo "7) Searching for concatenations like '/' + workspace..."
rg -nF --hidden -S "'/' + workspace" "${IGNORE[@]}" --glob "$GLOB" || true
rg -nF --hidden -S '\"/\" + workspace' "${IGNORE[@]}" --glob "$GLOB" || true
echo

echo "Done."

Length of output: 9171


Guard against missing workspace slug when navigating (must fix)

router.push builds /${workspace?.slug}/... which becomes /undefined/... if the provider isn't ready — add a fallback to route params and fail gracefully.

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

 import { useEffect, useRef, useState } from "react";
+import { useParams } from "next/navigation";
@@
-  const { workspace } = useWorkspace();
+  const { workspace } = useWorkspace();
+  const params = useParams<{ workspace: string }>();
@@
-          router.push(`/${workspace?.slug}/apis/${apiId}/keys/${keyspaceId}/${keyData.id}`);
+          {
+            const slug = workspace?.slug ?? params?.workspace;
+            if (!slug) {
+              toast.error("Failed to Navigate", {
+                description: "Workspace is not loaded yet. Please try again.",
+              });
+              return;
+            }
+            router.push(
+              `/${encodeURIComponent(slug)}/apis/${encodeURIComponent(apiId)}/keys/${encodeURIComponent(
+                keyspaceId,
+              )}/${encodeURIComponent(keyData.id)}`,
+            );
+          }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useWorkspace } from "@/providers/workspace-provider";
import { useEffect, useRef, useState } from "react";
import { useParams } from "next/navigation";
import { useWorkspace } from "@/providers/workspace-provider";
const { workspace } = useWorkspace();
const params = useParams<{ workspace: string }>();
{
const slug = workspace?.slug ?? params?.workspace;
if (!slug) {
toast.error("Failed to Navigate", {
description: "Workspace is not loaded yet. Please try again.",
});
return;
}
router.push(
`/${encodeURIComponent(slug)}/apis/${encodeURIComponent(apiId)}/keys/${encodeURIComponent(
keyspaceId,
)}/${encodeURIComponent(keyData.id)}`,
);
}
🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx
around line 5, the router.push call constructs paths using workspace?.slug which
can be undefined when the provider isn't ready; change the navigation to first
resolve a safe slug (e.g. const slug = workspace?.slug ?? params?.workspace ??
router.query?.workspace ?? '') and if slug is empty fail gracefully (return
early, show an error/toast or disable the button) so you never push a path
containing "undefined"; build the path using the safe slug (and
encodeURIComponent) and only call router.push when slug is present.

Comment on lines +62 to +64
router.push(
`/${workspace?.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against undefined workspace slug/key_auth_id to prevent broken URLs.

Optional chaining inside template literals will stringify to "undefined", yielding paths like "/undefined/apis/...". Add a runtime guard and make the link href resilient.

Apply these diffs:

-      router.push(
-        `/${workspace?.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`,
-      );
+      if (!workspace?.slug || !log.key_details?.key_auth_id) {
+        setIsNavigating(false);
+        return;
+      }
+      router.push(
+        `/${workspace.slug}/apis/${apiId}/keys/${log.key_details.key_auth_id}/${log.key_id}`,
+      );
-        href={`/${workspace?.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`}
+        href={
+          workspace?.slug && log.key_details?.key_auth_id
+            ? `/${workspace.slug}/apis/${apiId}/keys/${log.key_details.key_auth_id}/${log.key_id}`
+            : "#"
+        }

Also applies to: 89-89

Comment on lines +70 to +98
// Loading state component
const LoadingNavbar = ({ workspace }: LoadingNavbarProps) => (
<Navbar>
<Navbar.Breadcrumbs icon={<Nodes />}>
<Navbar.Breadcrumbs.Link href={`/${workspace?.slug}/apis`}>APIs</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link href="#" isIdentifier className="group" noop>
<div className="h-6 w-20 bg-grayA-3 rounded animate-pulse transition-all " />
</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link href="#" noop active>
<div className="hover:bg-gray-3 rounded-lg flex items-center gap-1 p-1">
<div className="h-6 w-16 bg-grayA-3 rounded animate-pulse transition-all " />
<ChevronExpandY className="size-4" />
</div>
</Navbar.Breadcrumbs.Link>
</Navbar.Breadcrumbs>
<Navbar.Actions>
<NavbarActionButton disabled>
<Plus />
Create new key
</NavbarActionButton>
<div className="h-7 bg-grayA-2 border border-gray-6 rounded-md animate-pulse px-3 flex gap-2 items-center justify-center w-[190px] transition-all ">
<div className="h-3 w-[190px] bg-grayA-3 rounded" />
<div>
<TaskUnchecked size="md-regular" className="!size-4" />
</div>
</div>
</Navbar.Actions>
</Navbar>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

Avoid generating broken links when workspace slug is missing; centralize href building

Current code can emit /undefined/... during load. Build hrefs via a helper and mark links noop when slug is absent.

+// Workspace-safe href builder
+const wsHref = (slug?: string, path = "") => (slug ? `/${slug}${path}` : "#");

 // Loading state component
 const LoadingNavbar = ({ workspace }: LoadingNavbarProps) => (
   <Navbar>
     <Navbar.Breadcrumbs icon={<Nodes />}>
-      <Navbar.Breadcrumbs.Link href={`/${workspace?.slug}/apis`}>APIs</Navbar.Breadcrumbs.Link>
+      <Navbar.Breadcrumbs.Link
+        href={wsHref(workspace?.slug, "/apis")}
+        noop={!workspace}
+      >
+        APIs
+      </Navbar.Breadcrumbs.Link>
       <Navbar.Breadcrumbs.Link href="#" isIdentifier className="group" noop>
         <div className="h-6 w-20 bg-grayA-3 rounded animate-pulse transition-all " />
       </Navbar.Breadcrumbs.Link>
   const { currentApi } = layoutData;

   // Define base path for API navigation
-  const base = `/${workspace?.slug}/apis/${currentApi.id}`;
+  const slug = workspace?.slug;
+  const base = wsHref(slug, `/apis/${currentApi.id}`);

   // Create navigation items for QuickNavPopover
   const navigationItems = [
     {
       id: "requests",
       label: "Requests",
-      href: `/${workspace?.slug}/apis/${currentApi.id}`,
+      href: wsHref(slug, `/apis/${currentApi.id}`),
     },
   ];

   // 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: wsHref(slug, `/apis/${currentApi.id}/keys/${currentApi.keyAuthId}`),
     });
   }

   // Add Settings navigation
   navigationItems.push({
     id: "settings",
     label: "Settings",
-    href: `/${workspace?.slug}/apis/${currentApi.id}/settings`,
+    href: wsHref(slug, `/apis/${currentApi.id}/settings`),
   });
-          <Navbar.Breadcrumbs.Link
-            href={`/${workspace?.slug}/apis`}
-            className={isMobile ? "hidden" : "max-md:hidden"}
-          >
+          <Navbar.Breadcrumbs.Link
+            href={wsHref(slug, "/apis")}
+            className={isMobile ? "hidden" : "max-md:hidden"}
+            noop={!slug}
+          >
             APIs
           </Navbar.Breadcrumbs.Link>
           <Navbar.Breadcrumbs.Link
             href={base}
             isIdentifier
             className={isMobile ? "hidden" : "group max-md:hidden"}
             noop
           >
             <div className="text-accent-10 group-hover:text-accent-12">{currentApi.name}</div>
           </Navbar.Breadcrumbs.Link>
-          <Navbar.Breadcrumbs.Link href={activePage?.href ?? ""} noop active={!shouldFetchKey}>
+          <Navbar.Breadcrumbs.Link
+            href={activePage?.href ?? wsHref(slug, `/apis/${currentApi.id}`)}
+            noop={!slug}
+            active={!shouldFetchKey}
+          >
             <QuickNavPopover items={navigationItems} shortcutKey="M">
               <div className="hover:bg-gray-3 rounded-lg flex items-center gap-1 p-1">
                 {activePage?.text ?? ""}
                 <ChevronExpandY className="size-4" />
               </div>
             </QuickNavPopover>
           </Navbar.Breadcrumbs.Link>

Also applies to: 119-146, 151-173, 165-172


🏁 Script executed:

#!/bin/bash
# Find potentially unsafe hrefs using `workspace?.slug` that could yield `/undefined/...`
rg -nP --type=ts --type=tsx -C2 'href\s*=\s*{`/\$\{workspace\?\.\s*slug\}[^`]*`}'

Length of output: 27


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the targeted file if present
FILES=$(find . -type f -name 'api-id-navbar.tsx' 2>/dev/null || true)
if [ -n "$FILES" ]; then
  for f in $FILES; do
    echo "=== FILE: $f ==="
    sed -n '1,240p' "$f" || true
  done
else
  echo "No api-id-navbar.tsx found."
fi

echo "=== Search: occurrences of workspace?.slug ==="
rg -n --hidden -S 'workspace\?\.\s*slug' || true

echo "=== Search: template usages with \${workspace?....} ==="
rg -n --hidden -S '\$\{workspace\?\.' || true

echo "=== Search: href template literals (look for backtick hrefs) ==="
rg -n --hidden -S 'href\s*=\s*{`' || true
rg -n --hidden -S 'href\s*=\s*{`[^`]*\$\{workspace' || true

echo "=== Fallback: any '/${workspace?.' occurrences ==="
rg -n --hidden -S '/\$\{workspace\?\.' || true

echo "=== Done ==="

Length of output: 27261


Centralize workspace-safe href builder and mark links noop when slug is missing

Template literals like /${workspace?.slug}/... render as /undefined/... when slug is absent — add a small helper (e.g. const wsHref = (slug?: string, path="") => slug ? \/${slug}${path}` : "#") and set noop={!slug}on Navbar links. Fix LoadingNavbar and NavbarContent in apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx (around lines ~74, 120–152) and sweep other/${workspace?.slug}` occurrences across the repo.

🤖 Prompt for AI Agents
In apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx around
lines 70–98 and 120–152, template literals like `/${workspace?.slug}/...` can
produce `/undefined/...` when slug is absent; add a small helper near the top of
the file (e.g. wsHref = (slug?: string, path = "") => slug ? `/${slug}${path}` :
"#") and use wsHref(workspace?.slug, "/apis") (and other paths) instead of raw
template literals, set noop={!workspace?.slug} (or noop={!slug}) on
Navbar.Breadcrumbs.Link and other Navbar links when slug is missing, and update
LoadingNavbar and NavbarContent uses accordingly; then sweep the repo for other
`/${workspace?.slug}` occurrences and replace with wsHref + noop guards.

@@ -1,4 +1,4 @@
import { nameSchema } from "@/app/(app)/apis/[apiId]/_components/create-key/create-key.schema";
import { nameSchema } from "@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add "use client" directive.

This module uses React hooks and RHF; without the directive, Next.js will error at runtime.

Apply:

+"use client";
+
 import { nameSchema } from "@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { nameSchema } from "@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema";
"use client";
import { nameSchema } from "@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema";
🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-key-name.tsx
around line 1, this module requires the "use client" directive; add a single
line containing "use client" (exactly that string) as the very first line of the
file so Next.js treats the module as a client component and React hooks/RHF work
correctly.

@@ -1,4 +1,4 @@
import { getDefaultValues } from "@/app/(app)/apis/[apiId]/_components/create-key/create-key.utils";
import { getDefaultValues } from "@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.utils";
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

Avoid cross-route imports from app/ segments; move shared util to lib/

Importing from app/(app)/[...] ties this file to a specific route segment and can cause unintended bundling or server/client boundary issues in Next.js. Prefer relocating create-key.utils to a shared lib and importing from there.

Proposed change (after moving the util into lib):

-import { getDefaultValues } from "@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.utils";
+import { getDefaultValues } from "@/lib/create-key/create-key.utils";

Run to find similar cross-route imports to update:


🏁 Script executed:

#!/bin/bash
# Find imports that reach into app/(app)/[workspace]/.../create-key.utils
rg -nP -C2 $'from\\s+[\'"]@/app/\\(app\\)/\\[workspace\\]/apis/\\[apiId\\]/_components/create-key/create-key\\.utils[\'"]'

Length of output: 2785


Avoid cross-route imports from app/ segments — move create-key shared code to /lib/

Importing from app/(app)/[workspace]/... ties these modules to a route segment and risks bundling / server–client boundary issues; move shared create-key code to /lib and update imports.

Affected files:

  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/utils.ts
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-expiration/utils.ts
  • apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-ratelimits/utils.ts
  • apps/dashboard/app/new/hooks/use-key-creation-step.tsx

Proposed change (move util into lib):

-import { getDefaultValues } from "@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.utils";
+import { getDefaultValues } from "@/lib/create-key/create-key.utils";

Also move create-key.create-key.schema into /lib and update its import(s) (edit-credits/utils.ts).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { getDefaultValues } from "@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.utils";
import { getDefaultValues } from "@/lib/create-key/create-key.utils";
🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-ratelimits/utils.ts
(line 1), you are importing getDefaultValues from an app route segment; move the
shared create-key utility out of app/(app)/[workspace]/... into a neutral folder
like lib (e.g., apps/dashboard/lib/create-key/*), export getDefaultValues from
there, then update this file's import to point to the new lib path. Repeat the
same change for the other affected files listed (edit-credits/utils.ts,
edit-expiration/utils.ts, and
apps/dashboard/app/new/hooks/use-key-creation-step.tsx) so they import from the
new lib location; additionally move the create-key.create-key.schema file into
/lib and update any imports (notably edit-credits/utils.ts) to reference the new
lib schema path.

Comment on lines +17 to +27
<Navbar.Breadcrumbs.Link href={`/${workspace?.slug}/identities`}>
Identities
</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link
href={`/${workspace?.slug}/identities/${encodeURIComponent(identityId)}`}
className="w-[200px] truncate"
active
isIdentifier
>
{identityId}
</Navbar.Breadcrumbs.Link>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against “/undefined/…” hrefs when workspace isn’t ready

Using template literals with workspace?.slug produces /undefined/... if the provider hasn’t resolved. Disable links until the slug exists.

-        <Navbar.Breadcrumbs.Link href={`/${workspace?.slug}/identities`}>
+        <Navbar.Breadcrumbs.Link
+          href={workspace ? `/${workspace.slug}/identities` : "#"}
+          noop={!workspace}
+        >
           Identities
         </Navbar.Breadcrumbs.Link>
-        <Navbar.Breadcrumbs.Link
-          href={`/${workspace?.slug}/identities/${encodeURIComponent(identityId)}`}
+        <Navbar.Breadcrumbs.Link
+          href={workspace ? `/${workspace.slug}/identities/${encodeURIComponent(identityId)}` : "#"}
+          noop={!workspace}
           className="w-[200px] truncate"
           active
           isIdentifier
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Navbar.Breadcrumbs.Link href={`/${workspace?.slug}/identities`}>
Identities
</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link
href={`/${workspace?.slug}/identities/${encodeURIComponent(identityId)}`}
className="w-[200px] truncate"
active
isIdentifier
>
{identityId}
</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link
href={workspace ? `/${workspace.slug}/identities` : "#"}
noop={!workspace}
>
Identities
</Navbar.Breadcrumbs.Link>
<Navbar.Breadcrumbs.Link
href={workspace ? `/${workspace.slug}/identities/${encodeURIComponent(identityId)}` : "#"}
noop={!workspace}
className="w-[200px] truncate"
active
isIdentifier
>
{identityId}
</Navbar.Breadcrumbs.Link>
🤖 Prompt for AI Agents
In apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx
around lines 17 to 27, the breadcrumb links use template literals with
workspace?.slug which can produce "/undefined/…" before workspace is loaded;
change to guard on workspace?.slug (and identityId where appropriate) and
disable or avoid rendering the link until slug exists: either render a
non-clickable placeholder (no href, add aria-disabled and tabIndex={-1} or a
disabled prop) when workspace?.slug is falsy, or conditionally render the
Navbar.Breadcrumbs.Link only when workspace?.slug is truthy; ensure
encodeURIComponent(identityId) is only used when identityId is defined and keep
visual text the same.

Comment on lines +144 to +150
(() => {
try {
return JSON.stringify(JSON.parse(key.meta), null, 2);
} catch {
return key.meta;
}
})()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Preserve pretty-printed JSON formatting

Rendering pretty JSON as plain text collapses whitespace. Wrap in

 or Code with whitespace preservation.

-                        (() => {
-                          try {
-                            return JSON.stringify(JSON.parse(key.meta), null, 2);
-                          } catch {
-                            return key.meta;
-                          }
-                        })()
+                        (() => {
+                          try {
+                            const pretty = JSON.stringify(JSON.parse(key.meta), null, 2);
+                            return <pre className="whitespace-pre-wrap">{pretty}</pre>;
+                          } catch {
+                            return <span className="break-all">{key.meta}</span>;
+                          }
+                        })()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(() => {
try {
return JSON.stringify(JSON.parse(key.meta), null, 2);
} catch {
return key.meta;
}
})()
(() => {
try {
const pretty = JSON.stringify(JSON.parse(key.meta), null, 2);
return <pre className="whitespace-pre-wrap">{pretty}</pre>;
} catch {
return <span className="break-all">{key.meta}</span>;
}
})()
🤖 Prompt for AI Agents
In apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx around
lines 144 to 150, the pretty-printed JSON string is being rendered as plain text
which collapses whitespace; change the render so the formatted JSON is wrapped
in an element that preserves whitespace (e.g., a <pre> or a <code> with CSS like
white-space: pre or pre-wrap) so the indentation/newlines are shown, and ensure
any dangerous HTML is not injected (render the string as text content, not raw
HTML).

Comment on lines +33 to +35
const prefetchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const hasPrefetchedRef = useRef(false);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix timeout typing for browser compatibility.

In the browser, setTimeout returns number, not NodeJS.Timeout. Use a portable type.

-  const prefetchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
+  const prefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const prefetchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const hasPrefetchedRef = useRef(false);
const prefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const hasPrefetchedRef = useRef(false);
🤖 Prompt for AI Agents
In apps/dashboard/app/(app)/[workspace]/identities/row.tsx around lines 33 to
35, the timeout ref is typed as NodeJS.Timeout which breaks in browsers; change
the type to a portable return type from setTimeout (e.g.
useRef<ReturnType<typeof setTimeout> | null> or number | null) so it works in
both Node and browser environments, and keep the initial value null.

Comment on lines +9 to +16
export default function Page() {
const router = useRouter();
const { workspace, isLoading } = useWorkspace();

if (!workspace && !isLoading) {
router.push("/new");
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don’t navigate during render — move redirect into an effect.

Calling router.push while rendering can cause warnings and flaky navigation.

-import { useRouter } from "next/navigation";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
@@
-  if (!workspace && !isLoading) {
-    router.push("/new");
-    return null;
-  }
+  useEffect(() => {
+    if (!isLoading && !workspace) {
+      router.replace("/new");
+    }
+  }, [isLoading, workspace, router]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/dashboard/app/(app)/[workspace]/logs/page.tsx around lines 9 to 16, the
component calls router.push during render which can cause warnings and flaky
navigation; move that redirect into a useEffect so navigation happens after
render. Implement a useEffect(() => { if (!workspace && !isLoading) {
router.replace('/new') } }, [workspace, isLoading, router]) (use replace to
avoid extra history entry), and keep the component returning null while loading;
remove router.push from the render path.

Comment on lines +6 to +13
export default function WorkspacePage() {
const router = useRouter();
const { workspace } = useWorkspace();

router.replace(`/${workspace?.slug}/apis`);

return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Don’t navigate during render; wrap replace in useEffect

Calling router.replace in render can cause loops and warnings. Run it in an effect and guard slug.

 import { useRouter } from "next/navigation";
+import { useEffect } from "react";
 
 export default function WorkspacePage() {
   const router = useRouter();
   const { workspace } = useWorkspace();
 
-  router.replace(`/${workspace?.slug}/apis`);
+  useEffect(() => {
+    if (workspace?.slug) {
+      router.replace(`/${workspace.slug}/apis`);
+    }
+  }, [router, workspace?.slug]);
 
   return null;
 }

Search for other instances of navigation in render:


🏁 Script executed:

#!/bin/bash
rg -nP --type=tsx -C2 'router\.(replace|push)\s*\(' apps/dashboard | sed -n '1,200p'

Length of output: 27


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for router.replace/push usages under apps/dashboard..."
rg -nP -C2 "router\.(replace|push)\s*\(" apps/dashboard || true

echo
echo "Searching entire repo for router.replace/push..."
rg -nP -C2 "router\.(replace|push)\s*\(" || true

echo
file="apps/dashboard/app/(app)/[workspace]/page.tsx"
if [ -f "$file" ]; then
  echo "=== $file ==="
  sed -n '1,240p' "$file"
else
  echo "$file not found. Listing apps/dashboard/app files:"
  find apps/dashboard/app -maxdepth 4 -type f -print | sed -n '1,200p' || true
fi

Length of output: 41858


Don’t navigate during render — move router.replace into useEffect and guard slug

Calling router.replace in render can cause loops/warnings. Run it in an effect and only when workspace?.slug is present.

 import { useRouter } from "next/navigation";
+import { useEffect } from "react";
 
 export default function WorkspacePage() {
   const router = useRouter();
   const { workspace } = useWorkspace();
 
-  router.replace(`/${workspace?.slug}/apis`);
+  useEffect(() => {
+    if (workspace?.slug) {
+      router.replace(`/${workspace.slug}/apis`);
+    }
+  }, [router, workspace?.slug]);
 
   return null;
 }
  • Also found similar render-time navigation that should be converted to effects: apps/dashboard/app/(app)/overview/page.tsx, apps/dashboard/app/(app)/[workspace]/settings/page.tsx, apps/dashboard/app/(app)/[workspace]/logs/page.tsx, apps/dashboard/app/(app)/layout.tsx. Fix those the same way.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function WorkspacePage() {
const router = useRouter();
const { workspace } = useWorkspace();
router.replace(`/${workspace?.slug}/apis`);
return null;
}
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function WorkspacePage() {
const router = useRouter();
const { workspace } = useWorkspace();
useEffect(() => {
if (workspace?.slug) {
router.replace(`/${workspace.slug}/apis`);
}
}, [router, workspace?.slug]);
return null;
}
🤖 Prompt for AI Agents
In apps/dashboard/app/(app)/[workspace]/page.tsx around lines 6 to 13,
router.replace is being called during render which can cause navigation loops
and warnings; move the router.replace call into a useEffect and only invoke it
when workspace?.slug is truthy (e.g., useEffect(() => { if (workspace?.slug)
router.replace(`/${workspace.slug}/apis`) }, [router, workspace?.slug])); apply
the same fix to the other files noted:
apps/dashboard/app/(app)/overview/page.tsx,
apps/dashboard/app/(app)/[workspace]/settings/page.tsx,
apps/dashboard/app/(app)/[workspace]/logs/page.tsx, and
apps/dashboard/app/(app)/layout.tsx so all render-time navigations are converted
to guarded useEffect-based navigations.

@vercel vercel bot temporarily deployed to Preview – dashboard September 22, 2025 00:05 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard September 22, 2025 00:28 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard September 22, 2025 00:34 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard September 22, 2025 00:43 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard September 22, 2025 00:53 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard September 22, 2025 00:58 Inactive
@perkinsjr perkinsjr closed this Sep 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant