Files
Danny White 92952bf903 fix(studio): clarify S3 access key dialogs (#51070)
## 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 -->
2026-09-30 15:21:47 +10:00

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>
)
}