import { zodResolver } from '@hookform/resolvers/zod' import { snakeCase } from 'lodash' import { ChevronDown } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import z from 'zod' import { useParams } from 'common' import { useIcebergWrapperExtension } from 'components/interfaces/Storage/AnalyticBucketDetails/useIcebergWrapper' 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 { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-create-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { BASE_PATH, IS_PLATFORM } from 'lib/constants' import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Button, cn, Collapsible, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_, Label_Shadcn_, Listbox, Modal, RadioGroupStacked, RadioGroupStackedItem, Toggle, WarningIcon, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { convertFromBytes, convertToBytes } from './StorageSettings/StorageSettings.utils' export interface CreateBucketModalProps { visible: boolean onClose: () => void } const FormSchema = z.object({ name: z .string() .trim() .min(1, 'Please provide a name for your bucket') .regex( /^[a-z0-9.-]+$/, 'The name of the bucket must only contain lowercase letters, numbers, dots, and hyphens' ) .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' ), type: z.enum(['STANDARD', 'ANALYTICS']).default('STANDARD'), 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') .default(0), allowed_mime_types: z.string().trim().default(''), }) export type CreateBucketForm = z.infer const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => { const { ref } = useParams() const org = useSelectedOrganization() const { mutate: sendEvent } = useSendEventMutation() const router = useRouter() const { mutateAsync: createBucket, isLoading: isCreating } = useBucketCreateMutation({ // [Joshen] Silencing the error here as it's being handled in onSubmit onError: () => {}, }) const { mutateAsync: createIcebergWrapper, isLoading: isCreatingIcebergWrapper } = useIcebergWrapperCreateMutation() const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM }) const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0) const formattedGlobalUploadLimit = `${value} ${unit}` const [selectedUnit, setSelectedUnit] = useState(StorageSizeUnits.BYTES) const [showConfiguration, setShowConfiguration] = useState(false) const form = useForm({ resolver: zodResolver(FormSchema), defaultValues: { name: '', public: false, type: 'STANDARD', has_file_size_limit: false, formatted_size_limit: 0, allowed_mime_types: '', }, }) const bucketName = snakeCase(form.watch('name')) const isPublicBucket = form.watch('public') const isStandardBucket = form.watch('type') === 'STANDARD' const hasFileSizeLimit = form.watch('has_file_size_limit') const formattedSizeLimit = form.watch('formatted_size_limit') const icebergWrapperExtensionState = useIcebergWrapperExtension() const onSubmit: SubmitHandler = async (values) => { if (!ref) return console.error('Project ref is required') if (values.type === 'ANALYTICS' && !icebergCatalogEnabled) { toast.error( 'The Analytics catalog feature is not enabled for your project. Please contact support to enable it.' ) return } try { const fileSizeLimit = values.has_file_size_limit ? convertToBytes(values.formatted_size_limit, selectedUnit) : undefined const allowedMimeTypes = values.allowed_mime_types.length > 0 ? values.allowed_mime_types.split(',').map((x) => x.trim()) : undefined await createBucket({ projectRef: ref, id: values.name, type: values.type, isPublic: values.public, file_size_limit: fileSizeLimit, allowed_mime_types: allowedMimeTypes, }) sendEvent({ action: 'storage_bucket_created', properties: { bucketType: values.type }, groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) if (values.type === 'ANALYTICS' && icebergWrapperExtensionState === 'installed') { await createIcebergWrapper({ bucketName: values.name }) } toast.success(`Successfully created bucket ${values.name}`) router.push(`/project/${ref}/storage/buckets/${values.name}`) onClose() } catch (error: any) { toast.error(`Failed to create bucket: ${error.message}`) } } useEffect(() => { if (visible) { form.reset() setSelectedUnit(StorageSizeUnits.BYTES) setShowConfiguration(false) } }, [visible, form]) const icebergCatalogEnabled = data?.features?.icebergCatalog?.enabled return ( onClose()} >
( )} />
( field.onChange(v)} value={field.value} >

Compatible with S3 buckets.

{IS_PLATFORM && (

Stores Iceberg files and is optimized for analytical workloads.

{!icebergCatalogEnabled && (
This is currently in alpha and not enabled for your project. Sign up{' '} here .
)}
)}
)} />
{isStandardBucket ? ( <>
( )} /> {isPublicBucket && (

Users can read objects in public buckets without any authorization.

Row level security (RLS) policies are still required for other operations such as object uploads and deletes.

)}
setShowConfiguration(!showConfiguration)} >

Additional restrictions

( )} /> {hasFileSizeLimit && (
( { if (event.charCode < 48 || event.charCode > 57) { event.preventDefault() } }} /> Equivalent to{' '} {convertToBytes( formattedSizeLimit, selectedUnit ).toLocaleString()}{' '} bytes. )} />
{Object.values(StorageSizeUnits).map((unit: string) => (
{unit}
))}
{IS_PLATFORM && (

Note: Individual bucket uploads will still be capped at the{' '} global upload limit {' '} of {formattedGlobalUploadLimit}

)}
)}
( )} />
) : ( {icebergWrapperExtensionState === 'installed' ? (

Supabase will setup a foreign data wrapper {bucketName && {`${bucketName}_fdw`}} {' '} for easier access to the data. This action will also create{' '} S3 Access Keys {bucketName && ( <> {' '} named {`${bucketName}_keys`} )} and four Vault Secrets {bucketName && ( <> {' '} prefixed with{' '} {`${bucketName}_vault_`} )} .

As a final step, you'll need to create an{' '} Iceberg namespace before you connect the Iceberg data to your database.

) : ( You need to install the Iceberg wrapper extension to connect your Analytic bucket to your database.

You need to install the wrappers extension (with the minimum version of 0.5.3) if you want to connect your Analytics bucket to your database.

)}
)}
) } export default CreateBucketModal