Files
supabase/apps/studio/components/interfaces/APIKeys/APIKeyRow.tsx
Jonathan Summers-Muir 4649bf911e feat: new api keys [hidden] (#33252)
* feat: add basic api keys ui

* init JWT secrets. rough

* Update JWTSecretKeysTable.tsx

* added some info hover cards.

• found this this is probably the wrong direction
• will create a new page for next iteration.

* init new version

* add illustrations

* Update JWTSecretKeysTablev2.tsx

* chore: delete API key now works

* some style changes

* added better tables

* Update JWTSecretKeysTablev2.tsx

* add public JWT dialog

* moar

* adding sub layout in

* starts adding in a ButtonGroup

* about to make into separate components

* added quick copy to project loading screen

* build state

* basic loading

* confirm dialog and loading states

* switched for better loading experience

* moved styles of Input to InputVariants

* issue with ref type

* loading,error and rest states

* new loading states

* alt l;ayout

* add group

* updated error states for permissions

* copy button behaviour for secret keys

* delete dialog

* Update QuickKeyCopy.tsx

* fix type errors

* Update JWTSecretKeysTablev2.tsx

* update menu to hide pages

* Update SettingsMenu.utils.tsx

* Update resource-query.ts

* remove old file

* moved JWT secrets to use valtio

* Update api-keys-query.ts

* fix typecheck

* rename files

* remove JWT stuff

* revert file

* remove more JWT stuff

* Update package.json

* Update pnpm-lock.yaml

* Update ProjectLayout.tsx

* Update PublishableAPIKeys.tsx

* Update api-keys-query.ts

* refactor api-keys-query

* Update SettingsMenu.utils.tsx

* Some clean up

* more clean up and refactor

* Update APIKeyRow.tsx

* Update LayoutHeader.tsx

* resolve comments

* Update CreateSecretAPIKeyModal.tsx

* Update APIKeyRow.tsx

* Add perms check for delete API keys

* Remove console log

* Delete ConnectDialog.tsx

* use project ref

---------

Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2025-02-05 15:21:10 +01:00

194 lines
5.5 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { InputVariants } from '@ui/components/shadcn/ui/input'
import { Eye, MoreVertical } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import CopyButton from 'components/ui/CopyButton'
import { useAPIKeyIdQuery } from 'data/api-keys/[id]/api-key-id-query'
import { APIKeysData } from 'data/api-keys/api-keys-query'
import { apiKeysKeys } from 'data/api-keys/keys'
import { AnimatePresence, motion } from 'framer-motion'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
TableCell,
TableRow,
cn,
} from 'ui'
import { APIKeyDeleteDialog } from './APIKeyDeleteDialog'
export const APIKeyRow = ({
apiKey,
}: {
apiKey: Extract<APIKeysData[number], { type: 'secret' | 'publishable' }>
}) => {
const MotionTableRow = motion(TableRow)
return (
<MotionTableRow
layout
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{
type: 'spring',
stiffness: 500,
damping: 50,
mass: 1,
}}
>
<TableCell className="py-2">{apiKey.description || '/'}</TableCell>
<TableCell className="py-2">
<div className="flex flex-row gap-2">
<Input apiKey={apiKey} />
</div>
</TableCell>
<TableCell className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger className="px-1 focus-visible:outline-none" asChild>
<Button
type="text"
size="tiny"
icon={
<MoreVertical size="14" className="text-foreground-light hover:text-foreground" />
}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="max-w-40" align="end">
<APIKeyDeleteDialog apiKey={apiKey} />
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</MotionTableRow>
)
}
function Input({
apiKey,
}: {
apiKey: Extract<APIKeysData[number], { type: 'secret' | 'publishable' }>
}) {
const queryClient = useQueryClient()
const { ref: projectRef } = useParams()
const [show, setShowState] = useState(false)
// to do
// const canReadAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*')
const isSecret = apiKey.type === 'secret'
const {
data,
isLoading: isLoadingApiKey,
error,
refetch: refetchApiKey,
} = useAPIKeyIdQuery(
{
projectRef,
id: apiKey.id as string,
reveal: true,
},
{
enabled: show,
staleTime: 0, // Data is considered stale immediately
cacheTime: 0, // Cache is cleared immediately after query becomes stale
}
)
async function onSubmitShow() {
setShowState(true)
// Set a timeout to invalidate the cache after a certain amount of time
setTimeout(() => {
setShowState(false)
queryClient.removeQueries({
queryKey: apiKeysKeys.single(projectRef, apiKey.id as string),
exact: true,
})
}, 10000) // Destroy query after 10 seconds
}
async function onCopy() {
// if ID already exists from a reveal action, return that
if (data?.api_key) return data?.api_key
try {
// fetch ID and then destroy query immediately
const result = await refetchApiKey()
queryClient.removeQueries({
queryKey: apiKeysKeys.single(projectRef, apiKey.id as string),
exact: true,
})
if (result.isSuccess) return result.data.api_key
if (error) {
toast.error('Failed to copy secret API key')
}
} catch (error) {
console.error('Failed to fetch API key:', error)
}
// Fallback to the masked version
return apiKey.api_key
}
return (
<>
<div
className={cn(
InputVariants({ size: 'tiny' }),
'flex-1 grow gap-0 font-mono rounded-full max-w-60 overflow-hidden',
show ? 'ring-1 ring-foreground-lighter ring-opacity-50' : 'ring-0 ring-opacity-0',
'transition-all'
)}
>
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={show ? 'shown' : 'hidden'}
initial={{ opacity: 0, y: show ? 16 : -16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: show ? 16 : -16 }}
transition={{
duration: 0.12,
y: { type: 'spring', stiffness: 2450, damping: 55 },
}}
className="truncate"
>
{show ? data?.api_key : apiKey?.api_key}
</motion.span>
</AnimatePresence>
</div>
{isSecret && (
<AnimatePresence>
{!show && (
<motion.div
initial={{ opacity: 0, scale: 1, width: 0 }}
animate={{ opacity: 1, scale: 1, width: 'auto' }}
exit={{ opacity: 0, scale: 1, width: 0 }}
transition={{ duration: 0.12 }}
style={{ overflow: 'hidden' }}
>
<Button
type="outline"
className="rounded-full px-2"
icon={<Eye strokeWidth={2} />}
onClick={onSubmitShow}
/>
</motion.div>
)}
</AnimatePresence>
)}
<CopyButton type="default" asyncText={onCopy} iconOnly className="rounded-full px-2" />
</>
)
}