import { zodResolver } from '@hookform/resolvers/zod' import { useParams } from 'common' import { useEffect, useRef, useState } from 'react' import { useForm, type SubmitHandler } from 'react-hook-form' import { toast } from 'sonner' import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogSection, DialogSectionSeparator, DialogTitle, Form, FormControl, FormField, FormMessage, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from 'ui' import { Admonition } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { z } from 'zod' import { StorageSizeUnits } from '@/components/interfaces/Storage/StorageSettings/StorageSettings.constants' import { convertFromBytes, convertToBytes, } from '@/components/interfaces/Storage/StorageSettings/StorageSettings.utils' import { InlineLink } from '@/components/ui/InlineLink' import { useProjectStorageConfigQuery } from '@/data/config/project-storage-config-query' import { useBucketUpdateMutation } from '@/data/storage/bucket-update-mutation' import { Bucket } from '@/data/storage/buckets-query' import { DOCS_URL, IS_PLATFORM } from '@/lib/constants' export interface EditBucketModalProps { visible: boolean bucket: Bucket onClose: () => void } const BucketSchema = z.object({ name: z.string(), 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(''), }) const formId = 'edit-storage-bucket-form' export const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalProps) => { const { ref } = useParams() const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM }) const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0) const formattedGlobalUploadLimit = `${value} ${unit}` const bucketIdRef = useRef(null) const [selectedUnit, setSelectedUnit] = useState(StorageSizeUnits.MB) const { value: fileSizeLimit } = convertFromBytes(bucket?.file_size_limit ?? 0) const { mutate: updateBucket, isPending: isUpdating } = useBucketUpdateMutation({ onSuccess: () => { toast.success(`Successfully updated bucket "${bucket?.name}"`) onClose() }, onError: (error) => { // Handle specific error cases for inline display const errorMessage = error.message?.toLowerCase() || '' if ( errorMessage.includes('exceeded the maximum allowed size') || errorMessage.includes('maximum allowed size') || errorMessage.includes('entity too large') || errorMessage.includes('payload too large') ) { // Set form error for the file size limit field form.setError('formatted_size_limit', { type: 'manual', message: `Exceeds global limit of ${formattedGlobalUploadLimit}.`, }) } else 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 update bucket: ${error.message || 'Unknown error'}`) } }, }) const defaultValues = { name: bucket?.name ?? '', public: bucket?.public, has_file_size_limit: Boolean(bucket?.file_size_limit), formatted_size_limit: bucket?.file_size_limit ? (fileSizeLimit ?? 0) : undefined, allowed_mime_types: (bucket?.allowed_mime_types ?? []).join(', '), } const form = useForm>({ resolver: zodResolver(BucketSchema as any), defaultValues, values: defaultValues, mode: 'onSubmit', }) const { formatted_size_limit: formattedSizeLimitError } = form.formState.errors const isPublicBucket = form.watch('public') const hasFileSizeLimit = form.watch('has_file_size_limit') const [hasAllowedMimeTypes, setHasAllowedMimeTypes] = useState( Boolean(bucket?.allowed_mime_types?.length) ) const isChangingBucketVisibility = bucket?.public !== isPublicBucket const isMakingBucketPrivate = bucket?.public && !isPublicBucket const isMakingBucketPublic = !bucket?.public && isPublicBucket const closeModal = () => { form.reset() onClose() } const onSubmit: SubmitHandler> = async (values) => { if (bucket === undefined) return console.error('Bucket is required') if (ref === undefined) return console.error('Project ref is required') // Client-side validation: Check if bucket limit exceeds global limit // [Joshen] Should shift this into superRefine in the form schema if ( values.has_file_size_limit && values.formatted_size_limit !== undefined && data?.fileSizeLimit ) { const bucketLimitInBytes = convertToBytes( values.formatted_size_limit, selectedUnit as StorageSizeUnits ) if (bucketLimitInBytes > data.fileSizeLimit) { return form.setError('formatted_size_limit', { type: 'manual', message: 'exceed_global_limit', }) } } updateBucket({ projectRef: ref, id: bucket.id, isPublic: values.public, file_size_limit: values.has_file_size_limit && values.formatted_size_limit ? convertToBytes(values.formatted_size_limit, selectedUnit as StorageSizeUnits) : null, allowed_mime_types: hasAllowedMimeTypes ? values.allowed_mime_types.length > 0 ? values.allowed_mime_types.split(',').map((x: string) => x.trim()) : null : null, }) } useEffect(() => { if (visible && bucket) { // Only set the selectedUnit when the bucket changes (different bucket ID) // This preserves the user's unit selection when reopening the modal for the same bucket if (bucketIdRef.current !== bucket.id && bucket.file_size_limit) { const { unit } = convertFromBytes(bucket.file_size_limit) setSelectedUnit(unit) bucketIdRef.current = bucket.id } } }, [visible, bucket, form]) return ( { if (!open) closeModal() }} > {`Edit bucket “${bucket?.name}”`}
( )} />
( )} /> {isChangingBucketVisibility && ( {isMakingBucketPublic && (

This will make all objects in your bucket publicly accessible.

)} {isMakingBucketPrivate && ( <>

All objects in your bucket will only accessible via signed URLs, or downloaded with the right authorization headers.

Assets cached in the CDN may still be publicly accessible. You can consider{' '} purging the cache {' '} or moving your assets to a new bucket.

)} } /> )}
( )} /> {hasFileSizeLimit && (
(
)} /> {formattedSizeLimitError?.message === 'exceed_global_limit' && ( Exceeds global limit of {formattedGlobalUploadLimit}. Increase limit in{' '} Storage Settings {' '} first. )} {IS_PLATFORM && (

This project has a{' '} global file size limit {' '} of {formattedGlobalUploadLimit}.

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