11import { useState } from "react" ;
22import { Exit } from "effect" ;
33import * as AsyncResult from "effect/unstable/reactivity/AsyncResult" ;
4- import { useAtomSet , useAtomValue } from "@effect/atom-react" ;
4+ import { useAtomRefresh , useAtomSet , useAtomValue } from "@effect/atom-react" ;
55import { toast } from "sonner" ;
66import { apiKeyWriteKeys } from "../api/reactivity-keys" ;
77import { trackEvent } from "../api/analytics" ;
@@ -21,6 +21,8 @@ import {
2121import { Input } from "../components/input" ;
2222import { Label } from "../components/label" ;
2323import { useExecutorDocumentTitle } from "../lib/document-title" ;
24+ import { ErrorState } from "../components/error-state" ;
25+ import { isAsyncResultLoading } from "../lib/async-result" ;
2426
2527// ---------------------------------------------------------------------------
2628// Shared API-keys page. Reads/writes the provider-neutral `/account/api-keys`
@@ -61,6 +63,7 @@ const defaultApiKeyName = (): string =>
6163export function ApiKeysPage ( ) {
6264 useExecutorDocumentTitle ( "API keys" ) ;
6365 const result = useAtomValue ( apiKeysAtom ) ;
66+ const refreshApiKeys = useAtomRefresh ( apiKeysAtom ) ;
6467 const doCreate = useAtomSet ( createApiKey , { mode : "promiseExit" } ) ;
6568 const doRevoke = useAtomSet ( revokeApiKey , { mode : "promiseExit" } ) ;
6669 const [ createOpen , setCreateOpen ] = useState ( false ) ;
@@ -134,65 +137,69 @@ export function ApiKeysPage() {
134137 </ p >
135138 </ PageHeader >
136139
137- { AsyncResult . match ( result , {
138- onInitial : ( ) => (
139- < div className = "rounded-md border border-border bg-card p-6 text-sm text-muted-foreground" >
140- Loading API keys...
141- </ div >
142- ) ,
143- onFailure : ( ) => (
144- < div className = "rounded-md border border-border bg-card p-6 text-sm text-destructive" >
145- Failed to load API keys
146- </ div >
147- ) ,
148- onSuccess : ( { value } ) =>
149- value . apiKeys . length === 0 ? (
150- < div className = "rounded-md border border-dashed border-border bg-card p-8" >
151- < h2 className = "text-base font-semibold text-foreground" > No API keys</ h2 >
152- < p className = "mt-2 max-w-xl text-sm leading-6 text-muted-foreground" >
153- Create a key and send it in the Authorization Bearer header.
154- </ p >
140+ { isAsyncResultLoading ( result ) ? (
141+ < div className = "rounded-md border border-border bg-card p-6 text-sm text-muted-foreground" >
142+ Loading API keys...
143+ </ div >
144+ ) : (
145+ AsyncResult . match ( result , {
146+ onInitial : ( ) => (
147+ < div className = "rounded-md border border-border bg-card p-6 text-sm text-muted-foreground" >
148+ Loading API keys...
155149 </ div >
156- ) : (
157- < div className = "overflow-hidden rounded-md border border-border bg-card" >
158- < div className = "grid grid-cols-[1fr_auto] gap-4 border-b border-border px-4 py-3 text-xs font-medium uppercase tracking-wider text-muted-foreground md:grid-cols-[1.4fr_1fr_1fr_auto]" >
159- < span > Name</ span >
160- < span className = "hidden md:block" > Created</ span >
161- < span className = "hidden md:block" > Last used</ span >
162- < span className = "text-right" > Actions</ span >
150+ ) ,
151+ onFailure : ( ) => (
152+ < ErrorState message = "Failed to load API keys" onRetry = { refreshApiKeys } />
153+ ) ,
154+ onSuccess : ( { value } ) =>
155+ value . apiKeys . length === 0 ? (
156+ < div className = "rounded-md border border-dashed border-border bg-card p-8" >
157+ < h2 className = "text-base font-semibold text-foreground" > No API keys</ h2 >
158+ < p className = "mt-2 max-w-xl text-sm leading-6 text-muted-foreground" >
159+ Create a key and send it in the Authorization Bearer header.
160+ </ p >
163161 </ div >
164- { value . apiKeys . map ( ( key : ApiKeySummary ) => (
165- < div
166- key = { key . id }
167- className = "grid grid-cols-[1fr_auto] items-center gap-4 border-b border-border px-4 py-4 last:border-b-0 md:grid-cols-[1.4fr_1fr_1fr_auto]"
168- >
169- < div className = "min-w-0" >
170- < p className = "truncate text-sm font-medium text-foreground" > { key . name } </ p >
171- < p className = "mt-1 font-mono text-xs text-muted-foreground" >
172- { key . obfuscatedValue }
162+ ) : (
163+ < div className = "overflow-hidden rounded-md border border-border bg-card" >
164+ < div className = "grid grid-cols-[1fr_auto] gap-4 border-b border-border px-4 py-3 text-xs font-medium uppercase tracking-wider text-muted-foreground md:grid-cols-[1.4fr_1fr_1fr_auto]" >
165+ < span > Name</ span >
166+ < span className = "hidden md:block" > Created</ span >
167+ < span className = "hidden md:block" > Last used</ span >
168+ < span className = "text-right" > Actions</ span >
169+ </ div >
170+ { value . apiKeys . map ( ( key : ApiKeySummary ) => (
171+ < div
172+ key = { key . id }
173+ className = "grid grid-cols-[1fr_auto] items-center gap-4 border-b border-border px-4 py-4 last:border-b-0 md:grid-cols-[1.4fr_1fr_1fr_auto]"
174+ >
175+ < div className = "min-w-0" >
176+ < p className = "truncate text-sm font-medium text-foreground" > { key . name } </ p >
177+ < p className = "mt-1 font-mono text-xs text-muted-foreground" >
178+ { key . obfuscatedValue }
179+ </ p >
180+ </ div >
181+ < p className = "hidden text-sm text-muted-foreground md:block" >
182+ { formatDate ( key . createdAt ) }
173183 </ p >
184+ < p className = "hidden text-sm text-muted-foreground md:block" >
185+ { formatDate ( key . lastUsedAt ) }
186+ </ p >
187+ < Button
188+ variant = "ghost"
189+ size = "icon-sm"
190+ onClick = { ( ) => handleRevoke ( key ) }
191+ disabled = { revokingId === key . id }
192+ title = { `Revoke ${ key . name } ` }
193+ className = "text-muted-foreground hover:text-destructive"
194+ >
195+ < span aria-hidden = "true" > ×</ span >
196+ </ Button >
174197 </ div >
175- < p className = "hidden text-sm text-muted-foreground md:block" >
176- { formatDate ( key . createdAt ) }
177- </ p >
178- < p className = "hidden text-sm text-muted-foreground md:block" >
179- { formatDate ( key . lastUsedAt ) }
180- </ p >
181- < Button
182- variant = "ghost"
183- size = "icon-sm"
184- onClick = { ( ) => handleRevoke ( key ) }
185- disabled = { revokingId === key . id }
186- title = { `Revoke ${ key . name } ` }
187- className = "text-muted-foreground hover:text-destructive"
188- >
189- < span aria-hidden = "true" > ×</ span >
190- </ Button >
191- </ div >
192- ) ) }
193- </ div >
194- ) ,
195- } ) }
198+ ) ) }
199+ </ div >
200+ ) ,
201+ } )
202+ ) }
196203
197204 < Dialog open = { createOpen } onOpenChange = { closeCreate } >
198205 < DialogContent className = "sm:max-w-[480px]" >
0 commit comments