import { zodResolver } from '@hookform/resolvers/zod' import { useParams } from 'common' import { useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, Form, FormControl, FormField, FormMessage, Input_Shadcn_, Select_Shadcn_, SelectContent_Shadcn_, SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, Switch, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import z from 'zod' import { inverseValidBucketNameRegex, validBucketNameRegex } from './CreateBucketModal.utils' import { convertFromBytes, convertToBytes } from './StorageSettings/StorageSettings.utils' import { StorageSizeUnits } from '@/components/interfaces/Storage/StorageSettings/StorageSettings.constants' import { InlineLink } from '@/components/ui/InlineLink' import { useProjectStorageConfigQuery } from '@/data/config/project-storage-config-query' import { useBucketCreateMutation } from '@/data/storage/bucket-create-mutation' import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from '@/lib/constants' const FormSchema = z .object({ name: z .string() .trim() .min(1, 'Please provide a name for your bucket') .max(100, 'Bucket name should be below 100 characters') .refine( (value) => !value.endsWith(' '), 'The name of the bucket cannot end with a whitespace' ) .refine( (value) => value !== 'public', '"public" is a reserved name. Please choose another name' ), public: z.boolean().default(false), has_file_size_limit: z.boolean().default(false), formatted_size_limit: z.coerce .number() .min(0, 'File size upload limit has to be at least 0') .optional(), allowed_mime_types: z.string().trim().default(''), }) .superRefine((data, ctx) => { if (!validBucketNameRegex.test(data.name)) { const [match] = data.name.match(inverseValidBucketNameRegex) ?? [] ctx.addIssue({ path: ['name'], code: z.ZodIssueCode.custom, message: !!match ? `Bucket name cannot contain the "${match}" character` : 'Bucket name contains an invalid special character', }) } }) const formId = 'create-storage-bucket-form' export type CreateBucketForm = z.infer interface CreateBucketModalProps { open: boolean onOpenChange: (value: boolean) => void } export const CreateBucketModal = ({ open, onOpenChange }: CreateBucketModalProps) => { const { ref } = useParams() const { data: org } = useSelectedOrganizationQuery() const [selectedUnit, setSelectedUnit] = useState(StorageSizeUnits.MB) const [hasAllowedMimeTypes, setHasAllowedMimeTypes] = useState(false) const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM }) const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0) const formattedGlobalUploadLimit = `${value} ${unit}` const { mutate: sendEvent } = useSendEventMutation() const { mutateAsync: createBucket, isPending: isCreatingBucket } = useBucketCreateMutation({ // [Joshen] Silencing the error here as it's being handled in onSubmit onError: () => {}, }) const form = useForm({ resolver: zodResolver(FormSchema), defaultValues: { name: '', public: false, has_file_size_limit: false, formatted_size_limit: undefined, allowed_mime_types: '', }, }) const { formatted_size_limit: formattedSizeLimitError } = form.formState.errors const isPublicBucket = form.watch('public') const hasFileSizeLimit = form.watch('has_file_size_limit') const onSubmit: SubmitHandler = async (values) => { if (!ref) return console.error('Project ref is required') // [Joshen] Should shift this into superRefine in the form schema try { const fileSizeLimit = values.has_file_size_limit && values.formatted_size_limit !== undefined ? convertToBytes(values.formatted_size_limit, selectedUnit as StorageSizeUnits) : undefined const allowedMimeTypes = hasAllowedMimeTypes && values.allowed_mime_types.length > 0 ? values.allowed_mime_types.split(',').map((x) => x.trim()) : undefined if (!!fileSizeLimit && !!data?.fileSizeLimit && fileSizeLimit > data.fileSizeLimit) { return form.setError('formatted_size_limit', { type: 'manual', message: 'exceed_global_limit', }) } await createBucket({ projectRef: ref, id: values.name, type: 'STANDARD', isPublic: values.public, file_size_limit: fileSizeLimit, allowed_mime_types: allowedMimeTypes, }) sendEvent({ action: 'storage_bucket_created', properties: { bucketType: 'STANDARD' }, groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) toast.success(`Successfully created bucket ${values.name}`) form.reset() setSelectedUnit(StorageSizeUnits.MB) onOpenChange(false) } catch (error: any) { // Handle specific error cases for inline display const errorMessage = error.message?.toLowerCase() || '' if ( errorMessage.includes('mime type') && (errorMessage.includes('is not supported') || errorMessage.includes('not supported')) ) { // Set form error for the MIME types field form.setError('allowed_mime_types', { type: 'manual', message: 'Invalid MIME type format. Please check your input.', }) } else { // For other errors, show a toast as fallback toast.error(`Failed to create bucket: ${error.message}`) } } } const handleClose = () => { form.reset() setSelectedUnit(StorageSizeUnits.MB) onOpenChange(false) } return ( { if (!open) { handleClose() } }} > Create file bucket
( )} /> ( )} /> {isPublicBucket && ( )} ( )} /> {hasFileSizeLimit && (
(
{selectedUnit} {Object.values(StorageSizeUnits).map((unit: string) => ( {unit} ))}
)} /> {formattedSizeLimitError?.message === 'exceed_global_limit' && ( Exceeds global limit of {formattedGlobalUploadLimit}. Increase limit in{' '} onOpenChange(false)} > Storage Settings {' '} first. )} {IS_PLATFORM && (

This project has a{' '} onOpenChange(false)} > global file size limit {' '} of {formattedGlobalUploadLimit}.

)}
)}
{hasAllowedMimeTypes && ( ( )} /> )}
) }