@@ -14,16 +14,6 @@ import {
1414 DialogHeader ,
1515 DialogTitle ,
1616} from '@/components/ui/dialog'
17- import {
18- AlertDialog ,
19- AlertDialogAction ,
20- AlertDialogCancel ,
21- AlertDialogContent ,
22- AlertDialogDescription ,
23- AlertDialogFooter ,
24- AlertDialogHeader ,
25- AlertDialogTitle ,
26- } from '@/components/ui/alert-dialog'
2717import { toast } from 'sonner'
2818import { API_URL } from '@/config/api'
2919import { cn } from '@/lib/utils'
@@ -57,8 +47,6 @@ async function fetchKeys(token: string): Promise<APIKey[]> {
5747 return data . keys || [ ]
5848}
5949
60- // -- Sub-components --
61-
6250function TierBadge ( { tier } : { tier : string } ) {
6351 const styles : Record < string , string > = {
6452 enterprise : 'bg-violet-500/10 text-violet-400 border-violet-500/20' ,
@@ -153,52 +141,29 @@ function EmptyState({ onGenerate }: { onGenerate: () => void }) {
153141 )
154142}
155143
156- // Table row -- hover reveals revoke button, revoked rows go 50% opacity
157- function KeyRow ( {
158- apiKey,
159- onRevoke,
160- revoking,
161- } : {
162- apiKey : APIKey
163- onRevoke : ( ) => void
164- revoking : boolean
165- } ) {
144+ function KeyRow ( { apiKey, onRevoke, revoking } : { apiKey : APIKey ; onRevoke : ( ) => void ; revoking : boolean } ) {
166145 const isRevoked = ! apiKey . active
167146 return (
168147 < div className = { cn (
169148 'group grid items-center gap-4 px-4 py-3.5 border-b border-border last:border-0 transition-colors' ,
170149 'grid-cols-[1fr_200px_80px_110px_120px]' ,
171150 isRevoked ? 'opacity-50' : 'hover:bg-muted/20' ,
172151 ) } >
173- { /* Name + created */ }
174152 < div className = "min-w-0" >
175153 < p className = "text-sm font-medium text-foreground truncate" > { apiKey . name } </ p >
176154 < p className = "text-[11px] text-muted-foreground mt-0.5 flex items-center gap-1" >
177155 < Clock className = "w-3 h-3" />
178156 Created { timeAgo ( apiKey . created_at ) }
179157 </ p >
180158 </ div >
181-
182- { /* Key preview + inline copy */ }
183159 < div className = "flex items-center gap-2 min-w-0" >
184- < code className = "text-xs font-mono text-muted-foreground truncate flex-1" >
185- { apiKey . key_preview }
186- </ code >
160+ < code className = "text-xs font-mono text-muted-foreground truncate flex-1" > { apiKey . key_preview } </ code >
187161 { ! isRevoked && < CopyButton value = { apiKey . key_preview } /> }
188162 </ div >
189-
190- { /* Tier */ }
191163 < div > < TierBadge tier = { apiKey . tier } /> </ div >
192-
193- { /* Last used */ }
194164 < span className = "text-xs text-muted-foreground" >
195- { apiKey . last_used_at
196- ? timeAgo ( apiKey . last_used_at )
197- : < span className = "text-muted-foreground/40" > Never</ span >
198- }
165+ { apiKey . last_used_at ? timeAgo ( apiKey . last_used_at ) : < span className = "text-muted-foreground/40" > Never</ span > }
199166 </ span >
200-
201- { /* Status + hover-reveal revoke */ }
202167 < div className = "flex items-center justify-end gap-3" >
203168 < span className = "inline-flex items-center gap-1.5" >
204169 < span className = { cn ( 'w-1.5 h-1.5 rounded-full' , apiKey . active ? 'bg-emerald-400' : 'bg-zinc-600' ) } />
@@ -221,7 +186,6 @@ function KeyRow({
221186 )
222187}
223188
224- // One-time key reveal -- shown immediately after generation
225189function NewKeyModal ( { apiKey, onClose } : { apiKey : string | null ; onClose : ( ) => void } ) {
226190 if ( ! apiKey ) return null
227191 return (
@@ -239,9 +203,7 @@ function NewKeyModal({ apiKey, onClose }: { apiKey: string | null; onClose: () =
239203 < div className = "space-y-3" >
240204 < div className = "rounded-lg border border-border bg-muted/30 overflow-hidden" >
241205 < div className = "px-4 py-3" >
242- < code className = "text-xs font-mono text-foreground break-all leading-relaxed select-all" >
243- { apiKey }
244- </ code >
206+ < code className = "text-xs font-mono text-foreground break-all leading-relaxed select-all" > { apiKey } </ code >
245207 </ div >
246208 < div className = "border-t border-border px-4 py-2 flex justify-end bg-muted/50" >
247209 < CopyButton value = { apiKey } />
@@ -272,17 +234,8 @@ function NewKeyModal({ apiKey, onClose }: { apiKey: string | null; onClose: () =
272234 )
273235}
274236
275- // Name-only generate dialog
276- function GenerateKeyModal ( {
277- open,
278- onClose,
279- onGenerate,
280- generating,
281- } : {
282- open : boolean
283- onClose : ( ) => void
284- onGenerate : ( name : string ) => void
285- generating : boolean
237+ function GenerateKeyModal ( { open, onClose, onGenerate, generating } : {
238+ open : boolean ; onClose : ( ) => void ; onGenerate : ( name : string ) => void ; generating : boolean
286239} ) {
287240 const [ name , setName ] = useState ( '' )
288241 const handleSubmit = ( ) => {
@@ -321,7 +274,35 @@ function GenerateKeyModal({
321274 )
322275}
323276
324- // -- Main export (named, matches existing import in Dashboard.tsx) --
277+ // Revoke confirm -- uses existing Dialog, no new deps needed
278+ function RevokeConfirmModal ( { target, onConfirm, onClose } : {
279+ target : APIKey | null ; onConfirm : ( ) => void ; onClose : ( ) => void
280+ } ) {
281+ return (
282+ < Dialog open = { ! ! target } onOpenChange = { ( open ) => ! open && onClose ( ) } >
283+ < DialogContent className = "sm:max-w-sm" >
284+ < DialogHeader >
285+ < DialogTitle className = "text-base" > Revoke key</ DialogTitle >
286+ < DialogDescription >
287+ Revoke < span className = "font-medium text-foreground" > { target ?. name } </ span > ?
288+ Any services using this key will lose access immediately. This cannot be undone.
289+ </ DialogDescription >
290+ </ DialogHeader >
291+ < DialogFooter >
292+ < Button variant = "outline" size = "sm" onClick = { onClose } className = "h-8 text-xs" > Cancel</ Button >
293+ < Button
294+ variant = "destructive"
295+ size = "sm"
296+ onClick = { onConfirm }
297+ className = "h-8 text-xs"
298+ >
299+ Revoke key
300+ </ Button >
301+ </ DialogFooter >
302+ </ DialogContent >
303+ </ Dialog >
304+ )
305+ }
325306
326307export function APIKeysPage ( ) {
327308 const { session } = useAuth ( )
@@ -386,7 +367,6 @@ export function APIKeysPage() {
386367 const activeKeys = keys . filter ( ( k ) => k . active )
387368
388369 if ( isLoading ) return < APIKeysSkeleton />
389-
390370 if ( isError ) {
391371 return (
392372 < div className = "flex items-center justify-center min-h-[300px] text-muted-foreground text-sm" >
@@ -397,33 +377,23 @@ export function APIKeysPage() {
397377
398378 return (
399379 < div className = "space-y-4 max-w-4xl" >
400-
401- { /* Header -- same pattern as UsagePage */ }
402380 < div className = "flex items-start justify-between gap-4" >
403381 < div className = "flex items-center gap-3" >
404382 < div className = "w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center shrink-0" >
405383 < Key className = "w-5 h-5 text-primary" />
406384 </ div >
407385 < div >
408386 < h1 className = "text-lg font-semibold text-foreground" > API Keys</ h1 >
409- < p className = "text-sm text-muted-foreground" >
410- Manage keys for MCP, Claude Desktop, and API access
411- </ p >
387+ < p className = "text-sm text-muted-foreground" > Manage keys for MCP, Claude Desktop, and API access</ p >
412388 </ div >
413389 </ div >
414- < Button
415- size = "sm"
416- onClick = { ( ) => setGenerateOpen ( true ) }
417- disabled = { activeKeys . length >= 5 }
418- className = "gap-2 h-9 text-sm shrink-0"
419- >
390+ < Button size = "sm" onClick = { ( ) => setGenerateOpen ( true ) } disabled = { activeKeys . length >= 5 } className = "gap-2 h-9 text-sm shrink-0" >
420391 < Plus className = "w-4 h-4" />
421392 Generate key
422393 { activeKeys . length >= 5 && < span className = "text-xs opacity-60 ml-1" > (limit reached)</ span > }
423394 </ Button >
424395 </ div >
425396
426- { /* Security note */ }
427397 < div className = "flex items-start gap-3 rounded-lg border border-border bg-muted/20 px-4 py-3.5" >
428398 < Shield className = "w-4 h-4 text-muted-foreground shrink-0 mt-0.5" />
429399 < div >
@@ -435,14 +405,12 @@ export function APIKeysPage() {
435405 </ div >
436406 </ div >
437407
438- { /* Table or empty state */ }
439408 { keys . length === 0 ? (
440409 < div className = "rounded-lg border border-border" >
441410 < EmptyState onGenerate = { ( ) => setGenerateOpen ( true ) } />
442411 </ div >
443412 ) : (
444413 < div className = "rounded-lg border border-border overflow-hidden" >
445- { /* Column headers */ }
446414 < div className = "grid gap-4 px-4 py-2.5 bg-muted/40 border-b border-border grid-cols-[1fr_200px_80px_110px_120px]" >
447415 < span className = "text-[11px] font-medium text-muted-foreground uppercase tracking-wider" > Name</ span >
448416 < span className = "text-[11px] font-medium text-muted-foreground uppercase tracking-wider" > Key</ span >
@@ -451,14 +419,8 @@ export function APIKeysPage() {
451419 < span className = "text-[11px] font-medium text-muted-foreground uppercase tracking-wider text-right" > Status</ span >
452420 </ div >
453421 { keys . map ( ( k ) => (
454- < KeyRow
455- key = { k . id }
456- apiKey = { k }
457- onRevoke = { ( ) => setRevokeTarget ( k ) }
458- revoking = { revoking === k . id }
459- />
422+ < KeyRow key = { k . id } apiKey = { k } onRevoke = { ( ) => setRevokeTarget ( k ) } revoking = { revoking === k . id } />
460423 ) ) }
461- { /* Footer */ }
462424 < div className = "px-4 py-2.5 border-t border-border bg-muted/20 flex items-center justify-between" >
463425 < span className = "text-[11px] text-muted-foreground" >
464426 { activeKeys . length } active{ activeKeys . length !== keys . length && ` / ${ keys . length } total` }
@@ -470,40 +432,9 @@ export function APIKeysPage() {
470432 </ div >
471433 ) }
472434
473- { /* Modals */ }
474- < GenerateKeyModal
475- open = { generateOpen }
476- onClose = { ( ) => setGenerateOpen ( false ) }
477- onGenerate = { handleGenerate }
478- generating = { generating }
479- />
480-
481- < NewKeyModal
482- apiKey = { generatedKey }
483- onClose = { ( ) => setGeneratedKey ( null ) }
484- />
485-
486- < AlertDialog open = { ! ! revokeTarget } onOpenChange = { ( open ) => ! open && setRevokeTarget ( null ) } >
487- < AlertDialogContent className = "max-w-sm" >
488- < AlertDialogHeader >
489- < AlertDialogTitle className = "text-base" > Revoke key</ AlertDialogTitle >
490- < AlertDialogDescription >
491- Revoke < span className = "font-medium text-foreground" > { revokeTarget ?. name } </ span > ?
492- Any services using this key will lose access immediately. This cannot be undone.
493- </ AlertDialogDescription >
494- </ AlertDialogHeader >
495- < AlertDialogFooter >
496- < AlertDialogCancel className = "h-8 text-xs" > Cancel</ AlertDialogCancel >
497- < AlertDialogAction
498- onClick = { handleRevoke }
499- className = "h-8 text-xs bg-destructive hover:bg-destructive/90 text-destructive-foreground"
500- >
501- Revoke key
502- </ AlertDialogAction >
503- </ AlertDialogFooter >
504- </ AlertDialogContent >
505- </ AlertDialog >
506-
435+ < GenerateKeyModal open = { generateOpen } onClose = { ( ) => setGenerateOpen ( false ) } onGenerate = { handleGenerate } generating = { generating } />
436+ < NewKeyModal apiKey = { generatedKey } onClose = { ( ) => setGeneratedKey ( null ) } />
437+ < RevokeConfirmModal target = { revokeTarget } onConfirm = { handleRevoke } onClose = { ( ) => setRevokeTarget ( null ) } />
507438 </ div >
508439 )
509440}
0 commit comments