@@ -12,6 +12,7 @@ import {
1212} from "@/components/ui/dialog" ;
1313import { api } from "@/lib/api" ;
1414import { Key , Plus , Trash2 , Copy , Check } from "lucide-react" ;
15+ import { ConfirmationDialog } from "@/components/ui/confirm-dialog" ;
1516
1617interface ApiKey {
1718 id : string ;
@@ -25,6 +26,7 @@ export default function ApiKeyManager() {
2526 const [ newKey , setNewKey ] = useState < string | null > ( null ) ;
2627 const [ loading , setLoading ] = useState ( false ) ;
2728 const [ copied , setCopied ] = useState ( false ) ;
29+ const [ revokeConfirmKeyId , setRevokeConfirmKeyId ] = useState < string | null > ( null ) ;
2830
2931 const fetchKeys = async ( ) => {
3032 try {
@@ -58,9 +60,14 @@ export default function ApiKeyManager() {
5860 }
5961 } ;
6062
61- const revokeKey = async ( id : string ) => {
62- if ( ! confirm ( "Are you sure you want to revoke this key? Any integrations using it will immediately break." ) ) return ;
63-
63+ const revokeKey = ( id : string ) => {
64+ setRevokeConfirmKeyId ( id ) ;
65+ } ;
66+
67+ const executeRevokeKey = async ( ) => {
68+ if ( ! revokeConfirmKeyId ) return ;
69+ const id = revokeConfirmKeyId ;
70+ setRevokeConfirmKeyId ( null ) ;
6471 try {
6572 await api . delete ( `/api/v1/auth/api-keys/${ id } ` ) ;
6673 setKeys ( ( prev ) => prev . filter ( ( k ) => k . id !== id ) ) ;
@@ -78,97 +85,111 @@ export default function ApiKeyManager() {
7885 } ;
7986
8087 return (
81- < Dialog onOpenChange = { ( open ) => { if ( ! open ) setNewKey ( null ) ; } } >
82- < DialogTrigger
83- render = {
84- < button
85- className = "flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
86- aria-label = "Open API key manager"
87- >
88- < Key className = "mr-2 h-4 w-4" />
89- < span > API Keys</ span >
90- </ button >
91- }
92- />
93- < DialogContent className = "max-w-2xl sm:rounded-2xl border-border/40 p-6 md:p-8 bg-background/95 backdrop-blur-xl shadow-2xl" >
88+ < >
89+ < Dialog onOpenChange = { ( open ) => { if ( ! open ) setNewKey ( null ) ; } } >
90+ < DialogTrigger
91+ render = {
92+ < button
93+ className = "flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
94+ aria-label = "Open API key manager"
95+ >
96+ < Key className = "mr-2 h-4 w-4" />
97+ < span > API Keys</ span >
98+ </ button >
99+ }
100+ />
101+ < DialogContent className = "max-w-2xl sm:rounded-2xl border-border/40 p-6 md:p-8 bg-background/95 backdrop-blur-xl shadow-2xl" >
94102
95- < DialogHeader >
96- < DialogTitle className = "text-2xl font-bold tracking-tight" > API Keys</ DialogTitle >
97- < DialogDescription className = "text-sm text-muted-foreground mt-1.5" >
98- Manage API keys to access the RAG engine programmatically from your own applications or scripts.
99- </ DialogDescription >
100- </ DialogHeader >
103+ < DialogHeader >
104+ < DialogTitle className = "text-2xl font-bold tracking-tight" > API Keys</ DialogTitle >
105+ < DialogDescription className = "text-sm text-muted-foreground mt-1.5" >
106+ Manage API keys to access the RAG engine programmatically from your own applications or scripts.
107+ </ DialogDescription >
108+ </ DialogHeader >
101109
102- { newKey && (
103- < div className = "my-6 p-5 border border-primary/20 bg-primary/5 rounded-xl space-y-3 animate-in fade-in zoom-in-95 duration-300" >
104- < h4 className = "font-semibold text-primary flex items-center gap-2" >
105- < Key className = "w-4 h-4" /> Save your new API key
106- </ h4 >
107- < p className = "text-sm text-muted-foreground" >
108- Please copy this key and store it somewhere safe. For security reasons, you will < strong > never</ strong > be able to view it again.
109- </ p >
110- < div className = "flex items-center gap-2 mt-2" >
111- < code className = "flex-1 bg-background/80 border border-border/50 px-4 py-2.5 rounded-lg text-sm font-mono break-all text-foreground shadow-inner" >
112- { newKey }
113- </ code >
114- < Button
115- onClick = { copyToClipboard }
116- variant = { copied ? "default" : "secondary" }
117- className = "shrink-0 shadow-sm"
118- aria-label = { copied ? "API key copied" : "Copy new API key" }
119- >
120- { copied ? < Check className = "w-4 h-4 mr-2" /> : < Copy className = "w-4 h-4 mr-2" /> }
121- { copied ? "Copied!" : "Copy" }
122- </ Button >
110+ { newKey && (
111+ < div className = "my-6 p-5 border border-primary/20 bg-primary/5 rounded-xl space-y-3 animate-in fade-in zoom-in-95 duration-300" >
112+ < h4 className = "font-semibold text-primary flex items-center gap-2" >
113+ < Key className = "w-4 h-4" /> Save your new API key
114+ </ h4 >
115+ < p className = "text-sm text-muted-foreground" >
116+ Please copy this key and store it somewhere safe. For security reasons, you will < strong > never</ strong > be able to view it again.
117+ </ p >
118+ < div className = "flex items-center gap-2 mt-2" >
119+ < code className = "flex-1 bg-background/80 border border-border/50 px-4 py-2.5 rounded-lg text-sm font-mono break-all text-foreground shadow-inner" >
120+ { newKey }
121+ </ code >
122+ < Button
123+ onClick = { copyToClipboard }
124+ variant = { copied ? "default" : "secondary" }
125+ className = "shrink-0 shadow-sm"
126+ aria-label = { copied ? "API key copied" : "Copy new API key" }
127+ >
128+ { copied ? < Check className = "w-4 h-4 mr-2" /> : < Copy className = "w-4 h-4 mr-2" /> }
129+ { copied ? "Copied!" : "Copy" }
130+ </ Button >
131+ </ div >
123132 </ div >
124- </ div >
125- ) }
133+ ) }
126134
127- < div className = "space-y-4 mt-6" >
128- < div className = "flex items-center justify-between" >
129- < h3 className = "text-sm font-medium text-foreground/80 uppercase tracking-wider" > Active Keys</ h3 >
130- < Button onClick = { generateKey } disabled = { loading } size = "sm" className = "rounded-full shadow-sm hover:shadow-md transition-shadow" >
131- < Plus className = "w-4 h-4 mr-1.5" />
132- Generate New Key
133- </ Button >
134- </ div >
135+ < div className = "space-y-4 mt-6" >
136+ < div className = "flex items-center justify-between" >
137+ < h3 className = "text-sm font-medium text-foreground/80 uppercase tracking-wider" > Active Keys</ h3 >
138+ < Button onClick = { generateKey } disabled = { loading } size = "sm" className = "rounded-full shadow-sm hover:shadow-md transition-shadow" >
139+ < Plus className = "w-4 h-4 mr-1.5" />
140+ Generate New Key
141+ </ Button >
142+ </ div >
135143
136- < div className = "rounded-xl border border-border/50 bg-card overflow-hidden shadow-sm" >
137- { keys . length === 0 ? (
138- < div className = "p-8 text-center text-sm text-muted-foreground bg-muted/20" >
139- < Key className = "w-8 h-8 mx-auto mb-3 opacity-20" />
140- You don't have any API keys yet.
141- </ div >
142- ) : (
143- < div className = "divide-y divide-border/50" >
144- { keys . map ( ( key ) => (
145- < div key = { key . id } className = "flex items-center justify-between p-4 hover:bg-muted/30 transition-colors group" >
146- < div className = "space-y-1" >
147- < div className = "font-mono text-sm font-medium tracking-tight" >
148- { key . key_prefix } ••••••••••••••••••••••
149- </ div >
150- < div className = "text-xs text-muted-foreground flex gap-4" >
151- < span > Created: { new Date ( key . created_at ) . toLocaleDateString ( ) } </ span >
152- < span > Last used: { key . last_used ? new Date ( key . last_used ) . toLocaleDateString ( ) : "Never" } </ span >
144+ < div className = "rounded-xl border border-border/50 bg-card overflow-hidden shadow-sm" >
145+ { keys . length === 0 ? (
146+ < div className = "p-8 text-center text-sm text-muted-foreground bg-muted/20" >
147+ < Key className = "w-8 h-8 mx-auto mb-3 opacity-20" />
148+ You don't have any API keys yet.
149+ </ div >
150+ ) : (
151+ < div className = "divide-y divide-border/50" >
152+ { keys . map ( ( key ) => (
153+ < div key = { key . id } className = "flex items-center justify-between p-4 hover:bg-muted/30 transition-colors group" >
154+ < div className = "space-y-1" >
155+ < div className = "font-mono text-sm font-medium tracking-tight" >
156+ { key . key_prefix } ••••••••••••••••••••••
157+ </ div >
158+ < div className = "text-xs text-muted-foreground flex gap-4" >
159+ < span > Created: { new Date ( key . created_at ) . toLocaleDateString ( ) } </ span >
160+ < span > Last used: { key . last_used ? new Date ( key . last_used ) . toLocaleDateString ( ) : "Never" } </ span >
161+ </ div >
153162 </ div >
163+ < Button
164+ variant = "ghost"
165+ size = "icon"
166+ onClick = { ( ) => revokeKey ( key . id ) }
167+ className = "text-destructive/70 hover:text-destructive hover:bg-destructive/10 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-all"
168+ title = "Revoke key"
169+ aria-label = { `Revoke API key ${ key . key_prefix } ` }
170+ >
171+ < Trash2 className = "w-4 h-4" />
172+ </ Button >
154173 </ div >
155- < Button
156- variant = "ghost"
157- size = "icon"
158- onClick = { ( ) => revokeKey ( key . id ) }
159- className = "text-destructive/70 hover:text-destructive hover:bg-destructive/10 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-all"
160- title = "Revoke key"
161- aria-label = { `Revoke API key ${ key . key_prefix } ` }
162- >
163- < Trash2 className = "w-4 h-4" />
164- </ Button >
165- </ div >
166- ) ) }
167- </ div >
168- ) }
174+ ) ) }
175+ </ div >
176+ ) }
177+ </ div >
169178 </ div >
170- </ div >
171- </ DialogContent >
172- </ Dialog >
179+ </ DialogContent >
180+ </ Dialog >
181+
182+ { /* Revoke Key Confirmation Dialog */ }
183+ < ConfirmationDialog
184+ open = { revokeConfirmKeyId !== null }
185+ onOpenChange = { ( open ) => { if ( ! open ) setRevokeConfirmKeyId ( null ) ; } }
186+ title = "Revoke API Key"
187+ message = "Are you sure you want to revoke this key? Any integrations using it will immediately break."
188+ confirmLabel = "Revoke"
189+ cancelLabel = "Cancel"
190+ variant = "danger"
191+ onConfirm = { executeRevokeKey }
192+ />
193+ </ >
173194 ) ;
174195}
0 commit comments