| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- // @ts-nocheck
- import { zodResolver } from '@hookform/resolvers/zod'
- import { useParams } from 'common'
- import { SubmitHandler, useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Button,
- cn,
- DialogFooter,
- DialogSection,
- Form,
- FormControl,
- FormField,
- Input,
- SheetFooter,
- SheetSection,
- } from 'ui'
- import { Admonition } from 'ui-patterns'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import z from 'zod'
- import { useIcebergWrapperExtension } from './AnalyticsBucketDetails/useIcebergWrapper'
- import {
- reservedPrefixes,
- reservedSuffixes,
- validBucketNameRegex,
- } from './CreateAnalyticsBucketForm.utils'
- import { InlineLink } from '@/components/ui/InlineLink'
- import { useDatabaseExtensionEnableMutation } from '@/data/database-extensions/database-extension-enable-mutation'
- import { useAnalyticsBucketCreateMutation } from '@/data/storage/analytics-bucket-create-mutation'
- import { useAnalyticsBucketsQuery } from '@/data/storage/analytics-buckets-query'
- import { useIcebergWrapperCreateMutation } from '@/data/storage/iceberg-wrapper-create-mutation'
- import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { DOCS_URL } from '@/lib/constants'
- const FormSchema = z
- .object({
- name: z
- .string()
- .trim()
- .min(3, 'Bucket name should be at least 3 characters')
- .max(63, 'Bucket name should be up to 63 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'
- ),
- })
- .superRefine((data, ctx) => {
- if (reservedPrefixes.test(data.name)) {
- const [match] = data.name.match(reservedPrefixes) ?? []
- return ctx.addIssue({
- path: ['name'],
- code: z.ZodIssueCode.custom,
- message: `Bucket name cannot start with "${match}"`,
- })
- }
- if (reservedSuffixes.test(data.name)) {
- const [match] = data.name.match(reservedSuffixes) ?? []
- return ctx.addIssue({
- path: ['name'],
- code: z.ZodIssueCode.custom,
- message: `Bucket name cannot end with "${match}"`,
- })
- }
- if (/[A-Z]/.test(data.name)) {
- return ctx.addIssue({
- path: ['name'],
- code: z.ZodIssueCode.custom,
- message: 'Bucket name can only be lowercase characters',
- })
- }
- if (!validBucketNameRegex.test(data.name)) {
- if (!/^[a-z0-9]/.test(data.name)) {
- return ctx.addIssue({
- path: ['name'],
- code: z.ZodIssueCode.custom,
- message: 'Bucket name must start with a lowercase letter or number.',
- })
- }
- if (!/[a-z0-9]$/.test(data.name)) {
- return ctx.addIssue({
- path: ['name'],
- code: z.ZodIssueCode.custom,
- message: 'Bucket name must end with a lowercase letter or number.',
- })
- }
- const [match] = data.name.match(/[^a-z0-9-]/) ?? []
- return 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-analytics-storage-bucket-form'
- export type CreateAnalyticsBucketForm = z.infer<typeof FormSchema>
- interface CreateAnalyticsBucketFormProps {
- type?: 'dialog' | 'sheet'
- onOpenChange: (value: boolean) => void
- }
- export const CreateAnalyticsBucketForm = ({
- type = 'dialog',
- onOpenChange,
- }: CreateAnalyticsBucketFormProps) => {
- const { ref } = useParams()
- const { data: org } = useSelectedOrganizationQuery()
- const { data: project } = useSelectedProjectQuery()
- const { extension: wrappersExtension, state: wrappersExtensionState } =
- useIcebergWrapperExtension()
- const { data: buckets = [] } = useAnalyticsBucketsQuery({ projectRef: ref })
- const wrappersExtensionNeedsUpgrading = wrappersExtensionState === 'needs-upgrade'
- const { mutate: sendEvent } = useSendEventMutation()
- const { mutateAsync: createAnalyticsBucket, isPending: isCreatingAnalyticsBucket } =
- useAnalyticsBucketCreateMutation({
- // [Joshen] Silencing the error here as it's being handled in onSubmit
- onError: () => {},
- })
- const { mutateAsync: createIcebergWrapper, isPending: isCreatingIcebergWrapper } =
- useIcebergWrapperCreateMutation()
- const { mutateAsync: enableExtension, isPending: isEnablingExtension } =
- useDatabaseExtensionEnableMutation()
- const isCreating = isEnablingExtension || isCreatingIcebergWrapper || isCreatingAnalyticsBucket
- const form = useForm<CreateAnalyticsBucketForm>({
- resolver: zodResolver(FormSchema as any),
- defaultValues: { name: '' },
- })
- const onSubmit: SubmitHandler<CreateAnalyticsBucketForm> = async (values) => {
- if (!ref) return console.error('Project ref is required')
- if (!project) return console.error('Project details is required')
- if (!wrappersExtension) return console.error('Unable to find wrappers extension')
- const hasExistingBucket = buckets.some((x) => x.name === values.name)
- if (hasExistingBucket) return toast.error('Bucket name already exists')
- try {
- await createAnalyticsBucket({
- projectRef: ref,
- bucketName: values.name,
- })
- if (wrappersExtensionState === 'not-installed') {
- await enableExtension({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- name: wrappersExtension.name,
- schema: wrappersExtension.schema ?? 'extensions',
- version: wrappersExtension.default_version,
- })
- }
- await createIcebergWrapper({ bucketName: values.name })
- sendEvent({
- action: 'storage_bucket_created',
- properties: { bucketType: 'analytics' },
- groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
- })
- form.reset()
- toast.success(`Created bucket “${values.name}”`)
- onOpenChange(false)
- } catch (error: any) {
- toast.error(`Failed to create bucket: ${error.message}`)
- }
- }
- const Section = type === 'dialog' ? DialogSection : SheetSection
- const Footer = type === 'dialog' ? DialogFooter : SheetFooter
- return (
- <>
- <Section className="flex flex-col p-0! grow">
- <Form {...form}>
- <form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
- <FormField
- key="name"
- name="name"
- control={form.control}
- render={({ field }) => (
- <FormItemLayout
- name="name"
- className="p-5"
- label="Bucket name"
- labelOptional="Cannot be changed after creation"
- description="Must be between 3 – 63 characters. Only lowercase letters, numbers, and hyphens are allowed."
- >
- <FormControl>
- <Input
- id="name"
- data-1p-ignore
- data-lpignore="true"
- data-form-type="other"
- data-bwignore
- {...field}
- placeholder="Enter bucket name"
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- {wrappersExtensionNeedsUpgrading ? (
- <Admonition
- type="warning"
- className={cn('border-x-0 rounded-none', type === 'dialog' && 'border-b-0')}
- title="Wrappers extension must be updated for Iceberg Wrapper support"
- >
- <p className="prose max-w-full text-sm leading-normal!">
- Update the <code className="text-code-inline">wrappers</code> extension by
- upgrading your project from your{' '}
- <InlineLink href={`/project/${ref}/settings/infrastructure`}>
- project settings
- </InlineLink>{' '}
- before creating an Analytics bucket.{' '}
- <InlineLink href={`${DOCS_URL}/guides/database/extensions/wrappers/iceberg`}>
- Learn more
- </InlineLink>
- .
- </p>
- </Admonition>
- ) : (
- <Admonition
- type="default"
- className={cn('border-x-0 rounded-none', type === 'dialog' && 'border-b-0')}
- >
- <p className="leading-normal!">
- Briven will install the{' '}
- {wrappersExtensionState !== 'installed' ? 'Wrappers extension and ' : ''}
- Iceberg Wrapper integration on your behalf.{' '}
- <InlineLink href={`${DOCS_URL}/guides/database/extensions/wrappers/iceberg`}>
- Learn more
- </InlineLink>
- .
- </p>
- </Admonition>
- )}
- </form>
- </Form>
- </Section>
- <Footer>
- <Button type="default" disabled={isCreating} onClick={() => onOpenChange(false)}>
- Cancel
- </Button>
- <Button
- form={formId}
- htmlType="submit"
- loading={isCreating}
- disabled={wrappersExtensionNeedsUpgrading || isCreating}
- >
- Create bucket
- </Button>
- </Footer>
- </>
- )
- }
|