feat: premium API keys page redesign#290
Conversation
Visual overhaul to Stripe/Vercel quality. All functionality preserved. - Table layout with column headers (Name | Key | Tier | Last used | Status) - Skeleton loading state replacing spinner - Hover-reveal revoke button (reduces visual noise) - One-time key reveal modal with amber security warning and usage hint - Inline copy button with 2s checkmark state - Color-coded tier badges (free/pro/enterprise) - Security callout with Shield icon - Footer summary showing active count and remaining slots - AlertDialog for revoke confirmation (was plain Dialog) - Matches UsagePage header pattern exactly
|
@DevanshuNEU is attempting to deploy a commit to the Dev's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughIntroduces a new API Keys management page component providing a complete interface for users to view, generate, and revoke API keys. The implementation includes data fetching from a /keys endpoint, modals for key generation and display, confirmation dialogs for revocation, clipboard support, and comprehensive error handling with loading states. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/pages/APIKeysPage.tsx`:
- Around line 177-183: The inline Copy button currently copies the masked
apiKey.key_preview (from the list API) which is not a usable credential; update
the UI in APIKeysPage so the row-level copy either is removed or is explicitly
relabeled as a preview copy. Concretely, remove or conditionally replace the
<CopyButton value={apiKey.key_preview} /> next to apiKey.key_preview with a
non-misleading control (e.g., a disabled copy or a button labeled "Copy Preview"
that clearly indicates it's a preview-only value) and keep the existing
isRevoked guard; adjust any CopyButton props or aria-labels so consumers see
"Preview" instead of implying a real key is copied.
- Around line 163-167: The grid uses fixed pixel column widths which get clipped
due to the parent using overflow-hidden; update the container and column defs in
the div where className is built in APIKeysPage.tsx (the grid div around the API
key rows, referenced alongside isRevoked) to allow narrow viewports by (1)
making the first column shrink correctly with min-w-0 (add the utility/class
that sets min-width:0 on the grid item or container) and (2) replace the rigid
grid-cols-[1fr_200px_80px_110px_120px] with flexible minmax values (e.g., use
minmax(...) or relative widths so right-hand columns can overflow) and ensure
the row wrapper allows horizontal scrolling on small screens by switching
overflow-hidden to overflow-x-auto (apply this change to both occurrences noted
around lines 163-167 and 456-464).
- Around line 295-320: Add a "generation in flight" guard and disable the input
while generating: introduce a boolean state (e.g., isGenerating) used by the
existing button disable logic, update handleSubmit to return early if
isGenerating, call setIsGenerating(true) before invoking onGenerate and
setIsGenerating(false) when the generate promise resolves/rejects; also pass
isGenerating to the Input (disabled prop) and change the onKeyDown handler to
ignore Enter when isGenerating so pressing Enter cannot submit while a request
is in flight. Ensure you reference handleSubmit, onGenerate, and the
Input/id="key-name" element when making these changes.
- Around line 205-215: The revoke button is hidden from keyboard and touch users
due to "opacity-0 group-hover:opacity-100"; update the button styling around the
button component (the <button> using onRevoke, revoking, className, cn and
rendering <Trash2>) to ensure it's visible to focus and coarse-pointer devices
by adding focus-visible and focus-within visibility classes and a non-hover path
for small/touch layouts (e.g., add focus-visible:opacity-100
focus-within:opacity-100 and a small-screen or coarse-pointer alternative such
as sm:opacity-100 or a media-query based class) so keyboard and touch users can
always see and activate the revoke control.
- Around line 348-352: The query key for useQuery in APIKeysPage is global
('api-keys') causing cross-user cache leakage; update the queryKey used in
useQuery (the call that currently passes ['api-keys'] and calls
fetchKeys(token)) to include the current user's identity (e.g., session.user.id
from useAuth) so caches are partitioned per user, and adjust the enabled
condition to require the user id (e.g., enabled when token and session.user.id
are truthy) so the query only runs for the correct user.
- Around line 227-228: Make the one-time secret reveal dialog fully
non-dismissible except via the explicit "Done" button: prevent Escape and
outside-click closing on the Dialog that currently renders Dialog and
DialogContent (open onOpenChange={onClose}) by adding handlers to swallow
onEscapeKeyDown and onInteractOutside (or the framework-equivalent props) so
they call event.preventDefault()/event.stopPropagation() when in reveal mode,
and also hide or disable the X/close button rendered inside DialogContent during
the reveal (render it conditionally or set disabled when the one-time secret
reveal state is active). Apply the same change to the other reveal dialog
instance at the second location (lines ~273-279) so both dialogs are only
dismissible via the Done button.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 07de3dac-a474-4c30-b87f-99584bae4b3d
📒 Files selected for processing (1)
frontend/src/pages/APIKeysPage.tsx
| <div className={cn( | ||
| 'group grid items-center gap-4 px-4 py-3.5 border-b border-border last:border-0 transition-colors', | ||
| 'grid-cols-[1fr_200px_80px_110px_120px]', | ||
| isRevoked ? 'opacity-50' : 'hover:bg-muted/20', | ||
| )}> |
There was a problem hiding this comment.
Allow the new grid table to fit narrow viewports.
These fixed-width columns need roughly 600px before content, but the wrapper also uses overflow-hidden. On phones or split panes, the right-hand columns get clipped instead of scrollable.
Suggested fix
<div className={cn(
- 'group grid items-center gap-4 px-4 py-3.5 border-b border-border last:border-0 transition-colors',
+ 'group min-w-[640px] grid items-center gap-4 px-4 py-3.5 border-b border-border last:border-0 transition-colors',
'grid-cols-[1fr_200px_80px_110px_120px]',
isRevoked ? 'opacity-50' : 'hover:bg-muted/20',
)}>- <div className="rounded-lg border border-border overflow-hidden">
+ <div className="rounded-lg border border-border overflow-x-auto">
{/* Column headers */}
- <div className="grid gap-4 px-4 py-2.5 bg-muted/40 border-b border-border grid-cols-[1fr_200px_80px_110px_120px]">
+ <div className="min-w-[640px] grid gap-4 px-4 py-2.5 bg-muted/40 border-b border-border grid-cols-[1fr_200px_80px_110px_120px]">Also applies to: 456-464
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/APIKeysPage.tsx` around lines 163 - 167, The grid uses
fixed pixel column widths which get clipped due to the parent using
overflow-hidden; update the container and column defs in the div where className
is built in APIKeysPage.tsx (the grid div around the API key rows, referenced
alongside isRevoked) to allow narrow viewports by (1) making the first column
shrink correctly with min-w-0 (add the utility/class that sets min-width:0 on
the grid item or container) and (2) replace the rigid
grid-cols-[1fr_200px_80px_110px_120px] with flexible minmax values (e.g., use
minmax(...) or relative widths so right-hand columns can overflow) and ensure
the row wrapper allows horizontal scrolling on small screens by switching
overflow-hidden to overflow-x-auto (apply this change to both occurrences noted
around lines 163-167 and 456-464).
| {/* Key preview + inline copy */} | ||
| <div className="flex items-center gap-2 min-w-0"> | ||
| <code className="text-xs font-mono text-muted-foreground truncate flex-1"> | ||
| {apiKey.key_preview} | ||
| </code> | ||
| {!isRevoked && <CopyButton value={apiKey.key_preview} />} | ||
| </div> |
There was a problem hiding this comment.
The row-level copy action currently copies an unusable value.
The list API (backend/services/rate_limiter.py:233-250) only returns a masked key_preview, so this button copies ci_...suffix, not a working credential. Either remove the action here or relabel it as preview-only; plain “Copy” strongly implies the real key is recoverable from the list view.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/APIKeysPage.tsx` around lines 177 - 183, The inline Copy
button currently copies the masked apiKey.key_preview (from the list API) which
is not a usable credential; update the UI in APIKeysPage so the row-level copy
either is removed or is explicitly relabeled as a preview copy. Concretely,
remove or conditionally replace the <CopyButton value={apiKey.key_preview} />
next to apiKey.key_preview with a non-misleading control (e.g., a disabled copy
or a button labeled "Copy Preview" that clearly indicates it's a preview-only
value) and keep the existing isRevoked guard; adjust any CopyButton props or
aria-labels so consumers see "Preview" instead of implying a real key is copied.
| <button | ||
| onClick={onRevoke} | ||
| disabled={revoking} | ||
| className={cn( | ||
| 'opacity-0 group-hover:opacity-100 p-1 rounded transition-all', | ||
| 'text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10', | ||
| )} | ||
| title="Revoke key" | ||
| > | ||
| <Trash2 className="w-3.5 h-3.5" /> | ||
| </button> |
There was a problem hiding this comment.
Keep the revoke control visible for keyboard and touch users.
opacity-0 group-hover:opacity-100 makes the action effectively invisible on focus, and coarse-pointer devices never get hover at all. Add a focus-visible/focus-within path and don’t hide it on small/touch layouts.
Suggested fix
<button
onClick={onRevoke}
disabled={revoking}
className={cn(
- 'opacity-0 group-hover:opacity-100 p-1 rounded transition-all',
+ 'opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 p-1 rounded transition-all',
+ 'focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10',
)}
+ aria-label={`Revoke key ${apiKey.name}`}
title="Revoke key"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| onClick={onRevoke} | |
| disabled={revoking} | |
| className={cn( | |
| 'opacity-0 group-hover:opacity-100 p-1 rounded transition-all', | |
| 'text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10', | |
| )} | |
| title="Revoke key" | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| </button> | |
| <button | |
| onClick={onRevoke} | |
| disabled={revoking} | |
| className={cn( | |
| 'opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 p-1 rounded transition-all', | |
| 'focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', | |
| 'text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10', | |
| )} | |
| aria-label={`Revoke key ${apiKey.name}`} | |
| title="Revoke key" | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/APIKeysPage.tsx` around lines 205 - 215, The revoke button
is hidden from keyboard and touch users due to "opacity-0
group-hover:opacity-100"; update the button styling around the button component
(the <button> using onRevoke, revoking, className, cn and rendering <Trash2>) to
ensure it's visible to focus and coarse-pointer devices by adding focus-visible
and focus-within visibility classes and a non-hover path for small/touch layouts
(e.g., add focus-visible:opacity-100 focus-within:opacity-100 and a small-screen
or coarse-pointer alternative such as sm:opacity-100 or a media-query based
class) so keyboard and touch users can always see and activate the revoke
control.
| <Dialog open onOpenChange={onClose}> | ||
| <DialogContent className="sm:max-w-md"> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
wc -l frontend/src/pages/APIKeysPage.tsxRepository: OpenCodeIntel/opencodeintel
Length of output: 109
🏁 Script executed:
cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '200,300p'Repository: OpenCodeIntel/opencodeintel
Length of output: 4294
🏁 Script executed:
head -50 frontend/src/pages/APIKeysPage.tsx | grep -E "import.*Dialog|from.*dialog"Repository: OpenCodeIntel/opencodeintel
Length of output: 140
🏁 Script executed:
cd frontend && fd -e "tsx" -e "ts" | xargs grep -l "export.*Dialog" | head -5Repository: OpenCodeIntel/opencodeintel
Length of output: 98
🏁 Script executed:
cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '1,30p'Repository: OpenCodeIntel/opencodeintel
Length of output: 1175
🏁 Script executed:
find frontend -name "dialog*" -type f | head -10Repository: OpenCodeIntel/opencodeintel
Length of output: 108
🏁 Script executed:
cat frontend/src/components/ui/dialog.tsxRepository: OpenCodeIntel/opencodeintel
Length of output: 3805
🏁 Script executed:
cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '50,150p' | grep -E "onClose|apiKey|setApiKey"Repository: OpenCodeIntel/opencodeintel
Length of output: 53
🏁 Script executed:
cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '400,525p'Repository: OpenCodeIntel/opencodeintel
Length of output: 5990
🏁 Script executed:
cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '340,380p'Repository: OpenCodeIntel/opencodeintel
Length of output: 1781
🏁 Script executed:
rg "onInteractOutside|onEscapeKeyDown" frontend/src --type tsx | head -10Repository: OpenCodeIntel/opencodeintel
Length of output: 98
🏁 Script executed:
rg "onInteractOutside|onEscapeKeyDown" frontend/src -t ts -t js | head -10Repository: OpenCodeIntel/opencodeintel
Length of output: 53
🏁 Script executed:
rg "DialogContent" frontend/src -A 3 -t ts -t js | grep -E "onInteractOutside|onEscapeKeyDown" | head -5Repository: OpenCodeIntel/opencodeintel
Length of output: 53
🏁 Script executed:
rg "DialogPrimitive.Close" frontend/src -A 5 -B 5Repository: OpenCodeIntel/opencodeintel
Length of output: 2029
🏁 Script executed:
python3 << 'EOF'
# Check Radix UI Dialog documentation for valid props
# Based on the component code, DialogContent accepts ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
# which means all standard Radix UI Content props are available
print("DialogContent wraps DialogPrimitive.Content from `@radix-ui/react-dialog`")
print("Valid props from Radix UI Dialog.Content include:")
print("- onInteractOutside: (event: PointerEvent) => void")
print("- onEscapeKeyDown: (event: KeyboardEvent) => void")
print("\nThese are standard Radix UI Dialog.Content props.")
EOFRepository: OpenCodeIntel/opencodeintel
Length of output: 344
🏁 Script executed:
cat -n frontend/src/components/ui/dialog.tsx | sed -n '32,45p'Repository: OpenCodeIntel/opencodeintel
Length of output: 1253
🏁 Script executed:
cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '223,281p'Repository: OpenCodeIntel/opencodeintel
Length of output: 2973
Prevent accidental dismissal of the one-time secret reveal dialog.
The dialog can be closed by pressing Escape, clicking outside, or clicking the X button, permanently discarding the only copy of the generated API key. For a one-time reveal with an irreplaceable secret, dismissal should only be possible through the explicit "Done" button.
The suggested fix prevents Escape and outside clicks, but still leaves the X close button functional. To fully address this, the close button should also be disabled or removed during the reveal.
Suggested fix
- <Dialog open onOpenChange={onClose}>
- <DialogContent className="sm:max-w-md">
+ <Dialog open onOpenChange={(open) => {
+ if (!open) onClose()
+ }}>
+ <DialogContent
+ className="sm:max-w-md"
+ onInteractOutside={(e) => e.preventDefault()}
+ onEscapeKeyDown={(e) => e.preventDefault()}
+ >Note: Also address the X close button (rendered unconditionally in DialogContent) by disabling it or making it conditional during secret reveal.
Also applies to: 273-279
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/APIKeysPage.tsx` around lines 227 - 228, Make the one-time
secret reveal dialog fully non-dismissible except via the explicit "Done"
button: prevent Escape and outside-click closing on the Dialog that currently
renders Dialog and DialogContent (open onOpenChange={onClose}) by adding
handlers to swallow onEscapeKeyDown and onInteractOutside (or the
framework-equivalent props) so they call
event.preventDefault()/event.stopPropagation() when in reveal mode, and also
hide or disable the X/close button rendered inside DialogContent during the
reveal (render it conditionally or set disabled when the one-time secret reveal
state is active). Apply the same change to the other reveal dialog instance at
the second location (lines ~273-279) so both dialogs are only dismissible via
the Done button.
| const [name, setName] = useState('') | ||
| const handleSubmit = () => { | ||
| if (!name.trim()) return | ||
| onGenerate(name.trim()) | ||
| setName('') | ||
| } | ||
| return ( | ||
| <Dialog open={open} onOpenChange={onClose}> | ||
| <DialogContent className="sm:max-w-sm"> | ||
| <DialogHeader> | ||
| <DialogTitle className="text-base">New API key</DialogTitle> | ||
| <DialogDescription> | ||
| Give it a name so you know where it is used. | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
| <div className="space-y-2"> | ||
| <Label htmlFor="key-name" className="text-xs text-muted-foreground">Key name</Label> | ||
| <Input | ||
| id="key-name" | ||
| placeholder="e.g. Claude Desktop, CI/CD, Local dev" | ||
| value={name} | ||
| onChange={(e) => setName(e.target.value)} | ||
| onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} | ||
| autoFocus | ||
| className="h-9 text-sm" | ||
| /> |
There was a problem hiding this comment.
Block repeat submissions while key generation is in flight.
The button disables, but the input stays live and Enter still calls handleSubmit(). That can send multiple POST /keys/generate requests for a non-idempotent operation and accidentally mint extra active keys.
Suggested fix
const [name, setName] = useState('')
const handleSubmit = () => {
- if (!name.trim()) return
+ if (generating || !name.trim()) return
onGenerate(name.trim())
setName('')
}
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle className="text-base">New API key</DialogTitle>
<DialogDescription>
Give it a name so you know where it is used.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="key-name" className="text-xs text-muted-foreground">Key name</Label>
<Input
id="key-name"
placeholder="e.g. Claude Desktop, CI/CD, Local dev"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
+ disabled={generating}
autoFocus
className="h-9 text-sm"
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [name, setName] = useState('') | |
| const handleSubmit = () => { | |
| if (!name.trim()) return | |
| onGenerate(name.trim()) | |
| setName('') | |
| } | |
| return ( | |
| <Dialog open={open} onOpenChange={onClose}> | |
| <DialogContent className="sm:max-w-sm"> | |
| <DialogHeader> | |
| <DialogTitle className="text-base">New API key</DialogTitle> | |
| <DialogDescription> | |
| Give it a name so you know where it is used. | |
| </DialogDescription> | |
| </DialogHeader> | |
| <div className="space-y-2"> | |
| <Label htmlFor="key-name" className="text-xs text-muted-foreground">Key name</Label> | |
| <Input | |
| id="key-name" | |
| placeholder="e.g. Claude Desktop, CI/CD, Local dev" | |
| value={name} | |
| onChange={(e) => setName(e.target.value)} | |
| onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} | |
| autoFocus | |
| className="h-9 text-sm" | |
| /> | |
| const [name, setName] = useState('') | |
| const handleSubmit = () => { | |
| if (generating || !name.trim()) return | |
| onGenerate(name.trim()) | |
| setName('') | |
| } | |
| return ( | |
| <Dialog open={open} onOpenChange={onClose}> | |
| <DialogContent className="sm:max-w-sm"> | |
| <DialogHeader> | |
| <DialogTitle className="text-base">New API key</DialogTitle> | |
| <DialogDescription> | |
| Give it a name so you know where it is used. | |
| </DialogDescription> | |
| </DialogHeader> | |
| <div className="space-y-2"> | |
| <Label htmlFor="key-name" className="text-xs text-muted-foreground">Key name</Label> | |
| <Input | |
| id="key-name" | |
| placeholder="e.g. Claude Desktop, CI/CD, Local dev" | |
| value={name} | |
| onChange={(e) => setName(e.target.value)} | |
| onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} | |
| disabled={generating} | |
| autoFocus | |
| className="h-9 text-sm" | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/APIKeysPage.tsx` around lines 295 - 320, Add a "generation
in flight" guard and disable the input while generating: introduce a boolean
state (e.g., isGenerating) used by the existing button disable logic, update
handleSubmit to return early if isGenerating, call setIsGenerating(true) before
invoking onGenerate and setIsGenerating(false) when the generate promise
resolves/rejects; also pass isGenerating to the Input (disabled prop) and change
the onKeyDown handler to ignore Enter when isGenerating so pressing Enter cannot
submit while a request is in flight. Ensure you reference handleSubmit,
onGenerate, and the Input/id="key-name" element when making these changes.
| const { data: keys = [], isLoading, isError } = useQuery({ | ||
| queryKey: ['api-keys'], | ||
| queryFn: () => fetchKeys(token), | ||
| enabled: !!token, | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
head -100 frontend/src/pages/APIKeysPage.tsxRepository: OpenCodeIntel/opencodeintel
Length of output: 3177
🏁 Script executed:
# Check for imports and session usage
rg "import.*session|useAuth|useSession|session\?\.user" frontend/src/pages/APIKeysPage.tsx -A 2 -B 2Repository: OpenCodeIntel/opencodeintel
Length of output: 531
🏁 Script executed:
# Look for other useQuery patterns with cache key scoping in the codebase
rg "queryKey.*\[.*,.*\]" frontend/src --type ts --type tsx -A 1 | head -50Repository: OpenCodeIntel/opencodeintel
Length of output: 98
🏁 Script executed:
# Check if there's a types.ts file with auth-related interfaces
find frontend/src -name "types.ts" -o -name "auth.ts" -o -name "session.ts" | head -20Repository: OpenCodeIntel/opencodeintel
Length of output: 141
🏁 Script executed:
# Understand the token source and how it relates to user identity
rg "token|fetchKeys" frontend/src/pages/APIKeysPage.tsx -B 5 -A 2 | head -80Repository: OpenCodeIntel/opencodeintel
Length of output: 1807
🏁 Script executed:
# Check the AuthContext to understand the session structure
cat frontend/src/contexts/AuthContext.tsx | head -150Repository: OpenCodeIntel/opencodeintel
Length of output: 3354
🏁 Script executed:
# Also check if there are other useQuery calls in the frontend that already use user identity in queryKey
rg "queryKey.*\[.*user|queryKey.*\[.*session" frontend/src -A 2 -B 1Repository: OpenCodeIntel/opencodeintel
Length of output: 1049
🏁 Script executed:
# Check hooks/useCachedQuery.ts mentioned in learnings to understand the caching pattern
cat frontend/src/hooks/useCachedQuery.ts 2>/dev/null || echo "File not found, searching for it:"Repository: OpenCodeIntel/opencodeintel
Length of output: 6300
🏁 Script executed:
fd "useCachedQuery" frontend/srcRepository: OpenCodeIntel/opencodeintel
Length of output: 107
Scope the API keys query cache by user identity.
['api-keys'] is shared globally, so a sign-out/sign-in cycle can briefly expose the previous user's cached keys until the refetch completes. Since session.user.id is available from useAuth(), include it in the query key to partition caches per user, matching the pattern already used in useUserUsage.
Suggested fix
const { data: keys = [], isLoading, isError } = useQuery({
- queryKey: ['api-keys'],
+ queryKey: ['api-keys', session?.user.id],
queryFn: () => fetchKeys(token),
- enabled: !!token,
+ enabled: !!token && !!session?.user.id,
})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/APIKeysPage.tsx` around lines 348 - 352, The query key for
useQuery in APIKeysPage is global ('api-keys') causing cross-user cache leakage;
update the queryKey used in useQuery (the call that currently passes
['api-keys'] and calls fetchKeys(token)) to include the current user's identity
(e.g., session.user.id from useAuth) so caches are partitioned per user, and
adjust the enabled condition to require the user id (e.g., enabled when token
and session.user.id are truthy) so the query only runs for the correct user.
|
Closing — this duplicated work already in PR #289. Redesign will be applied directly to feat/api-keys-dashboard branch instead. |
What
Full visual overhaul of
APIKeysPage.tsx. Addresses CEO feedback on PR #289 (generic design). All existing functionality preserved.Changes
What is NOT changed
APIKeysPageCloses
Supersedes the visual layer from PR #289. Backend from #288 is unchanged.
Summary by CodeRabbit