Conversation
This commit introduces a comprehensive refactoring of the dashboard routing system to support workspace-based URLs, enabling multi-workspace functionality with improved navigation patterns and enhanced error handling. ## Core Architecture Changes • **URL Structure Migration**: Migrated from `/apis/[apiId]` to `/[workspace]/apis/[apiId]` URL pattern across all dashboard routes, enabling workspace-scoped navigation • **Component Migration**: Relocated and updated 564 files including components, pages, and utilities to work within the new workspace-scoped file structure: - API management pages and components - Key creation and management flows - Logs viewing and filtering systems - Settings and billing pages - Identity and permission management - Authorization (roles & permissions) system • **Workspace Context Integration**: - Added workspace slug parameter extraction and validation - Implemented workspace-aware navigation and breadcrumbs - Updated all tRPC routes to include workspace context - Added workspace redirect hooks for seamless navigation ## Data Layer & Performance Improvements • **Enhanced Database Integration**: - Modified database queries to be workspace-aware - Updated API endpoints to handle workspace parameters - Enhanced caching strategies for workspace-scoped data - Implemented robust workspace loading with retry mechanisms • **Reliability Enhancements**: - Added intelligent retry logic for workspace loading failures - Implemented exponential backoff delays for API retries - Fixed workspace provider error handling and recovery - Optimized workspace switching performance ## UI/UX & Developer Experience • **Interface Improvements**: - Fixed accessibility issues in navigation components - Improved loading states and error handling across all pages - Enhanced workspace switching experience - Added proper workspace context to all major features • **Code Quality**: - Resolved merge conflicts from main branch integration - Fixed sidebar navigation issues - Corrected various linting and TypeScript errors - Addressed button nesting and HTML structure issues
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds workspace-aware routing and imports across dashboard pages. Introduces a new ApisNavbar, identities pages/components, and a workspace layout with Suspense. Updates many links to prefix workspace slug, adds Suspense/Loading in select components, refines settings invalidation after API deletion, and switches some selection logic to use IDs. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant V as View: ApisNavbar
participant T as TRPC: queryApiKeyDetails
Note over V: Mount with {apiId, keyspaceId, keyId, activePage}
V->>T: fetch layoutData (enabled gating, retry)
alt loading or error
V-->>U: Render LoadingNavbar
else success
V-->>U: Render NavbarContent with items (Requests, Keys?, Settings)
U->>V: Click Create new key
alt keyAuth exists
V-->>U: Open CreateKeyDialog
else
V-->>U: Disabled action
end
end
sequenceDiagram
autonumber
actor U as User
participant S as Settings: DeleteApi
participant M as Mutation: deleteApi
participant H as Helper: invalidateAfterApiDeletion
participant R as Router
U->>S: Confirm delete
S->>M: mutate({ apiId })
M-->>S: onSuccess(data)
S->>H: invalidateAfterApiDeletion()
S->>R: push `/{workspace.slug}/apis`
sequenceDiagram
autonumber
participant P as Page: /[workspaceSlug]/identities
participant Z as Zod: parse searchParams
participant W as useWorkspaceNavigation
participant R as Results (Suspense)
participant Q as TRPC: identities.search
P->>Z: validate {search?, limit?}
P->>W: get workspace (feature flags)
alt identities disabled
P-->>P: Render OptIn
else enabled
P-->>P: Render Navigation, SearchField
P->>R: Suspense mount
R->>Q: fetch({search, limit})
alt success
R-->>P: Render table rows
else error/empty
R-->>P: Show fallback/empty state
end
end
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)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
🧰 Additional context used🧠 Learnings (7)📓 Common learnings📚 Learning: 2025-09-23T17:39:59.820ZApplied to files:
📚 Learning: 2025-08-18T10:28:47.391ZApplied to files:
📚 Learning: 2025-09-22T18:44:56.279ZApplied to files:
📚 Learning: 2025-06-24T13:29:10.129ZApplied to files:
📚 Learning: 2025-08-25T13:46:34.441ZApplied to files:
📚 Learning: 2025-08-25T13:46:08.303ZApplied to files:
🧬 Code graph analysis (2)apps/dashboard/app/(app)/[workspaceSlug]/logs/page.tsx (1)
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-api.tsx (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
🔇 Additional comments (3)
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (20)
apps/dashboard/app/(app)/[workspace]/authorization/permissions/components/table/components/selection-controls/index.tsx (1)
1-6: Add 'use client' directive; hooks and framer-motion require a Client Component.This module uses useState/useRef and framer-motion; without 'use client' it will fail in App Router.
Apply this diff at the top:
+'use client'; + import { AnimatedCounter } from "@/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls"; import { ConfirmPopover } from "@/components/confirmation-popover"; import { Trash, XMark } from "@unkey/icons"; import { Button } from "@unkey/ui"; import { AnimatePresence, motion } from "framer-motion"; import { useRef, useState } from "react";apps/dashboard/app/(app)/[workspace]/logs/components/table/log-details/components/log-footer.tsx (1)
13-15: Outcome: default should be "N/A" and UI currently renders the wrong value.
- Per prior learning, default to "N/A" (not "VALID").
- Bug: you compute contentCopy but render {content}, so null outcomes render empty despite styling based on the fallback.
Apply:
-const DEFAULT_OUTCOME = "VALID"; +const DEFAULT_OUTCOME = "N/A"; - description: (content) => { - let contentCopy = content; - if (contentCopy == null) { - contentCopy = DEFAULT_OUTCOME; - } - return ( - <Badge - className={cn( - { - "text-amber-11 bg-amber-3 hover:bg-amber-3 font-medium": - YELLOW_STATES.includes(contentCopy), - "text-red-11 bg-red-3 hover:bg-red-3 font-medium": - RED_STATES.includes(contentCopy), - }, - "uppercase", - )} - > - {content} - </Badge> - ); - }, - content: extractResponseField(log, "code"), + description: (content) => { + const display = content ?? DEFAULT_OUTCOME; + return ( + <Badge + className={cn( + { + "text-amber-11 bg-amber-3 hover:bg-amber-3 font-medium": + YELLOW_STATES.includes(display), + "text-red-11 bg-red-3 hover:bg-red-3 font-medium": + RED_STATES.includes(display), + }, + "uppercase", + )} + > + {display} + </Badge> + ); + }, + content: extractResponseField(log, "code") ?? DEFAULT_OUTCOME,Note: Change informed by retrieved learning for this component’s Outcome default.
Also applies to: 57-76, 79-82
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/methods-filter.tsx (1)
1-36: Add "use client" — this file uses hooks.This component calls
useFilters(), so it must be a client component in Next.js App Router. Add the directive at the top to avoid RSC/hook runtime errors.+'use client'; + import { useFilters } from "@/app/(app)/[workspace]/logs/hooks/use-filters";apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/index.tsx (1)
1-62: Add "use client" — this file also uses hooks.
useFilters()is used directly; mark this component as client to prevent RSC violations.+'use client'; + import { useFilters } from "@/app/(app)/[workspace]/logs/hooks/use-filters";apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/paths-filter.tsx (1)
1-37: Add "use client" — hooks andcrypto.randomUUID()require client context.This module uses
useFilters()and browsercrypto; mark as client to avoid server-runtime errors.+'use client'; + import { logsFilterFieldConfig } from "@/app/(app)/[workspace]/logs/filters.schema"; import { useFilters } from "@/app/(app)/[workspace]/logs/hooks/use-filters";apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsx (1)
35-57: Spurious error toasts on initial load and stale suppression across selections.
- You toast when
!log, which is true during initial fetch → user sees “Log Data Unavailable” before loading finishes.errorShownresets only whenselectedLogbecomes null, so switching to a new log may suppress valid errors.Gate on requestId, ignore the loading state (undefined), and reset per-requestId.
Apply:
const [errorShown, setErrorShown] = useState(false); - useEffect(() => { - if (!errorShown && selectedLog) { - if (error) { - toast.error("Error Loading Log Details", { - description: `${ - error.message || - "An unexpected error occurred while fetching log data. Please try again." - }`, - }); - setErrorShown(true); - } else if (!log) { - toast.error("Log Data Unavailable", { - description: - "Could not retrieve log information for this key. The log may have been deleted or is still processing.", - }); - setErrorShown(true); - } - } - - if (!selectedLog) { - setErrorShown(false); - } - }, [error, log, selectedLog, errorShown]); + const requestId = selectedLog?.request_id; + + // Reset toast state whenever a new log is selected or the drawer closes + useEffect(() => { + setErrorShown(false); + }, [requestId]); + + useEffect(() => { + if (!requestId || errorShown) return; + if (error) { + toast.error("Error Loading Log Details", { + description: + error.message || + "An unexpected error occurred while fetching log data. Please try again.", + }); + setErrorShown(true); + return; + } + // Only treat explicit null as "unavailable"; undefined is still loading + if (log === null) { + toast.error("Log Data Unavailable", { + description: + "Could not retrieve log information for this key. The log may have been deleted or is still processing.", + }); + setErrorShown(true); + } + }, [requestId, error, log, errorShown]);apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-secret-section.tsx (1)
32-40: Copy button leaks the real secret when the snippet is hidden.
CopyButton always uses the unmaskedsnippet, so users can copy secrets even while the UI shows a masked value.Apply to copy exactly what’s visible:
const snippet = `curl -XPOST '${ process.env.NEXT_PUBLIC_UNKEY_API_URL ?? "https://api.unkey.com" }/v2/keys.verifyKey' \\ -H 'Authorization: Bearer <UNKEY_ROOT_KEY>' \\ -H 'Content-Type: application/json' \\ -d '{ "key": "${keyValue}" }'`; + + const displaySnippet = showKeyInSnippet + ? snippet + : snippet.replace(keyValue, maskedKey); @@ <Code className={codeClassName} visibleButton={ <VisibleButton isVisible={showKeyInSnippet} setIsVisible={setShowKeyInSnippet} /> } - copyButton={<CopyButton value={snippet} />} + copyButton={<CopyButton value={displaySnippet} />} > - {showKeyInSnippet ? snippet : snippet.replace(keyValue, maskedKey)} + {displaySnippet} </Code>Also applies to: 65-71
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/delete-api.tsx (1)
61-63: Prevent double‑submit: await the mutation.
react-hook-form’sisSubmittingflips back to false immediately becauseonSubmitdoesn’t await the mutation, enabling rapid double clicks. UsemutateAsyncand await it.- async function onSubmit(_values: z.infer<typeof formSchema>) { - deleteApi.mutate({ apiId: api.id }); - } + async function onSubmit(_values: z.infer<typeof formSchema>) { + await deleteApi.mutateAsync({ apiId: api.id }); + }apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/keys-table-action.popover.constants.tsx (1)
195-199: Add "use client" — this module uses React hooks and browser APIs.This file calls
trpc.useUtils()and referencesnavigator.clipboardvia handlers, so it must be a client component. Without the directive, Next.js will treat it as a server component and error at build/runtime.+ "use client"; + import { MAX_KEYS_FETCH_LIMIT } from "@/app/(app)/[workspace]/authorization/roles/components/upsert-role/components/assign-key/hooks/use-fetch-keys";apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/components/assign-role/create-key-options.tsx (1)
41-45: Tooltip trigger is a non‑focusable div — keyboard users can’t access tooltipWrap the trigger in a focusable element or add
tabIndex=0(and ideallyaria-describedbylinked to content).- <div className="flex w-full text-accent-8 text-xs gap-4 py-0.5 items-center group flex-row"> + <div tabIndex={0} className="flex w-full text-accent-8 text-xs gap-4 py-0.5 items-center group flex-row">Optional: add
role="button"if semantically appropriate.apps/dashboard/app/(app)/[workspace]/authorization/roles/components/upsert-role/components/assign-key/create-key-options.tsx (1)
34-38: Tooltip trigger needs to be focusable for a11yUse a focusable element or add
tabIndex=0on the div so keyboard users can access the tooltip.- <div className="flex w-full text-accent-8 text-xs gap-4 py-0.5 items-center group flex-row"> + <div tabIndex={0} className="flex w-full text-accent-8 text-xs gap-4 py-0.5 items-center group flex-row">Also applies to: 40-55
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx (1)
83-95: Add slug check before navigating to details.Right now, navigation can render
"/undefined/apis/..."if the workspace is not ready.case "go-to-details": - if (!keyspaceId) { + if (!keyspaceId || !workspace?.slug) { toast.error("Failed to Navigate", { - description: "Keyspace ID is required to view key details.", + description: !workspace?.slug + ? "Workspace is not ready yet. Please try again." + : "Keyspace ID is required to view key details.", action: { label: "Contact Support", onClick: () => window.open("mailto:support@unkey.dev", "_blank"), }, }); return; } - router.push(`/${workspace?.slug}/apis/${apiId}/keys/${keyspaceId}/${keyData.id}`); + router.push(`/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${keyData.id}`); break;apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (1)
48-67: Fix workspace slug nullability, preserve new‑tab behavior, and precompute navigation pathworkspace?.slug is used directly in href/router.push and in the useCallback deps — when workspace is not loaded the link becomes "/undefined/…"; preventing default on every click blocks Cmd/Ctrl/middle‑click. Fix the override-indicator component to guard the slug, precompute detailsPath, allow modified clicks, and use detailsPath/router in the deps.
Location: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx — router.push template (≈lines 62–64), Link href (≈line 89), deps (≈lines 66–67).
Apply:
export const KeyIdentifierColumn = ({ log, apiId, onNavigate }: KeyIdentifierColumnProps) => { const { workspace } = useWorkspace(); const router = useRouter(); const errorPercentage = getErrorPercentage(log); const severity = getErrorSeverity(log); const hasErrors = severity !== "none"; const [isNavigating, setIsNavigating] = useState(false); + // Guard and precompute path + const slug = workspace?.slug; + if (!slug) return null; + const detailsPath = + log.key_details?.key_auth_id + ? `/${slug}/apis/${apiId}/keys/${log.key_details.key_auth_id}/${log.key_id}` + : undefined; const handleLinkClick = useCallback( (e: React.MouseEvent) => { - e.preventDefault(); - setIsNavigating(true); - onNavigate?.(); - - router.push( - `/${workspace?.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`, - ); + // Let modified clicks open new tabs/windows + const isModified = e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0; + if (isModified || !detailsPath) return; + e.preventDefault(); + setIsNavigating(true); + onNavigate?.(); + router.push(detailsPath); }, - [apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router.push, workspace?.slug], + [detailsPath, onNavigate, router], ); ... <Link title={`View details for ${log.key_id}`} className="font-mono group-hover:underline decoration-dotted" - href={`/${workspace?.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`} + href={detailsPath ?? "#"} onClick={handleLinkClick} >Multiple other files use
workspace?.slug(see grep output); apply the same guard/precompute pattern where those hrefs or router.push calls can run before workspace is available.apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (3)
176-183: Guard key-details link against undefined workspace slug.Using workspace?.slug directly can render "/undefined/..." during loading. Disable link until slug exists.
- <Link + <Link title={`View details for ${key.id}`} className="font-mono group-hover:underline decoration-dotted" - href={`/${workspace?.slug}/apis/${apiId}/keys/${keyspaceId}/${key.id}`} - aria-disabled={isNavigating} + href={workspace ? `/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${key.id}` : "#"} + aria-disabled={!workspace || isNavigating} onClick={() => { handleLinkClick(key.id); }} >
151-159: Workspace-scoped identities link is missing slug.Link currently points to /identities/{id}; should include workspace slug and be disabled until available.
- <Link + <Link title={"View details for identity"} className="font-mono group-hover:underline decoration-dotted" - href={`/identities/${key.identity_id}`} + href={ + workspace + ? `/${workspace.slug}/identities/${key.identity_id}` + : "#" + } target="_blank" rel="noopener noreferrer" - aria-disabled={isNavigating} + aria-disabled={!workspace || isNavigating} >
1-399: Fix '/undefined' links from optional workspace.slug interpolationMultiple files construct hrefs like
/${workspace?.slug}/...— whenworkspace?.slugis undefined this renders/undefined/.... Guard the slug or avoid interpolating the optional chain directly in template literals; conditionally render Link or set href only whenworkspace?.slugis present.Key locations (repo-wide — fix all similar occurrences):
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx — href at ~line 180
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx — activePage.href ~line 19
- apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx — linkPath ~line 25
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx — base/nav hrefs ~lines 120–137
- apps/dashboard/components/navigation/sidebar/workspace-navigations.tsx — basePath ~line 43
- apps/dashboard/components/navigation/sidebar/usage-banner.tsx — billing href ~line 45
- apps/dashboard/app/(app)/[workspace]/identities/navigation.tsx — href ~line 13
- apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx — href ~line 14
- apps/dashboard/app/(app)/[workspace]/settings/team/client.tsx — href ~line 93
- apps/dashboard/app/new/components/onboarding-success-step.tsx — router.push at ~line 61
Suggested quick fix pattern:
- href={workspace?.slug ?
/${workspace.slug}/apis/${apiId}: undefined}- or render only when workspace?.slug exists.
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-external-id/index.tsx (2)
49-58: Bug: onOpenChange guard checks prop instead of event arg.Use the
openparameter, notisOpen, or the dialog can close incorrectly when the confirm popover is active.Apply:
- const handleDialogOpenChange = (open: boolean) => { - if (isConfirmPopoverOpen && !isOpen) { + const handleDialogOpenChange = (open: boolean) => { + if (isConfirmPopoverOpen && !open) { // If confirm popover is active don't let this trigger outer popover return; }
60-69: Clear action should also null externalId (consistency + server intent).Batch clear sets both
idandexternalIdtonull; single clear sends onlyid: null. Align behavior to avoid stale external IDs.Apply:
const clearSelection = async () => { setSelectedIdentityId(null); await updateKeyOwner.mutateAsync({ keyIds: keyDetails.id, ownerType: "v2", identity: { - id: null, + id: null, + externalId: null, }, }); };Note: Past learning indicates
externalIdandownerIdare handled separately server-side andownerIdwill be removed; keepingexternalIdexplicit avoids ambiguity.apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls/components/batch-edit-external-id.tsx (1)
45-56: Same onOpenChange bug as single-edit.Check
openarg instead ofisOpen.Apply:
- const handleDialogOpenChange = (open: boolean) => { - if (isConfirmPopoverOpen && !isOpen) { + const handleDialogOpenChange = (open: boolean) => { + if (isConfirmPopoverOpen && !open) { // If confirm popover is active don't let this trigger outer popover return; }apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-expiration/utils.ts (1)
5-13: Treat expiration timestamps as milliseconds (fix places that treat them as seconds).
- Why: schemas and API use UNIX timestamps in milliseconds (see create-key.schema.ts and query-api-keys/schema.ts); the form expects Date objects (expiration.data?: Date). getKeyExpirationDefaults (new Date(keyDetails.expires)) is correct for ms.
- Action: fix code that treats expires as seconds — e.g. apps/.../status-cell/use-key-status.ts currently does (keyData.expires * 1000 - Date.now()) / (...) — remove the 1000 or normalize expires to milliseconds first (use (keyData.expires - Date.now()) / (100060*60) or convert explicitly).
- Check and correct any other usages (normalize or document unit):
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema.ts (expires: number — ms)
- apps/dashboard/lib/trpc/routers/api/keys/query-api-keys/schema.ts (expires: z.number().nullable())
- apps/dashboard/lib/trpc/routers/api/keys/query-api-keys/get-all-keys.ts (serializes expires with getTime())
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/.../edit-expiration/utils.ts (file under review) — OK, but ensure keyDetails.expires is ms before new Date(...)
- Run a repo-wide search for "expires * 1000" / "expires *1000" and for arithmetic using expires to find remaining mismatches.
...p/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx
Outdated
Show resolved
Hide resolved
...[keyAuthId]/_components/components/table/components/actions/components/edit-credits/utils.ts
Outdated
Show resolved
Hide resolved
...eyAuthId]/_components/components/table/components/actions/components/edit-metadata/index.tsx
Outdated
Show resolved
Hide resolved
...)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/charts/bar-chart/utils.ts
Show resolved
Hide resolved
...rd/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/index.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/page.tsx
Show resolved
Hide resolved
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
Outdated
Show resolved
Hide resolved
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (1)
193-202: Guard against undefined workspace before building the key details link.
workspace.slugis dereferenced unconditionally. If the hook returnsnull/undefinedduring loading, this will throw at render time.Apply this diff to make the link safe while preserving UX:
- <Link - title={`View details for ${key.id}`} - className="font-mono group-hover:underline decoration-dotted" - href={`/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${key.id}`} - aria-disabled={isNavigating} - onClick={() => { - handleLinkClick(key.id); - }} - > + <Link + title={`View details for ${key.id}`} + className="font-mono group-hover:underline decoration-dotted" + href={ + workspace + ? `/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${key.id}` + : "#" + } + aria-disabled={isNavigating || !workspace} + onClick={(e) => { + if (!workspace) { + e.preventDefault(); + e.stopPropagation(); + return; + } + handleLinkClick(key.id); + }} + >
🧹 Nitpick comments (23)
apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx (3)
9-16: Handle loading/null workspace to avoid a runtime crashIf
useWorkspaceNavigation()returnsnull/undefinedwhile loading,workspace.slugwill throw. Render a loading state until the workspace is ready.export function Navigation() { const workspace = useWorkspaceNavigation(); + if (!workspace) { + return <Loading />; + } + return ( <Navbar> <Navbar.Breadcrumbs icon={<Layers3 />}> - <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/logs`}> + <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/logs`}> Logs </Navbar.Breadcrumbs.Link> </Navbar.Breadcrumbs> </Navbar> ); }
15-17: URL-encode the slug and mark the current crumb (a11y)Encode the slug to be safe against unexpected characters and optionally mark the active page.
- <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/logs`}> + <Navbar.Breadcrumbs.Link href={`/${encodeURIComponent(workspace.slug)}/logs`} aria-current="page">
7-7: Avoid server‑onlyredirect()in client components
next/navigation’sredirect()is server-only. If you intended to redirect from this client component, useuseRouter().push(...)instead; otherwise remove the import.apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx (3)
28-28: Handle workspace loading/null before usage.Depending on the hook’s lifecycle,
workspacecould be transiently undefined. Add a guard or loading fallback to avoidworkspace.slugaccess before ready.Example:
const workspace = useWorkspaceNavigation(); if (!workspace) { return <Loading />; }
41-43: beforeunload handler won’t reliably prompt without returnValue.Set
e.returnValue = ""for cross‑browser confirmation on tab/window close.Apply this diff:
- const handleBeforeUnload = (e: BeforeUnloadEvent) => { - e.preventDefault(); - }; + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + e.preventDefault(); + // Required in some browsers for the prompt to show + e.returnValue = ""; + };
97-99: Guard workspace presence and encode path segments.
- Add a defensive check for
workspace.slug.- Use
encodeURIComponentfor all dynamic path parts.Apply this diff:
- router.push( - `/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}/${keyData.id}` - ); + if (!workspace?.slug) { + toast.error("Failed to Navigate", { + description: "Workspace context is not ready. Please try again.", + }); + return; + } + router.push( + `/${encodeURIComponent(workspace.slug)}/apis/${encodeURIComponent(apiId)}/keys/${encodeURIComponent(keyspaceId)}/${encodeURIComponent(keyData.id)}` + );Longer-term, consider centralized route builders (e.g., a helper from
useWorkspaceNavigationor apathsutil) to avoid manual string templates.apps/dashboard/app/(app)/[workspace]/apis/page.tsx (1)
6-7: Remove unused import
redirect is unused.-import { useSearchParams, redirect } from "next/navigation"; +import { useSearchParams } from "next/navigation";apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (3)
183-187: Remove redundant cloneElement/class merge.
iconContaineralready hascursor-pointer; cloning to re-append it is unnecessary noise.Apply this diff:
- {React.cloneElement(iconContainer, { - className: cn( - iconContainer.props.className, - "cursor-pointer" - ), - })} + {iconContainer}
291-293: Narrow the dependency to a stable primitive.Depending on the whole
workspaceobject can cause unnecessary recomputes. Useworkspace?.slug.Apply this diff:
- hoveredKeyId, - workspace, + hoveredKeyId, + workspace?.slug,
355-361: WraptotalCountin an element; flex gap doesn’t apply to text nodes.Currently
{totalCount}is a bare text node, so spacing may be off compared to adjacent spans.Apply this diff:
- <div className="flex gap-2"> - <span>Showing</span>{" "} - <span className="text-accent-12">{keys.length}</span> - <span>of</span> - {totalCount} - <span>keys</span> - </div> + <div className="flex gap-2"> + <span>Showing</span> + <span className="text-accent-12">{keys.length}</span> + <span>of</span> + <span className="text-accent-12">{totalCount}</span> + <span>keys</span> + </div>apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (3)
183-185: URL‑encode the workspace slug in hrefAvoids malformed URLs if slugs contain reserved characters.
- <Link - href={`/${identity.workspace.slug}/apis/${key.keyAuth.api.id}/keys/${key.keyAuth.id}/${key.id}`} - > + <Link + href={`/${encodeURIComponent(identity.workspace.slug)}/apis/${key.keyAuth.api.id}/keys/${key.keyAuth.id}/${key.id}`} + >
202-206: Don’t type async server components as React.FCReact.FC implies a sync render; server components can be async. Remove React.FC.
-const LastUsed: React.FC<{ - workspaceId: string; - keySpaceId: string; - keyId: string; -}> = async (props) => { +async function LastUsed(props: { + workspaceId: string; + keySpaceId: string; + keyId: string; +}) {
209-213: Fetch only what you need: limit 1You only read the first row; reduce ClickHouse load.
- limit: 50, + limit: 1,apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx (2)
32-33: Initialize router for client-side navigation.Apply:
- const workspace = useWorkspaceNavigation(); + const workspace = useWorkspaceNavigation(); + const router = useRouter();
98-100: Handle empty input gracefully and improve numeric UX.Avoid coercing empty input to 0; set
undefinedto let zod validation work, and hint numeric keypad on mobile.Apply:
- type="text" - onChange={(e) => - field.onChange(Number(e.target.value.replace(/\D/g, ""))) - } + type="text" + inputMode="numeric" + onChange={(e) => { + const v = e.target.value.replace(/\D/g, ""); + field.onChange(v ? Number(v) : undefined); + }}apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx (3)
9-12: Remove unused imports (Loading,redirect).They’re not used and will fail linting.
Apply:
-import { Loading } from "@unkey/ui"; -import { redirect } from "next/navigation";
209-216: Guard against a transient null workspace before building hrefs.If the workspace hook yields a brief null, using
workspace.slugwill throw. Early-return a minimal skeleton.Apply:
export const ApisNavbar = ({ @@ }: ApisNavbarProps) => { - const workspace = useWorkspaceNavigation(); + const workspace = useWorkspaceNavigation(); + if (!workspace) { + return <Navbar />; + }
127-149: Minor: reusebaseto avoid string duplication.Not required, but reduces risk of path typos.
For example:
- const navigationItems = [ + const navigationItems = [ { id: "requests", label: "Requests", - href: `/${workspace.slug}/apis/${currentApi.id}`, + href: base, }, ]; @@ - navigationItems.push({ + navigationItems.push({ id: "keys", label: "Keys", - href: `/${workspace.slug}/apis/${currentApi.id}/keys/${currentApi.keyAuthId}`, + href: `${base}/keys/${currentApi.keyAuthId}`, }); @@ - navigationItems.push({ + navigationItems.push({ id: "settings", label: "Settings", - href: `/${workspace.slug}/apis/${currentApi.id}/settings`, + href: `${base}/settings`, });apps/dashboard/app/(app)/[workspace]/apis/[apiId]/page.tsx (2)
5-6: Remove unused imports (Loading,redirect).Apply:
-import { Loading } from "@unkey/ui"; -import { redirect } from "next/navigation";
10-20: Guard render until workspace is available.Prevents crashes if the hook is momentarily null before hydration.
Apply:
const workspace = useWorkspaceNavigation(); return ( + !workspace ? null : ( <div className="min-h-screen"> <ApisNavbar apiId={apiId} activePage={{ href: `/${workspace.slug}/apis/${apiId}`, text: "Requests", }} /> <LogsClient apiId={apiId} /> </div> + ) );apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx (2)
3-3: Remove unused imports and keep the workspace hook (don’t switch to params).
Loadingandredirectare unused. Continue using the workspace hook for security; don’t replace with route params.Apply:
-import { Loading } from "@unkey/ui"; -import { redirect } from "next/navigation";Also applies to: 6-7
16-24: Gate rendering untilworkspaceis ready to avoid/undefined/...hrefs or crashes.Keeps security of the hook while preventing transient issues.
Apply:
const workspace = useWorkspaceNavigation(); return ( - <div> + !workspace ? null : ( + <div> <ApisNavbar apiId={apiId} activePage={{ href: `/${workspace.slug}/apis/${apiId}/settings`, text: "Settings", }} /> <SettingsClient apiId={apiId} /> </div> + ) );apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx (1)
14-16: Guard against undefined or non-arrayfiltersto avoid runtime errors.If
datais truthy butfiltersis undefined or not an array,data?.filters.lengthcan throw. Harden the check.Apply:
- if (data?.filters.length === 0 || !data) { + if (!data || !Array.isArray(data.filters) || data.filters.length === 0) {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (26)
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx(6 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx(7 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx(3 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx(2 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx(13 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/page.tsx(2 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx(4 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx(4 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/settings-client.tsx(4 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx(2 hunks)apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx(2 hunks)apps/dashboard/app/(app)/[workspace]/apis/page.tsx(2 hunks)apps/dashboard/app/(app)/[workspace]/authorization/permissions/navigation.tsx(2 hunks)apps/dashboard/app/(app)/[workspace]/authorization/permissions/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx(2 hunks)apps/dashboard/app/(app)/[workspace]/authorization/roles/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx(9 hunks)apps/dashboard/app/(app)/[workspace]/identities/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/identities/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx(2 hunks)apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/logs/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/page.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (13)
- apps/dashboard/app/(app)/[workspace]/logs/page.tsx
- apps/dashboard/app/(app)/[workspace]/authorization/permissions/navigation.tsx
- apps/dashboard/app/(app)/[workspace]/identities/page.tsx
- apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx
- apps/dashboard/app/(app)/[workspace]/authorization/permissions/page.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/page.tsx
- apps/dashboard/app/(app)/[workspace]/authorization/roles/page.tsx
- apps/dashboard/app/(app)/[workspace]/identities/navigation.tsx
- apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/settings-client.tsx
🧰 Additional context used
🧠 Learnings (13)
📓 Common learnings
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.238Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.
Applied to files:
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsxapps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsxapps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx
📚 Learning: 2025-07-28T20:38:53.244Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx:322-341
Timestamp: 2025-07-28T20:38:53.244Z
Learning: In apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx, mcstepp prefers to keep hardcoded endpoint logic in the getDiffType function during POC phases for demonstrating diff functionality, rather than implementing a generic diff algorithm. This follows the pattern of keeping simplified implementations for demonstration purposes.
Applied to files:
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3797
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/control-cloud/index.tsx:1-4
Timestamp: 2025-08-18T10:28:47.391Z
Learning: In Next.js App Router, components that use React hooks don't need their own "use client" directive if they are rendered within a client component that already has the directive. The client boundary propagates to child components.
Applied to files:
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsxapps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsxapps/dashboard/app/(app)/[workspace]/page.tsxapps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsxapps/dashboard/app/(app)/[workspace]/logs/navigation.tsxapps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsxapps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
📚 Learning: 2025-09-22T18:44:56.238Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.238Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.
Applied to files:
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsxapps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsxapps/dashboard/app/(app)/[workspace]/apis/page.tsxapps/dashboard/app/(app)/[workspace]/page.tsxapps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsxapps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.
Applied to files:
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsxapps/dashboard/app/(app)/[workspace]/page.tsx
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.
Applied to files:
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsxapps/dashboard/app/(app)/[workspace]/apis/page.tsx
📚 Learning: 2024-10-04T20:44:38.489Z
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.
Applied to files:
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-04-24T14:34:30.621Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3115
File: apps/dashboard/components/logs/checkbox/filters-popover.tsx:33-55
Timestamp: 2025-04-24T14:34:30.621Z
Learning: In the ShortcutActivator component within filters-popover.tsx, the purpose is to track keys separately for each filter item, providing a registration mechanism for shortcuts passed to it rather than enforcing specific key combinations like option+shift+key.
Applied to files:
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-05-15T16:26:08.666Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3242
File: apps/dashboard/app/(app)/apis/[apiId]/_overview/components/table/components/override-indicator.tsx:50-65
Timestamp: 2025-05-15T16:26:08.666Z
Learning: In the Unkey dashboard, Next.js router (router.push) should be used for client-side navigation instead of window.location.href to preserve client state and enable smoother transitions between pages.
Applied to files:
apps/dashboard/app/(app)/[workspace]/page.tsxapps/dashboard/app/(app)/[workspace]/logs/navigation.tsxapps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx
📚 Learning: 2025-06-24T13:29:10.129Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3401
File: apps/dashboard/app/(app)/logs/filters.query-params.ts:10-0
Timestamp: 2025-06-24T13:29:10.129Z
Learning: The `buildQueryParams` function in `apps/dashboard/app/(app)/logs/filters.query-params.ts` calls `useFilters()` hook inside it, but this is valid because the function is only called from within other React hooks, maintaining the Rules of Hooks compliance.
Applied to files:
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
📚 Learning: 2025-08-25T13:46:08.303Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx:1-1
Timestamp: 2025-08-25T13:46:08.303Z
Learning: The NamespaceListDateTime component in apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx is intentionally designed to use the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than the namespace list hook, as clarified by ogzhanolguncu. This coupling is by design, not an architectural issue.
Applied to files:
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
📚 Learning: 2025-08-25T13:46:34.441Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx:4-4
Timestamp: 2025-08-25T13:46:34.441Z
Learning: The namespace list refresh component (apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx) intentionally uses the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than a namespace-specific hook. This cross-coupling between namespace list components and overview hooks is an architectural design decision.
Applied to files:
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
🔇 Additional comments (33)
apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx (1)
12-20: LGTM: Workspace-aware breadcrumbsGood use of the workspace hook and slug-scoped pathing, consistent with the slug routing migration.
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx (8)
5-5: Good: adopting workspace-aware navigation.Using
useWorkspaceNavigationaligns with slug routing and the security guidance to avoid URL params.
7-8: No action needed on imports.Leaving as-is is fine.
56-58: LGTM: close-attempt handler default action is clear.
91-93: LGTM: support contact action is reasonable.
153-156: LGTM: anchoring the confirm popover via ref is fine.
166-168: LGTM: copy and structure.
175-177: LGTM: showing non-secret key ID is appropriate.
208-209: LGTM: copy tweak reads well.apps/dashboard/app/(app)/[workspace]/apis/page.tsx (4)
4-13: Good switch to the workspace hook; verify loading guaranteesNice move to use useWorkspaceNavigation for security and caching. Ensure it never yields undefined or that you guard UI while loading.
22-25: Guard breadcrumb until workspace is ready to avoid crash/bad URL
If workspace is undefined during initial render, accessing workspace.slug will crash; previously it emitted "/undefined/apis". Render a non-link crumb until ready.- <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/apis`} active> - APIs - </Navbar.Breadcrumbs.Link> + {workspace ? ( + <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/apis`} active> + APIs + </Navbar.Breadcrumbs.Link> + ) : ( + <Navbar.Breadcrumbs.Item active>APIs</Navbar.Breadcrumbs.Item> + )}
27-31: Avoid accessing workspace.slug before ready; drop unnecessary key prop
Key is meaningless outside a list. Also guard until workspace exists.- <CreateApiButton - key="createApi" - defaultOpen={isNewApi} - workspaceSlug={workspace.slug} - /> + {workspace && ( + <CreateApiButton + defaultOpen={isNewApi} + workspaceSlug={workspace.slug} + /> + )}
34-34: Gate ApiListClient by workspace; show a loading fallback
Prevents runtime error and avoids rendering with an invalid slug.- <ApiListClient workspaceSlug={workspace.slug} /> + {workspace ? ( + <ApiListClient workspaceSlug={workspace.slug} /> + ) : ( + <Loading /> + )}apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (7)
6-6: Verify workspace hook uses the auth-validated provider (not URL params).Per retrieved learnings for this repo, the workspace must come from the secure workspace hook and not from URL params. Please confirm
useWorkspaceNavigation()is backed by the authorized workspace context, notuseParams.If it’s not, switch to the secure workspace hook and plumb slug via that. This preserves the access checks and caching guarantees.
30-35: Dynamic import mapping LGTM.Mapping to
mod.KeysTableActionswith an inline loader looks good.
41-49: UI polish LGTM.Button/ICON classes and loading state tweaks are fine.
63-66: Keys query call LGTM.Passing
keyAuthIdintouseKeysListQueryaligns with the route segment.
160-177: Good: identity link is gated behind workspace presence.This avoids constructing URLs from unvalidated params and aligns with the security guidance in retrieved learnings.
226-230: ConfirmHiddenValueCellexpectskey.start.Double-check
KeyDetailsincludesstart(and not e.g.prefix,preview, ormaskedValue) and thatHiddenValueCellrenders it as intended.
411-422: Fallback skeleton for unknown columns LGTM.Good generic guard; protects against layout regressions during column changes.
apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx (1)
14-27: Guard slug to avoid “/undefined”; URL‑encode slug and reuse an encoded base pathPrevents transient
/undefined/identitiesand ensures safe routing.export function Navigation({ identityId }: NavigationProps): JSX.Element { - const workspace = useWorkspaceNavigation(); + const workspace = useWorkspaceNavigation(); + const slug = workspace?.slug; + if (!slug) { + // Render nothing or a skeleton while workspace loads + return null; + } + const base = `/${encodeURIComponent(slug)}/identities`; + const encodedId = encodeURIComponent(identityId); return ( <Navbar> <Navbar.Breadcrumbs icon={<Fingerprint aria-hidden="true" focusable={false} />} > - <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/identities`}> + <Navbar.Breadcrumbs.Link href={base}> Identities </Navbar.Breadcrumbs.Link> <Navbar.Breadcrumbs.Link - href={`/${workspace.slug}/identities/${encodeURIComponent( - identityId - )}`} + href={`${base}/${encodedId}`} className="w-[200px] truncate" active isIdentifier > {identityId} </Navbar.Breadcrumbs.Link>apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (3)
161-172: Render key.meta inside; avoid returning raw objects and preserve formattingPrevents React from receiving a raw object and keeps whitespace.
- (() => { - try { - return JSON.stringify( - JSON.parse(key.meta), - null, - 2 - ); - } catch { - return key.meta; - } - })() + (() => { + if (typeof key.meta === "string") { + try { + return ( + <pre className="whitespace-pre-wrap break-words"> + {JSON.stringify(JSON.parse(key.meta), null, 2)} + </pre> + ); + } catch { + return ( + <pre className="whitespace-pre-wrap break-words">{key.meta}</pre> + ); + } + } + return ( + <pre className="whitespace-pre-wrap break-words"> + {JSON.stringify(key.meta, null, 2)} + </pre> + ); + })()
178-181: Good fix: non‑optional workspaceIdUsing
identity.workspace.idaddresses the earlier nullability concern.
220-225: Ensure time delta arithmetic is numericIf
lastUsedis an ISO string, subtraction yields NaN. Use getTime().- <span className="text-content"> - ({ms(Date.now() - lastUsed)} ago) - </span> + <span className="text-content"> + ({ms(Date.now() - new Date(lastUsed).getTime())} ago) + </span>Please confirm the type returned by
clickhouse.verifications.latest().val?.at(0)?.time. If it’s not a number in ms, the above change is required.apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx (1)
67-68: Revalidate path looks correct with workspace scoping.Looks good assuming the route is
/${workspace.slug}/apis/${apiId}/settings.If you want, I can scan the repo for the canonical path of this page to double-check the revalidate target.
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx (2)
31-66: Workspace-aware guard and revalidate path look solid.Early-return prevents using an undefined slug, and the revalidate path is correctly workspace-scoped.
Consider aligning behavior with DefaultBytes (either both push to
/newor both noop) for consistency. I can update both to a common pattern if desired.
104-106: LGTM on button props and layout tweaks.apps/dashboard/app/(app)/[workspace]/page.tsx (2)
5-11: Blocker: render‑phase redirect and possible "/undefined/apis"; move to useEffect + guard slugCall the redirect in an effect, guard until slug exists, and encode the segment.
Apply this diff within the function:
export default function WorkspacePage() { const router = useRouter(); const workspace = useWorkspaceNavigation(); - router.replace(`/${workspace.slug}/apis`); + useEffect(() => { + if (!workspace?.slug) return; + router.replace(`/${encodeURIComponent(workspace.slug)}/apis`); + }, [router, workspace?.slug]); return null; }Additional changes outside the selected range:
+"use client"; +import { useEffect } from "react"; import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation"; -import { useRouter, redirect } from "next/navigation"; +import { useRouter } from "next/navigation";
7-7: VerifyuseWorkspaceNavigationsources slug from the validated workspace hook (not URL params).Per retrieved learning, the slug must come from the secured workspace hook (useWorkspace) to enforce authorization/caching.
File: apps/dashboard/app/(app)/[workspace]/page.tsx — const workspace = useWorkspaceNavigation();
Confirm useWorkspaceNavigation reads the slug from useWorkspace (not useParams). Implementation was not found in the repository scan; if it uses useParams, replace/refactor to consume the validated workspace context.
apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx (4)
1-1: Good fix: Client component boundary added.Adding "use client" resolves prior feedback and ensures hooks run on the client.
4-8: LGTM: import consolidation and readability tweaks.Import grouping and multiline call improve readability without behavior changes.
Also applies to: 27-30
23-23: LGTM: trailing comma removed in toast options.No functional impact; consistent with formatter rules.
2-2: Workspace-scoped imports confirmed — no stale old imports foundRepository search found no imports from "@/app/(app)/logs/hooks/use-filters". All occurrences use the workspace-scoped path "@/app/(app)/[workspace]/logs/hooks/use-filters" in:
- apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/methods-filter.tsx
- apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/paths-filter.tsx
- apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/components/status-filter.tsx
- apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-filters/index.tsx
- apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx
Outdated
Show resolved
Hide resolved
|
@coderabbitai resolve |
✅ Actions performedComments resolved. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (1)
56-68: Preserve link semantics; don’t blanketpreventDefault()(breaks new‑tab/keyboard access).Respect modifier/middle clicks and only intercept plain left‑clicks. Also guard against missing slug/key_auth_id and encode path segments.
Apply:
- const handleLinkClick = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - setIsNavigating(true); - - onNavigate?.(); - - router.push( - `/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`, - ); - }, - [apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router.push, workspace.slug], - ); + const handleLinkClick = useCallback( + (e: React.MouseEvent<HTMLAnchorElement>) => { + // Let the browser handle new tab/window and middle/modified clicks. + if ( + e.defaultPrevented || + e.button !== 0 || + e.metaKey || + e.ctrlKey || + e.altKey || + e.shiftKey + ) { + return; + } + e.preventDefault(); + if (!workspace?.slug || !log.key_details?.key_auth_id) return; + setIsNavigating(true); + onNavigate?.(); + router.push( + `/${workspace.slug}/apis/${apiId}/keys/${encodeURIComponent( + log.key_details.key_auth_id, + )}/${encodeURIComponent(log.key_id)}` + ); + }, + [apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router, workspace?.slug], + );
🧹 Nitpick comments (22)
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/utils.ts (1)
5-9: Avoid inlinetypeof import(...)+ biome-ignore; export and import theRefilltype insteadThis works and fixes the prior "type-only schema import" issue, but it’s brittle (deep path) and requires a format suppression. Prefer exporting
Refillfrom the schema module and importing it here.Apply in this file:
-// biome-ignore format: the comma after z.infer is incorrect syntax -type Refill = z.infer< - typeof import("@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema").refillSchema ->; +// Refill type comes from the schema moduleAdd this import (outside the selected lines):
+import type { Refill } from "@/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema";And in
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/create-key.schema.ts, export the type:// inside create-key.schema.ts import { z } from "zod"; export const refillSchema = /* existing schema */; export type Refill = z.infer<typeof refillSchema>;apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx (1)
3-8: Optional: Drop Suspense if nothing here suspendsIf no child of Navbar suspends, the Suspense boundary and its import add noise. The early guard already renders Loading.
-import { Suspense } from "react"; +// import { Suspense } from "react"; // not needed if nothing below suspends @@ - return ( - <Suspense fallback={<Loading type="spinner" />}> - <Navbar> + return ( + <Navbar> @@ - </Navbar> - </Suspense> + </Navbar>If other children in this tree do suspend, keep Suspense (or move it to the smallest subtree that actually suspends).
Also applies to: 17-18, 32-33
apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx (5)
24-66: Suspense boundary is ineffective hereThe data-fetching hook runs above the boundary and
isLoadingis hardcodedfalse, so the fallback never shows. Remove the boundary (and related imports) to reduce complexity.- <Suspense fallback={<Loading type="spinner" />}> - <StatsCard + <StatsCard name={api.name} secondaryId={api.id} linkPath={`/${workspace.slug}/apis/${api.id}`} chart={ <StatsTimeseriesBarChart data={timeseries} // INFO: Causing too much lag when there are too many Charts. We'll try to optimize this in the future. isLoading={false} isError={isError} config={{ success: { label: "Valid", color: "hsl(var(--accent-4))", }, error: { label: "Invalid", color: "hsl(var(--orange-9))", }, }} /> } stats={ <> <MetricStats successCount={passed} errorCount={blocked} successLabel="VALID" errorLabel="INVALID" /> <div className="flex items-center gap-2 min-w-0 max-w-[40%]"> <Key className="text-accent-11 flex-shrink-0" /> <div className="text-xs text-accent-9 truncate"> {`${formatNumber(keyCount)} ${keyCount === 1 ? "Key" : "Keys"}`} </div> </div> </> } icon={<ProgressBar className="text-accent-11" />} - /> - </Suspense> + />
9-10: Remove unused imports if Suspense is removedClean up imports after removing the Suspense boundary.
-import { Loading } from "@unkey/ui"; -import { Suspense } from "react";
19-20: Compute success/error in a single passAvoid two reductions over the same array.
- const passed = timeseries?.reduce((acc, crr) => acc + crr.success, 0) ?? 0; - const blocked = timeseries?.reduce((acc, crr) => acc + crr.error, 0) ?? 0; + const { passed, blocked } = + (timeseries ?? []).reduce( + (acc, { success, error }) => { + acc.passed += success; + acc.blocked += error; + return acc; + }, + { passed: 0, blocked: 0 }, + );
33-34: Consider minimal loading state for first paintHardcoding
isLoading={false}removes helpful skeletons. A cheap compromise: only show loading until first data/error.- isLoading={false} + isLoading={!timeseries && !isError}
36-54: Label casing consistency and i18n"Valid/Invalid" vs "VALID/INVALID" are inconsistent. Consider consistent casing and routing labels through i18n.
apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (2)
185-191: Request only the needed row: changelimitfrom 50 to 1.You only use the first element; reduce ClickHouse work and latency.
Apply this diff:
- limit: 50, + limit: 1,
180-206: Avoid N+1 ClickHouse queries; batch by keyIds (and dropReact.FCon async server component).
- Batch last-used lookups for all keys in one query and map by
keyIdto eliminate per-row requests.- Minor: for server components, prefer
const LastUsed = async (...) => {}withoutReact.FCtyping.apps/dashboard/app/(app)/[workspace]/authorization/permissions/navigation.tsx (2)
27-39: Remove the outer Suspense; it’s redundant and hides the entire navbarThe dynamic import already renders a disabled trigger as its loading UI. Wrapping the whole Navbar in Suspense swaps the entire navigation for a spinner, degrading UX without benefit here.
Apply this diff:
- <Suspense fallback={<Loading type="spinner" />}> - <Navbar className="w-full flex justify-between"> + <Navbar className="w-full flex justify-between"> <Navbar.Breadcrumbs icon={<ShieldKey />} className="flex-1 w-full"> <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/authorization/roles`}> Authorization </Navbar.Breadcrumbs.Link> <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/authorization/permissions`} active> Permissions </Navbar.Breadcrumbs.Link> </Navbar.Breadcrumbs> <UpsertPermissionDialog triggerButton /> - </Navbar> - </Suspense> + </Navbar>
6-6: Drop unused import after removing Suspense
Loadingbecomes unused with the above change.Apply this diff:
-import { Loading } from "@unkey/ui";apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (1)
10-10: Remove Suspense wrapper; it’s a no‑op here.
Linkdoes not suspend; the fallback never renders. Simplify and drop the import/wrapper.Apply:
-import { Suspense, useCallback, useState } from "react"; +import { useCallback, useState } from "react";- <Suspense fallback={<Loading type="spinner" />}> - <Link - title={`View details for ${log.key_id}`} - className="font-mono group-hover:underline decoration-dotted" - href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`} - onClick={handleLinkClick} - > - <div className="font-mono font-medium truncate flex items-center"> - {shortenId(log.key_id)} - </div> - </Link> - </Suspense> + {/* Link rendered directly; no suspense needed */} + <Link + title={`View details for ${log.key_id}`} + className="font-mono group-hover:underline decoration-dotted" + href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`} + onClick={handleLinkClick} + > + <div className="font-mono font-medium truncate flex items-center"> + {shortenId(log.key_id)} + </div> + </Link>Also applies to: 87-99
apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx (2)
10-21: Switch to Suspense-based dynamic import; drop ssr:false + loadingThe outer Suspense won’t handle this dynamic import because
suspense: trueisn’t enabled. Prefer Suspense-driven loading and avoid duplicative fallbacks.Apply this diff:
-const UpsertRoleDialog = dynamic( - () => import("./components/upsert-role").then((mod) => mod.UpsertRoleDialog), - { - ssr: false, - loading: () => ( - <NavbarActionButton disabled> - <Plus /> - Create new role - </NavbarActionButton> - ), - }, -); +const UpsertRoleDialog = dynamic( + () => import("./components/upsert-role").then((mod) => mod.UpsertRoleDialog), + { suspense: true }, +);
6-6: Remove unused Loading import after scoping SuspenseIf you adopt the scoped Suspense,
Loadingbecomes unused.-import { Loading } from "@unkey/ui";apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx (5)
212-221: Reorder guards so error handling isn’t shadowed.Currently
!layoutDatais handled beforeerror, making the error branch mostly unreachable.Apply this diff:
- // Show loading state while fetching data - if (isLoading || !layoutData) { + // Show loading state while fetching data + if (isLoading) { return <LoadingNavbar workspace={workspace} />; } - // Handle error state - if (error) { - console.error("Failed to fetch API layout data:", error); - return <LoadingNavbar workspace={workspace} />; - } + // Handle error or missing data + if (error || !layoutData) { + console.error("Failed to fetch API layout data:", error); + return <LoadingNavbar workspace={workspace} />; + }
111-116: Remove noisy console.warn or gate it to dev.This warning triggers every time key params are present and adds noise.
Apply this diff:
- // If we expected to find a key but this component doesn't handle key details, - // we should handle this at a higher level or in a different component - if (shouldFetchKey) { - console.warn("Key fetching logic should be handled at a higher level"); - }
123-146: DRY: reuse base for nav hrefs.Reduces duplication and risk of mismatched paths.
Apply this diff:
const navigationItems = [ { id: "requests", label: "Requests", - href: `/${workspace.slug}/apis/${currentApi.id}`, + href: base, }, ]; // Add Keys navigation if keyAuthId exists if (currentApi.keyAuthId) { navigationItems.push({ id: "keys", label: "Keys", - href: `/${workspace.slug}/apis/${currentApi.id}/keys/${currentApi.keyAuthId}`, + href: `${base}/keys/${currentApi.keyAuthId}`, }); } // Add Settings navigation navigationItems.push({ id: "settings", label: "Settings", - href: `/${workspace.slug}/apis/${currentApi.id}/settings`, + href: `${base}/settings`, });
13-41: Derive ApiLayoutData from tRPC types to avoid drift.Manual mirrors of API shapes tend to rot. Prefer deriving the type from your tRPC router/client exports.
Example (adapt to your trpc exports):
// e.g., if you export RouterOutputs // import type { RouterOutputs } from "@/lib/trpc/types"; type ApiLayoutData = /* RouterOutputs["api"]["queryApiKeyDetails"] */ any; // or, if available, from the client helper: // type ApiLayoutData = NonNullable<Awaited<ReturnType<(typeof trpc.api.queryApiKeyDetails)["fetch"]>>>;
53-56: Narrow workspace prop to Pick<Workspace, "slug">Only workspace.slug is used for URLs—avoid coupling props to the full DB type; change LoadingNavbarProps and NavbarContentProps to workspace: Pick<Workspace, "slug">.
-interface LoadingNavbarProps { - workspace: Workspace; -} +interface LoadingNavbarProps { + workspace: Pick<Workspace, "slug">; +} -interface NavbarContentProps { +interface NavbarContentProps { apiId: string; keyspaceId?: string; keyId?: string; activePage?: { href: string; text: string; }; - workspace: Workspace; + workspace: Pick<Workspace, "slug">; isMobile: boolean; layoutData: ApiLayoutData; }apps/dashboard/app/(app)/[workspace]/apis/[apiId]/page.tsx (2)
14-22: Suspense wrapper is a no-op here; remove it or enable suspense in the query.
ApisNavbarusesuseQuerywithoutsuspense: true, so this Suspense never shows. Simplest: remove Suspense and its imports.Apply this diff:
-import { Loading } from "@unkey/ui"; -import { Suspense } from "react"; +// Suspense not needed unless the child suspends @@ - <Suspense fallback={<Loading type="spinner" />}> - <ApisNavbar - apiId={apiId} - activePage={{ - href: `/${workspace.slug}/apis/${apiId}`, - text: "Requests", - }} - /> - </Suspense> + <ApisNavbar + apiId={apiId} + activePage={{ + href: `/${workspace.slug}/apis/${apiId}`, + text: "Requests", + }} + />Alternatively, keep Suspense and set
suspense: trueon the TRPC query.Also applies to: 4-6
1-1: Consider server component page with a small client child.Making the entire page client-side increases bundle size and foregoes SSR. If only navigation needs client hooks, keep the page server and move hooks into client children.
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/index.tsx (1)
212-221: Gate the Suspense-mounted dialog to avoid page-level spinner/flicker.Without gating, if the dialog suspends on mount, a spinner may render inline on initial load. Mount only when open.
- <Suspense fallback={<Loading type="spinner" />}> - <KeyCreatedSuccessDialog - apiId={apiId} - keyspaceId={keyspaceId} - isOpen={successDialogOpen} - onClose={handleSuccessDialogClose} - keyData={createdKeyData} - onCreateAnother={openNewKeyDialog} - /> - </Suspense> + {successDialogOpen && ( + <Suspense fallback={<Loading type="spinner" />}> + <KeyCreatedSuccessDialog + apiId={apiId} + keyspaceId={keyspaceId} + isOpen={successDialogOpen} + onClose={handleSuccessDialogClose} + keyData={createdKeyData} + onCreateAnother={openNewKeyDialog} + /> + </Suspense> + )}Optional: code-split the dialog to reduce initial bundle.
import dynamic from "next/dynamic"; const KeyCreatedSuccessDialog = dynamic( () => import("./components/key-created-success-dialog").then(m => m.KeyCreatedSuccessDialog), { loading: () => <Loading type="spinner" />, ssr: false }, );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx(3 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/index.tsx(3 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx(4 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx(4 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/utils.ts(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-metadata/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx(5 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx(4 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx(4 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/settings-client.tsx(4 hunks)apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/apis/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/authorization/permissions/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx(3 hunks)apps/dashboard/app/(app)/[workspace]/identities/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/identities/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/logs/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspace]/page.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (18)
- apps/dashboard/app/(app)/[workspace]/identities/navigation.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-bytes.tsx
- apps/dashboard/app/(app)/[workspace]/logs/components/controls/components/logs-search/index.tsx
- apps/dashboard/app/(app)/[workspace]/page.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx
- apps/dashboard/app/(app)/[workspace]/logs/navigation.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx
- apps/dashboard/app/(app)/[workspace]/logs/page.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/default-prefix.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-metadata/index.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/index.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/page.tsx
- apps/dashboard/app/(app)/[workspace]/apis/page.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
- apps/dashboard/app/(app)/[workspace]/identities/page.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/settings/components/settings-client.tsx
- apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/page.tsx
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.
📚 Learning: 2025-09-22T18:44:56.238Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.238Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.
Applied to files:
apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsxapps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx
📚 Learning: 2024-10-08T15:33:04.290Z
Learnt from: AkshayBandi027
PR: unkeyed/unkey#2215
File: apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx:28-29
Timestamp: 2024-10-08T15:33:04.290Z
Learning: In `authorization/roles/[roleId]/update-role.tsx`, the tag `role-${role.id}` is revalidated after updating a role to ensure that the caching mechanism is properly handled for roles.
Applied to files:
apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3797
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/control-cloud/index.tsx:1-4
Timestamp: 2025-08-18T10:28:47.391Z
Learning: In Next.js App Router, components that use React hooks don't need their own "use client" directive if they are rendered within a client component that already has the directive. The client boundary propagates to child components.
Applied to files:
apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx
🔇 Additional comments (23)
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/utils.ts (1)
1-1: LGTM: workspace-scoped import pathThe updated import to the workspace-scoped utils path looks correct.
apps/dashboard/app/(app)/[workspace]/identities/[identityId]/navigation.tsx (1)
13-25: Guard and encode slug to prevent crashes and “/undefined” links
workspace.slugis used without a guard and without encoding. If the slug isn’t ready, this can crash or render invalid hrefs. Precompute an encoded base once and early‑return a loader if missing.export function Navigation({ identityId }: NavigationProps): JSX.Element { const workspace = useWorkspaceNavigation(); + const slug = workspace?.slug; + if (!slug) { + return <Loading type="spinner" />; + } + const base = `/${encodeURIComponent(slug)}/identities`; + const encodedId = encodeURIComponent(identityId); + return ( <Suspense fallback={<Loading type="spinner" />}> <Navbar> <Navbar.Breadcrumbs icon={<Fingerprint aria-hidden="true" focusable={false} />}> - <Navbar.Breadcrumbs.Link href={`/${workspace.slug}/identities`}> + <Navbar.Breadcrumbs.Link href={base}> Identities </Navbar.Breadcrumbs.Link> <Navbar.Breadcrumbs.Link - href={`/${workspace.slug}/identities/${encodeURIComponent(identityId)}`} + href={`${base}/${encodedId}`} className="w-[200px] truncate" active isIdentifier > {identityId} </Navbar.Breadcrumbs.Link>apps/dashboard/app/(app)/[workspace]/apis/_components/api-list-card.tsx (2)
22-22: Defensive guard around api.keysIf
api.keyscan be absent, this will throw. Safe-fallback to 0.- const keyCount = api.keys.reduce((acc, crr) => acc + crr.count, 0); + const keyCount = api.keys?.reduce((acc, crr) => acc + crr.count, 0) ?? 0;Can you confirm
ApiOverview['keys']is always a defined array?
28-28: Verify workspace slug availability and route shapeEnsure
workspace.slugis always defined here and the route pattern matches/{workspace}/apis/{apiId}after slug routing changes.apps/dashboard/app/(app)/[workspace]/identities/[identityId]/page.tsx (6)
19-19: Auth guard vianotFound()import looks good.Import is used correctly to gate unauthorized access.
35-38: Includingworkspace.idandworkspace.slugis correct for downstream usage.Needed for
LastUsedand slug-prefixed links.
156-156: Non-optionalworkspaceIdfix looks correct.Passing
identity.workspace.idsatisfies thestringprop and matches the prior guard.
161-163: Slug‑prefixed link is correct and avoids protocol‑relative//URLs.Matches the new workspace‑scoped routing.
198-200: Verify timestamp units (ms vs s).If
lastUsedis seconds, bothnew Date(lastUsed)andDate.now() - lastUsedwill be wrong. Confirm it’s milliseconds; if seconds, multiply by 1000.Apply this diff if it’s seconds:
- <span className="text-content-subtle">{new Date(lastUsed).toUTCString()}</span> - <span className="text-content">({ms(Date.now() - lastUsed)} ago)</span> + <span className="text-content-subtle">{new Date(lastUsed * 1000).toUTCString()}</span> + <span className="text-content">({ms(Date.now() - lastUsed * 1000)} ago)</span>
144-150: Fix meta rendering: avoid returning objects to React and preserve whitespace.Current IIFE can return a raw object (React crash) and collapses JSON formatting in a table cell. Render inside a
and handle string vs object explicitly.Apply this diff:
- <TableCell className="font-mono text-xs"> - {key.meta ? ( - (() => { - try { - return JSON.stringify(JSON.parse(key.meta), null, 2); - } catch { - return key.meta; - } - })() - ) : ( - <Minus className="text-content-subtle w-4 h-4" /> - )} - </TableCell> + <TableCell className="font-mono text-xs"> + {key.meta ? ( + typeof key.meta === "string" ? ( + (() => { + let text = key.meta; + try { + text = JSON.stringify(JSON.parse(key.meta), null, 2); + } catch {} + return <pre className="whitespace-pre-wrap break-words">{text}</pre>; + })() + ) : ( + <pre className="whitespace-pre-wrap break-words"> + {JSON.stringify(key.meta, null, 2)} + </pre> + ) + ) : ( + <Minus className="text-content-subtle w-4 h-4" /> + )} + </TableCell>apps/dashboard/app/(app)/[workspace]/authorization/permissions/navigation.tsx (3)
23-41: LGTM overallSolid client-side nav with workspace-scoped breadcrumbs and a lazy-loaded dialog trigger. Pattern aligns with the PR’s workspace routing changes.
10-12: UpsertPermissionDialog export verified — dynamic import OK
Named export found at apps/dashboard/app/(app)/[workspace]/authorization/permissions/components/upsert-permission/index.tsx:48.
30-33: Keep breadcrumb pointing to /authorization/roles — no /authorization index foundSearch returned only apps/dashboard/app/(app)/[workspace]/authorization/roles/page.tsx and no apps/dashboard/app/(app)/[workspace]/authorization/page.*. Leave the link as-is; if you want the root crumb to go to /authorization, add an index page at apps/dashboard/app/(app)/[workspace]/authorization/page.tsx and then update the href.
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (1)
2-2: Confirmed: useWorkspaceNavigation uses useWorkspace (no useParams)Verified apps/dashboard/hooks/use-workspace-navigation.tsx imports and uses useWorkspace and contains no useParams references — workspace slug is sourced from the secured workspace context.
apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx (3)
1-5: LGTM: Solid client-side nav compositionClient component + workspace-aware breadcrumbs setup looks good.
37-38: Ensure dialog triggers role cache revalidation on successPer prior learning, revalidate
role-${role.id}after updates to keep lists/breadcrumbs fresh. VerifyUpsertRoleDialogdoes this.
24-25: Confirm workspace.slug is always defined (or guard hrefs)
useWorkspaceNavigation throws while loading and redirects to /new when no workspace (apps/dashboard/hooks/use-workspace-navigation.tsx), so it won’t return null — verify the Workspace returned by the trpc/backend always includes a non-empty slug; if not, guard link creation or fall back to workspace.id.apps/dashboard/app/(app)/[workspace]/apis/[apiId]/api-id-navbar.tsx (3)
70-99: Skeleton loading state reads well and matches the final layout.
174-186: Create key action gating is correct.Correctly disables creation when no keyspace/keyAuth is present.
198-210: Good use of guarded query with retry/backoff.Enabled flag prevents spurious calls; retry settings are sensible defaults.
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/page.tsx (1)
8-25: Overall structure LGTM.Workspace-scoped href and navbar wiring look correct.
apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/index.tsx (2)
20-20: LGTM on addingSuspenseimport.
11-11: ConfirmLoadingexport and prop API.apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_components/create-key/index.tsx imports Loading from @unkey/ui and uses as the Suspense fallback; repo search did not locate the @unkey/ui source or a Loading export — verify that @unkey/ui exports Loading and that the correct prop is type="spinner" (not variant/size).
apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/api-list-card.tsx
Show resolved
Hide resolved
...p)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx
Show resolved
Hide resolved
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx (1)
10-20: Fix workspace hook usage before building slug hrefs.Line 10:
useWorkspaceNavigation()returns an object{ workspace, ... }, not the workspace itself. Assigning it directly makesworkspacethe wrapper object, so Line 19 ends up readingworkspace.slug, which resolves toundefined. That breaks the navbar link (/undefined/apis/...), blocking navigation on this page. Please destructure theworkspaceproperty before using it.- const workspace = useWorkspaceNavigation(); + const { workspace } = useWorkspaceNavigation();
🧹 Nitpick comments (14)
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (2)
151-156: Remove redundant workspace null-check.useWorkspaceNavigation guarantees a workspace; keep only the identity_id guard.
- {key.identity_id && workspace ? ( + {key.identity_id ? ( <Link title={"View details for identity"} className="font-mono group-hover:underline decoration-dotted" - href={`/${workspace.slug}/identities/${key.identity_id}`} + href={`/${workspace.slug}/identities/${key.identity_id}`} target="_blank" rel="noopener noreferrer" aria-disabled={isNavigating} >
271-272: Narrow useMemo deps to workspace.slug.Avoid invalidating columns when the workspace object identity changes.
- workspace, + workspace.slug,apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/create-api-button.tsx (1)
50-56: Parallelize cache invalidation and encode path segment in pushRun invalidation and revalidation concurrently and encode the id in the push URL.
const create = trpc.api.create.useMutation({ async onSuccess(res) { toast.success("Your API has been created"); - await revalidate(`/${workspaceSlug}/apis`); - api.overview.query.invalidate(); - router.push(`/${workspaceSlug}/apis/${res.id}`); + await Promise.all([ + revalidate(`/${workspaceSlug}/apis`), + api.overview.query.invalidate(), + ]); + router.push(`/${workspaceSlug}/apis/${encodeURIComponent(res.id)}`); setIsOpen(false); },apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx (1)
1-3: Avoid client layout; use segment loading.tsx for fallbackMarking the layout as a Client Component forces the entire subtree client-side. Prefer a Server Component layout and a route-segment loading.tsx for the spinner.
Proposed change in this file:
-"use client"; - -import { Loading } from "@unkey/ui"; -import { Suspense } from "react"; +// Server layout; no client imports hereMove the fallback to a new loading.tsx:
// apps/dashboard/app/(app)/[workspaceSlug]/loading.tsx "use client"; import { Loading } from "@unkey/ui"; export default function WorkspaceLoading() { return ( <div className="flex items-center justify-center w-full h-full min-h-[200px]"> <div className="flex flex-col items-center gap-4"> <Loading size={24} /> <p className="text-sm text-gray-600 dark:text-gray-400">Loading workspace...</p> </div> </div> ); }Also applies to: 21-23
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx (2)
67-68: Fix useCallback deps: depend onrouter, notrouter.push
router.pushis a method reference; depend on the router object for stability.- [apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router.push, workspace.slug], + [apiId, log.key_id, log.key_details?.key_auth_id, onNavigate, router, workspace.slug],
87-99: Remove unnecessary Suspense around a plain LinkNo async boundary here; Suspense adds overhead without benefit.
- <Suspense fallback={<Loading type="spinner" />}> - <Link - title={`View details for ${log.key_id}`} - className="font-mono group-hover:underline decoration-dotted" - href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`} - onClick={handleLinkClick} - > - <div className="font-mono font-medium truncate flex items-center"> - {shortenId(log.key_id)} - </div> - </Link> - </Suspense> + <Link + title={`View details for ${log.key_id}`} + className="font-mono group-hover:underline decoration-dotted" + href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`} + onClick={handleLinkClick} + > + <div className="font-mono font-medium truncate flex items-center"> + {shortenId(log.key_id)} + </div> + </Link>apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx (2)
73-76: Encode dynamic URL segmentsEnsure IDs are safely encoded.
- href={`/${workspace.slug}/apis/${apiId}/keys/${log.key_details?.key_auth_id}/${log.key_id}`} + href={`/${workspace.slug}/apis/${encodeURIComponent(apiId)}/keys/${encodeURIComponent( + log.key_details?.key_auth_id ?? "", + )}/${encodeURIComponent(log.key_id)}`}
117-119: Remove Suspense around static contentNo async children; wrap higher-level panels if needed, not this static section.
- <Suspense fallback={<Loading type="spinner" />}> - <LogSection title="Identifiers" details={identifiers} /> - </Suspense> + <LogSection title="Identifiers" details={identifiers} />apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsx (1)
20-27: Encode route params in hrefAvoid accidental path issues if ids contain special characters.
<ApisNavbar apiId={apiId} activePage={{ - href: `/${workspace.slug}/apis/${apiId}/keys/${keyspaceId}`, + href: `/${workspace.slug}/apis/${encodeURIComponent(apiId)}/keys/${encodeURIComponent( + keyspaceId, + )}`, text: "Keys", }} />apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/api-id-navbar.tsx (3)
113-115: Silence dev-only console.warn in productionGuard the warning to avoid noisy logs in production.
- if (shouldFetchKey) { - console.warn("Key fetching logic should be handled at a higher level"); - } + if (shouldFetchKey && process.env.NODE_ENV !== "production") { + console.warn("Key fetching logic should be handled at a higher level"); + }
200-210: Use exponential backoff for retriesAligns with the PR’s reliability goals and avoids thundering-herd retry bursts.
} = trpc.api.queryApiKeyDetails.useQuery( { apiId }, { enabled: Boolean(apiId), // Only run query if apiId exists retry: 3, - retryDelay: 1000, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 8000), }, );
166-169: Default breadcrumb label when activePage is absentAvoid empty text for the breadcrumb trigger.
- <div className="hover:bg-gray-3 rounded-lg flex items-center gap-1 p-1"> - {activePage?.text ?? ""} + <div className="hover:bg-gray-3 rounded-lg flex items-center gap-1 p-1"> + {activePage?.text ?? "Requests"} <ChevronExpandY className="size-4" /> </div>apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/page.tsx (1)
180-206: Avoid typing async components as React.FC.
React.FCdoesn’t model async server components. Prefer a named async function with an explicit props type.Apply this diff:
-const LastUsed: React.FC<{ - workspaceId: string; - keySpaceId: string; - keyId: string; -}> = async (props) => { +type LastUsedProps = { + workspaceId: string; + keySpaceId: string; + keyId: string; +}; + +async function LastUsed(props: LastUsedProps) { @@ -}; +}apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx (1)
33-49: Prefetch is a no-op and timeout type is brittle; prefetch via router and clean up.Currently the timer sets a flag but never prefetches. Also prefer
ReturnType<typeof setTimeout>for client safety and clear timers on unmount.Apply this diff:
- const prefetchTimeoutRef = useRef<NodeJS.Timeout | null>(null); + const prefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); @@ - const handlePrefetch = useCallback(() => { - if (hasPrefetchedRef.current) { - return; - } - - if (prefetchTimeoutRef.current) { - clearTimeout(prefetchTimeoutRef.current); - } - - prefetchTimeoutRef.current = setTimeout(() => { - // Use Link's built-in prefetch behavior instead of manual router.prefetch - hasPrefetchedRef.current = true; - }, 100); // 100ms debounce - }, []); + const handlePrefetch = useCallback(() => { + if (hasPrefetchedRef.current) return; + if (prefetchTimeoutRef.current) clearTimeout(prefetchTimeoutRef.current); + prefetchTimeoutRef.current = setTimeout(() => { + router.prefetch(detailsUrl); + hasPrefetchedRef.current = true; + }, 100); // 100ms debounce + }, [router, detailsUrl]);Additionally, add unmount cleanup and an accessibility role:
+ // Cleanup pending timer on unmount + useEffect(() => { + return () => { + if (prefetchTimeoutRef.current) clearTimeout(prefetchTimeoutRef.current); + }; + }, []); @@ - <TableRow + <TableRow + role="link"And import
useEffect:-import { memo, useCallback, useMemo, useRef } from "react"; +import { memo, useCallback, useMemo, useRef, useEffect } from "react";
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (81)
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/external-id-field/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx(3 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/key-secret-section.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/index.tsx(3 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx(4 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx(4 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/api-id-navbar.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/charts/bar-chart/utils.ts(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/components/logs-filters/outcome-filter.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/controls/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/utils.ts(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-expiration/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-expiration/utils.ts(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-external-id/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-key-name.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-metadata/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-ratelimits/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-ratelimits/utils.ts(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/components/assign-permission/permissions-field.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-rbac/components/assign-role/create-key-options.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/hooks/use-edit-key.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/keys-table-action.popover.constants.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls/components/batch-edit-external-id.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx(5 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsx(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx(4 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsx(4 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-api.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-protection.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/key-settings-form-helper.ts(3 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/settings-client.tsx(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/skeleton.tsx(3 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/update-api-name.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/page.tsx(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/api-list-card.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/api-list-client.tsx(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/_components/create-api-button.tsx(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsx(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/bucket-filter.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/events-filter.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/users-filter.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-queries/utils.ts(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/audit/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/constants.ts(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/components/table/components/actions/components/hooks/use-delete-permission.ts(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/components/table/components/selection-controls/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/components/table/permissions-list.tsx(3 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsx(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/components/hooks/use-delete-role.ts(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/components/hooks/use-fetch-connected-keys-and-perms.ts(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/keys-table-action.popover.constants.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/selection-controls/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/roles-list.tsx(3 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/upsert-role/components/assign-key/create-key-options.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/upsert-role/components/assign-permission/create-permission-options.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/navigation.tsx(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/page.tsx(3 hunks)apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/identities/filter.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-display/components/display-popover.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/methods-filter.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/paths-filter.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/status-filter.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-queries/utils.ts(2 hunks)apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-search/index.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/logs/components/table/log-details/components/log-footer.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/logs/navigation.tsx(1 hunks)apps/dashboard/app/(app)/[workspaceSlug]/logs/page.tsx(1 hunks)
✅ Files skipped from review due to trivial changes (9)
- apps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/events-filter.tsx
- apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-protection.tsx
- apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/update-api-name.tsx
- apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-credits/index.tsx
- apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-display/components/display-popover.tsx
- apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/controls/components/logs-filters/outcome-filter.tsx
- apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/external-id-field/index.tsx
- apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-external-id/index.tsx
- apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/hooks/use-edit-key.tsx
🧰 Additional context used
🧠 Learnings (19)
📓 Common learnings
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.279Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx:88-97
Timestamp: 2025-09-23T17:39:59.820Z
Learning: The useWorkspaceNavigation hook in the Unkey dashboard guarantees that a workspace exists. If no workspace is found, the hook redirects the user to create a new workspace. Users cannot be logged in without a workspace, and new users must create one to continue. Therefore, workspace will never be null when using this hook.
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:46:49.043Z
Learning: In the Unkey dashboard, there is no overview page at /${workspace.slug}/authorization. The roles page at /${workspace.slug}/authorization/roles serves as the default/primary page for the authorization section, so breadcrumb navigation appropriately points both "Authorization" and "Roles" breadcrumbs to the roles page.
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:40:44.944Z
Learning: In the Unkey dashboard authorization section navigation components, the maintainer prefers to wrap entire navbars in Suspense rather than scoping Suspense to individual components, even if it blocks the whole navigation during loading.
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.
📚 Learning: 2025-09-22T18:44:56.279Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx:4-5
Timestamp: 2025-09-22T18:44:56.279Z
Learning: In the Unkey dashboard, the workspace hook (useWorkspace) provides security validation by checking database access and user authorization to the workspace, with 10-minute caching for performance. Using URL params (useParams) for workspace slug would bypass this security validation and allow unauthorized access attempts. Always use the workspace hook for workspace-scoped navigation and handle loading states properly rather than switching to URL parameters.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/override-indicator.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/logs/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3797
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/control-cloud/index.tsx:1-4
Timestamp: 2025-08-18T10:28:47.391Z
Learning: In Next.js App Router, components that use React hooks don't need their own "use client" directive if they are rendered within a client component that already has the directive. The client boundary propagates to child components.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsxapps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/logs/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsxapps/dashboard/app/(app)/[workspaceSlug]/logs/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/identities/row.tsx
📚 Learning: 2025-09-23T17:39:59.820Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/apis/[apiId]/_overview/components/table/components/override-indicator.tsx:88-97
Timestamp: 2025-09-23T17:39:59.820Z
Learning: The useWorkspaceNavigation hook in the Unkey dashboard guarantees that a workspace exists. If no workspace is found, the hook redirects the user to create a new workspace. Users cannot be logged in without a workspace, and new users must create one to continue. Therefore, workspace will never be null when using this hook.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/override-indicator.tsxapps/dashboard/app/(app)/[workspaceSlug]/audit/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsxapps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/logs/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-06-24T13:29:10.129Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3401
File: apps/dashboard/app/(app)/logs/filters.query-params.ts:10-0
Timestamp: 2025-06-24T13:29:10.129Z
Learning: The `buildQueryParams` function in `apps/dashboard/app/(app)/logs/filters.query-params.ts` calls `useFilters()` hook inside it, but this is valid because the function is only called from within other React hooks, maintaining the Rules of Hooks compliance.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/status-filter.tsxapps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-queries/utils.tsapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/key-settings-form-helper.tsapps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-search/index.tsxapps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/methods-filter.tsxapps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-queries/utils.tsapps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/index.tsxapps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/index.tsxapps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/users-filter.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsxapps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/bucket-filter.tsxapps/dashboard/app/(app)/[workspaceSlug]/audit/components/controls/components/logs-filters/components/root-keys-filter.tsxapps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/components/paths-filter.tsx
📚 Learning: 2025-06-19T11:48:05.070Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3324
File: apps/dashboard/app/(app)/authorization/roles/components/table/components/actions/keys-table-action.popover.constants.tsx:17-18
Timestamp: 2025-06-19T11:48:05.070Z
Learning: In the authorization roles refactor, the RoleBasic type uses `roleId` as the property name for the role identifier, not `id`. This is consistent throughout the codebase in apps/dashboard/lib/trpc/routers/authorization/roles/query.ts.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/roles-list.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/components/hooks/use-delete-role.tsapps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/components/hooks/use-fetch-connected-keys-and-perms.ts
📚 Learning: 2024-10-08T15:33:04.290Z
Learnt from: AkshayBandi027
PR: unkeyed/unkey#2215
File: apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx:28-29
Timestamp: 2024-10-08T15:33:04.290Z
Learning: In `authorization/roles/[roleId]/update-role.tsx`, the tag `role-${role.id}` is revalidated after updating a role to ensure that the caching mechanism is properly handled for roles.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/roles-list.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/table/components/actions/components/hooks/use-delete-role.ts
📚 Learning: 2025-09-23T17:46:49.043Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:46:49.043Z
Learning: In the Unkey dashboard, there is no overview page at /${workspace.slug}/authorization. The roles page at /${workspace.slug}/authorization/roles serves as the default/primary page for the authorization section, so breadcrumb navigation appropriately points both "Authorization" and "Roles" breadcrumbs to the roles page.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/constants.tsapps/dashboard/app/(app)/[workspaceSlug]/audit/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/components/upsert-role/components/assign-key/create-key-options.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/roles/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/page.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-08-25T13:46:34.441Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx:4-4
Timestamp: 2025-08-25T13:46:34.441Z
Learning: The namespace list refresh component (apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx) intentionally uses the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than a namespace-specific hook. This cross-coupling between namespace list components and overview hooks is an architectural design decision.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-search/index.tsxapps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-filters/index.tsx
📚 Learning: 2025-08-25T13:46:08.303Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx:1-1
Timestamp: 2025-08-25T13:46:08.303Z
Learning: The NamespaceListDateTime component in apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx is intentionally designed to use the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than the namespace list hook, as clarified by ogzhanolguncu. This coupling is by design, not an architectural issue.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/logs/components/controls/components/logs-search/index.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/controls/index.tsx
📚 Learning: 2025-09-23T17:40:44.944Z
Learnt from: perkinsjr
PR: unkeyed/unkey#4009
File: apps/dashboard/app/(app)/[workspace]/authorization/roles/navigation.tsx:26-40
Timestamp: 2025-09-23T17:40:44.944Z
Learning: In the Unkey dashboard authorization section navigation components, the maintainer prefers to wrap entire navbars in Suspense rather than scoping Suspense to individual components, even if it blocks the whole navigation during loading.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_components/create-key/index.tsxapps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/navigation.tsxapps/dashboard/app/(app)/[workspaceSlug]/layout.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/api-id-navbar.tsx
📚 Learning: 2024-10-20T07:05:55.471Z
Learnt from: chronark
PR: unkeyed/unkey#2294
File: apps/api/src/pkg/keys/service.ts:268-271
Timestamp: 2024-10-20T07:05:55.471Z
Learning: In `apps/api/src/pkg/keys/service.ts`, `ratelimitAsync` is a table relation, not a column selection. When querying, ensure that table relations are included appropriately, not as columns.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/components/edit-ratelimits/index.tsx
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx
📚 Learning: 2024-12-03T14:23:07.189Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2143
File: apps/dashboard/app/(app)/logs/components/log-details/resizable-panel.tsx:37-49
Timestamp: 2024-12-03T14:23:07.189Z
Learning: In `apps/dashboard/app/(app)/logs/components/log-details/resizable-panel.tsx`, the resize handler is already debounced.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsxapps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/_overview/components/table/components/log-details/index.tsx
📚 Learning: 2024-12-03T14:17:08.016Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2143
File: apps/dashboard/app/(app)/logs/logs-page.tsx:77-83
Timestamp: 2024-12-03T14:17:08.016Z
Learning: The `<LogsTable />` component already implements virtualization to handle large datasets efficiently.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/components/log-details/index.tsx
📚 Learning: 2024-11-29T15:15:47.308Z
Learnt from: chronark
PR: unkeyed/unkey#2693
File: apps/api/src/routes/v1_keys_updateKey.ts:350-368
Timestamp: 2024-11-29T15:15:47.308Z
Learning: In `apps/api/src/routes/v1_keys_updateKey.ts`, the code intentionally handles `externalId` and `ownerId` separately for clarity. The `ownerId` field will be removed in the future, simplifying the code.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls/components/batch-edit-external-id.tsx
📚 Learning: 2025-06-19T13:01:55.338Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3315
File: apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/general-setup.tsx:40-50
Timestamp: 2025-06-19T13:01:55.338Z
Learning: In the create-key form's GeneralSetup component, the Controller is intentionally bound to "identityId" as the primary field while "externalId" is set explicitly via setValue. The ExternalIdField component has been designed to handle this pattern where it receives identityId as its value prop but manages both identityId and externalId through its onChange callback.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls/components/batch-edit-external-id.tsx
📚 Learning: 2024-10-04T20:44:38.489Z
Learnt from: chronark
PR: unkeyed/unkey#2180
File: apps/dashboard/lib/constants/workspace-navigations.tsx:56-118
Timestamp: 2024-10-04T20:44:38.489Z
Learning: When typing the `workspace` parameter in functions like `createWorkspaceNavigation`, prefer importing the `Workspace` type from the database module and picking the necessary keys (e.g., `features`) instead of redefining the interface.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.
Applied to files:
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
🧬 Code graph analysis (3)
apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/navigation.tsx (1)
apps/dashboard/app/(app)/identities/[identityId]/navigation.tsx (1)
Navigation(10-26)
apps/dashboard/app/(app)/[workspaceSlug]/identities/navigation.tsx (2)
apps/dashboard/app/(app)/identities/navigation.tsx (1)
Navigation(6-16)apps/dashboard/app/(app)/identities/[identityId]/navigation.tsx (1)
Navigation(10-26)
apps/dashboard/app/(app)/[workspaceSlug]/identities/page.tsx (1)
apps/dashboard/app/(app)/identities/page.tsx (1)
Page(26-66)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (javascript-typescript)
apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-api.tsx
Show resolved
Hide resolved
apps/dashboard/app/(app)/[workspaceSlug]/identities/[identityId]/page.tsx
Show resolved
Hide resolved
apps/dashboard/app/(app)/[workspaceSlug]/identities/components/results.tsx
Show resolved
Hide resolved
Graphite Automations"Post a GIF when PR approved" took an action on this PR • (09/26/25)1 gif was posted to this PR based on Andreas Thomas's automation. |

What does this PR do?
Core Architecture Changes
• URL Structure Migration: URL pattern across all dashboard routes, enabling workspace-scoped navigation
• Component Migration: Relocated and updated 564 files including components, pages,
and utilities to work within the new workspace-scoped file structure:
• Workspace Context Integration:
Performance Improvements
• Reliability Enhancements:
UI/UX & Developer Experience
• Interface Improvements:
• Code Quality:
Fixes # (issue)
If there is not an issue for this, please create one first. This is used to tracking purposes and also helps use understand why this PR exists
Type of change
How should this be tested?
Test 1 (Regular User):
- Login as a user who has a workspace, select the workspace, navigate around and interact with all pieces of the dashboard making sure nothing breaks.
Test 2 (new User):
- Sign up for an account, make sure you get navigated to
/newand that the dashboard functions after the fact.Test 3 (Dumb Developer):
Checklist
Required
pnpm buildpnpm fmtconsole.logsgit pull origin mainAppreciated