feat: WIP DON'T REVIEW WIP WIP WIP implement workspace-based URL routing architecture#4008
feat: WIP DON'T REVIEW WIP WIP WIP implement workspace-based URL routing architecture#4008
Conversation
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.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 📝 WalkthroughWalkthroughIntroduces 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
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
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)"
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
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. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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
TableActionPopoverDefaultTriggerstatically 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 fromdynamic().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
loadingfallback 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 constconflicts withMethodOption[].
as constmakes the array readonly and not assignable toMethodOption[]. 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
FilterCheckboxaccepts 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 mutationsIf downstream code mutates
ratelimit.data, you’ll be mutating eitherkeyDetails.key.ratelimits.itemsor the array returned bygetDefaultValues(). 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 missingworkspaceinlayoutDatabefore destructuring.
IflayoutData.workspaceis absent,workspaceData.ipWhitelistaccess 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
isLoadingorstatusand 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()andnavigator.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.mapassumes an array; ifpermissionsis 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 firstThe 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 missingUsing
?? ""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 slugLink 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 availableFile: 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"; |
There was a problem hiding this comment.
🧩 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\}/' -SLength 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.
| 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.
| router.push( | ||
| `/${workspace?.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`, | ||
| ); |
There was a problem hiding this comment.
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
| // 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> | ||
| ); |
There was a problem hiding this comment.
🛠️ 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"; | |||
There was a problem hiding this comment.
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.
| 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"; | |||
There was a problem hiding this comment.
🛠️ 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.
| 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.
| <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> |
There was a problem hiding this comment.
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.
| <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.
| (() => { | ||
| try { | ||
| return JSON.stringify(JSON.parse(key.meta), null, 2); | ||
| } catch { | ||
| return key.meta; | ||
| } | ||
| })() |
There was a problem hiding this comment.
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).
| const prefetchTimeoutRef = useRef<NodeJS.Timeout | null>(null); | ||
| const hasPrefetchedRef = useRef(false); | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| export default function Page() { | ||
| const router = useRouter(); | ||
| const { workspace, isLoading } = useWorkspace(); | ||
|
|
||
| if (!workspace && !isLoading) { | ||
| router.push("/new"); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
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.
| export default function WorkspacePage() { | ||
| const router = useRouter(); | ||
| const { workspace } = useWorkspace(); | ||
|
|
||
| router.replace(`/${workspace?.slug}/apis`); | ||
|
|
||
| return null; | ||
| } |
There was a problem hiding this comment.
🧩 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
fiLength 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.
| 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.
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:
• Workspace Context Integration:
• Data Layer Updates:
• UI/UX Improvements:
• Bug Fixes:
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
How should this be tested?
WILL FILLL SOON
Checklist
Required
pnpm buildpnpm fmtconsole.logsgit pull origin mainAppreciated
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Style