CreateVectorBucketDialog.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. // @ts-nocheck
  2. import { zodResolver } from '@hookform/resolvers/zod'
  3. import { useParams } from 'common'
  4. import { useEffect, useState } from 'react'
  5. import { SubmitHandler, useForm } from 'react-hook-form'
  6. import { toast } from 'sonner'
  7. import {
  8. Button,
  9. Dialog,
  10. DialogContent,
  11. DialogFooter,
  12. DialogHeader,
  13. DialogSection,
  14. DialogSectionSeparator,
  15. DialogTitle,
  16. Form,
  17. FormControl,
  18. FormField,
  19. Input,
  20. } from 'ui'
  21. import { Admonition } from 'ui-patterns/admonition'
  22. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  23. import z from 'zod'
  24. import { validVectorBucketName } from './CreateVectorBucketDialog.utils'
  25. import { useS3VectorsWrapperExtension } from './useS3VectorsWrapper'
  26. import { InlineLink } from '@/components/ui/InlineLink'
  27. import { useDatabaseExtensionEnableMutation } from '@/data/database-extensions/database-extension-enable-mutation'
  28. import { useS3VectorsWrapperCreateMutation } from '@/data/storage/s3-vectors-wrapper-create-mutation'
  29. import { useVectorBucketCreateMutation } from '@/data/storage/vector-bucket-create-mutation'
  30. import { useVectorBucketsQuery } from '@/data/storage/vector-buckets-query'
  31. import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
  32. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  33. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  34. import { DOCS_URL } from '@/lib/constants'
  35. const FormSchema = z.object({
  36. name: z
  37. .string()
  38. .trim()
  39. .min(3, 'Bucket name should be at least 3 characters')
  40. .max(63, 'Bucket name should be up to 63 characters')
  41. .superRefine((name, ctx) => {
  42. if (!validVectorBucketName.test(name)) {
  43. if (/[A-Z]/.test(name)) {
  44. return ctx.addIssue({
  45. path: [],
  46. code: z.ZodIssueCode.custom,
  47. message: 'Bucket name can only be lowercase characters',
  48. })
  49. }
  50. if (!/^[a-z0-9]/.test(name)) {
  51. return ctx.addIssue({
  52. path: [],
  53. code: z.ZodIssueCode.custom,
  54. message: 'Bucket name must start with a lowercase letter or number.',
  55. })
  56. }
  57. if (!/[a-z0-9]$/.test(name)) {
  58. return ctx.addIssue({
  59. path: [],
  60. code: z.ZodIssueCode.custom,
  61. message: 'Bucket name must end with a lowercase letter or number.',
  62. })
  63. }
  64. const [match] = name.match(/[^a-z0-9-]/) ?? []
  65. return ctx.addIssue({
  66. path: [],
  67. code: z.ZodIssueCode.custom,
  68. message: !!match
  69. ? `Bucket name cannot contain the "${match}" character`
  70. : 'Bucket name contains an invalid special character',
  71. })
  72. }
  73. }),
  74. })
  75. const formId = 'create-storage-bucket-form'
  76. export type CreateBucketForm = z.infer<typeof FormSchema>
  77. export const CreateVectorBucketDialog = ({
  78. visible,
  79. setVisible,
  80. }: {
  81. visible: boolean
  82. setVisible: (visible: boolean) => void
  83. }) => {
  84. const { ref } = useParams()
  85. const { data: org } = useSelectedOrganizationQuery()
  86. const { data: project } = useSelectedProjectQuery()
  87. const [isLoading, setIsLoading] = useState(false)
  88. const { data } = useVectorBucketsQuery({ projectRef: ref })
  89. const form = useForm<CreateBucketForm>({
  90. resolver: zodResolver(FormSchema as any),
  91. defaultValues: { name: '' },
  92. })
  93. const { mutate: sendEvent } = useSendEventMutation()
  94. const { mutateAsync: createVectorBucket } = useVectorBucketCreateMutation({
  95. onError: () => {},
  96. })
  97. const { extension: wrappersExtension, state: wrappersExtensionState } =
  98. useS3VectorsWrapperExtension()
  99. const { mutateAsync: createS3VectorsWrapper } = useS3VectorsWrapperCreateMutation()
  100. const { mutateAsync: enableExtension } = useDatabaseExtensionEnableMutation()
  101. const onSubmit: SubmitHandler<CreateBucketForm> = async (values) => {
  102. if (!ref) return console.error('Project ref is required')
  103. const hasExistingBucket = (data?.vectorBuckets ?? []).some(
  104. (x) => x.vectorBucketName === values.name
  105. )
  106. if (hasExistingBucket) return toast.error('Bucket name already exists')
  107. setIsLoading(true)
  108. try {
  109. await createVectorBucket({ projectRef: ref, bucketName: values.name })
  110. } catch (error: any) {
  111. toast.error(`Failed to create vector bucket: ${error.message}`)
  112. setIsLoading(false)
  113. return
  114. }
  115. try {
  116. if (!wrappersExtension) throw new Error('Unable to find wrappers extension.')
  117. if (wrappersExtensionState === 'not-installed') {
  118. // when it's not installed, we need to enable the extension and create the wrapper
  119. await enableExtension({
  120. projectRef: project?.ref!,
  121. connectionString: project?.connectionString,
  122. name: wrappersExtension.name,
  123. schema: wrappersExtension.schema ?? 'extensions',
  124. version: wrappersExtension.default_version,
  125. })
  126. }
  127. await createS3VectorsWrapper({ bucketName: values.name })
  128. } catch (error: any) {
  129. toast.warning(
  130. `Failed to create vector bucket integration: ${error.message}. The bucket will be created but you will need to manually install the integration.`
  131. )
  132. }
  133. setIsLoading(false)
  134. sendEvent({
  135. action: 'storage_bucket_created',
  136. properties: { bucketType: 'vector' },
  137. groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
  138. })
  139. toast.success(`Successfully created vector bucket ${values.name}`)
  140. form.reset()
  141. setVisible(false)
  142. }
  143. useEffect(() => {
  144. if (!visible) form.reset()
  145. }, [visible, form])
  146. return (
  147. <Dialog open={visible} onOpenChange={setVisible}>
  148. <DialogContent>
  149. <DialogHeader>
  150. <DialogTitle>Create vector bucket</DialogTitle>
  151. </DialogHeader>
  152. <DialogSectionSeparator />
  153. <Form {...form}>
  154. <form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
  155. <DialogSection className="flex flex-col p-0!">
  156. <FormField
  157. key="name"
  158. name="name"
  159. control={form.control}
  160. render={({ field }) => (
  161. <FormItemLayout
  162. name="name"
  163. label="Bucket name"
  164. className="px-5 py-5"
  165. labelOptional="Cannot be changed after creation"
  166. description="Must be between 3–63 characters. Only lowercase letters, numbers, and hyphens are allowed"
  167. >
  168. <FormControl>
  169. <Input
  170. id="name"
  171. data-1p-ignore
  172. data-lpignore="true"
  173. data-form-type="other"
  174. data-bwignore
  175. {...field}
  176. placeholder="Enter bucket name"
  177. />
  178. </FormControl>
  179. </FormItemLayout>
  180. )}
  181. />
  182. <Admonition type="default" className="border-x-0 border-b-0 rounded-none">
  183. <p>
  184. Briven will install the{' '}
  185. {wrappersExtensionState !== 'installed' ? 'Wrappers extension and ' : ''}
  186. S3 Vectors Wrapper integration on your behalf.{' '}
  187. <InlineLink href={`${DOCS_URL}/guides/database/extensions/wrappers/s3-vectors`}>
  188. Learn more
  189. </InlineLink>
  190. .
  191. </p>
  192. </Admonition>
  193. </DialogSection>
  194. </form>
  195. </Form>
  196. <DialogFooter>
  197. <Button type="default" disabled={isLoading} onClick={() => setVisible(false)}>
  198. Cancel
  199. </Button>
  200. <Button form={formId} htmlType="submit" loading={isLoading}>
  201. Create
  202. </Button>
  203. </DialogFooter>
  204. </DialogContent>
  205. </Dialog>
  206. )
  207. }