mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
## Problem The S3 access key creation dialogs are wider than their contents, use plural titles for one key pair, and call the name field “Description” even though the table calls it “Name”. The save state also implies both values disappear, although only the secret does. ## Solution Use the small dialog size for both states. Use singular titles, label the field “Name”, shorten the create button to “Create”, and clarify when the secret must be copied. The API field remains `description`. ## Review instructions 1. Open a project’s **Storage > S3** page and select **New access key**. Check the dialog width, title, Name field, and Create button. 2. Create a key and check the save dialog width, singular title, and secret visibility guidance. | Before | After | | --- | --- | | <img width="1084" height="572" alt="CleanShot 2026-09-30 at 14 37 20@2x" src="https://github.com/user-attachments/assets/781706ee-0ecc-4535-abb5-f6ac65f02c71" /> | <img width="844" height="584" alt="CleanShot 2026-09-30 at 14 36 56@2x" src="https://github.com/user-attachments/assets/119df19f-2f39-4e23-94b4-26665583765c" /> | | <img width="1096" height="730" alt="CleanShot 2026-09-30 at 14 37 57@2x" src="https://github.com/user-attachments/assets/00481ed7-dc05-48b9-8cbc-e76d608aa4b1" /> | <img width="842" height="780" alt="CleanShot 2026-09-30 at 14 37 39@2x" src="https://github.com/user-attachments/assets/8c837101-250a-474f-a0fc-cf46ce491f83" /> | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Updates** * The credential form now labels the field “Name” and uses “Create” for the submit button. * Confirmation text now clarifies that the access key is bucket-wide, bypasses RLS, and its secret is shown only once. It also refers to a single access key instead of using S3-specific wording. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
189 lines
5.9 KiB
TypeScript
189 lines
5.9 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { Plus } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import {
|
|
Button,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogSection,
|
|
DialogSectionSeparator,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
import { Input } from 'ui-patterns/DataInputs/Input'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import { z } from 'zod'
|
|
|
|
import { useProjectStorageConfigQuery } from '@/data/config/project-storage-config-query'
|
|
import { useS3AccessKeyCreateMutation } from '@/data/storage/s3-access-key-create-mutation'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useIsProjectActive } from '@/hooks/misc/useSelectedProject'
|
|
|
|
interface CreateCredentialModalProps {
|
|
visible: boolean
|
|
onOpenChange: (value: boolean) => void
|
|
}
|
|
|
|
export const CreateCredentialModal = ({ visible, onOpenChange }: CreateCredentialModalProps) => {
|
|
const { ref: projectRef } = useParams()
|
|
const isProjectActive = useIsProjectActive()
|
|
const [showSuccess, setShowSuccess] = useState(false)
|
|
|
|
const { can: canCreateCredentials } = useAsyncCheckPermissions(
|
|
PermissionAction.STORAGE_ADMIN_WRITE,
|
|
'*'
|
|
)
|
|
|
|
const { data: config } = useProjectStorageConfigQuery({ projectRef })
|
|
const isS3ConnectionEnabled = config?.features.s3Protocol.enabled
|
|
const disableCreation = !isProjectActive || !canCreateCredentials || !isS3ConnectionEnabled
|
|
|
|
const FormSchema = z.object({
|
|
description: z.string().min(3, {
|
|
message: 'Name must be at least 3 characters long',
|
|
}),
|
|
})
|
|
const form = useForm<z.infer<typeof FormSchema>>({
|
|
resolver: zodResolver(FormSchema),
|
|
defaultValues: {
|
|
description: '',
|
|
},
|
|
})
|
|
|
|
const {
|
|
data: createS3KeyData,
|
|
mutate: createS3AccessKey,
|
|
isPending: isCreating,
|
|
} = useS3AccessKeyCreateMutation({
|
|
onSuccess: () => {
|
|
setShowSuccess(true)
|
|
form.reset()
|
|
},
|
|
})
|
|
|
|
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
|
createS3AccessKey({ projectRef, ...data })
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
open={visible}
|
|
onOpenChange={(open) => {
|
|
onOpenChange(open)
|
|
if (!open) setShowSuccess(false)
|
|
}}
|
|
>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
icon={<Plus size={14} />}
|
|
disabled={disableCreation}
|
|
className="pointer-events-auto"
|
|
>
|
|
New access key
|
|
</Button>
|
|
</DialogTrigger>
|
|
</TooltipTrigger>
|
|
{disableCreation && (
|
|
<TooltipContent side="bottom">
|
|
{!isProjectActive
|
|
? 'Restore your project to create new access keys'
|
|
: !isS3ConnectionEnabled
|
|
? 'Connection via S3 protocol is currently disabled'
|
|
: !canCreateCredentials
|
|
? 'You need additional permissions to create new access keys'
|
|
: ''}
|
|
</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
|
|
<DialogContent
|
|
size="small"
|
|
onInteractOutside={(e) => {
|
|
if (showSuccess) e.preventDefault()
|
|
}}
|
|
>
|
|
{showSuccess ? (
|
|
<>
|
|
<DialogHeader>
|
|
<DialogTitle>Save your new access key</DialogTitle>
|
|
<DialogDescription>
|
|
Copy the access key ID and secret now. The secret won't be shown again. If you lose
|
|
it, create a new access key.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogSectionSeparator />
|
|
<DialogSection className="flex flex-col gap-4">
|
|
<FormItemLayout label="Access key ID" isReactForm={false}>
|
|
<Input className="input-mono" readOnly copy value={createS3KeyData?.access_key} />
|
|
</FormItemLayout>
|
|
<FormItemLayout label="Secret access key" isReactForm={false}>
|
|
<Input className="input-mono" readOnly copy value={createS3KeyData?.secret_key} />
|
|
</FormItemLayout>
|
|
</DialogSection>
|
|
<DialogFooter>
|
|
<Button
|
|
onClick={() => {
|
|
onOpenChange(false)
|
|
setShowSuccess(false)
|
|
}}
|
|
>
|
|
Done
|
|
</Button>
|
|
</DialogFooter>
|
|
</>
|
|
) : (
|
|
<>
|
|
<DialogHeader>
|
|
<DialogTitle>Create new access key</DialogTitle>
|
|
<DialogDescription>
|
|
This access key grants full access to every bucket and bypasses RLS policies.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogSectionSeparator />
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
<DialogSection>
|
|
<FormField
|
|
name="description"
|
|
render={({ field }) => (
|
|
<FormItemLayout label="Name">
|
|
<FormControl>
|
|
<Input
|
|
autoComplete="off"
|
|
placeholder="My test key"
|
|
type="text"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</DialogSection>
|
|
<DialogFooter>
|
|
<Button variant="primary" type="submit" loading={isCreating}>
|
|
Create
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</Form>
|
|
</>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|