ImportForeignSchemaDialog.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { useParams } from 'common'
  3. import { uniq } from 'lodash'
  4. import { useEffect, useState } from 'react'
  5. import { SubmitHandler, useForm } from 'react-hook-form'
  6. import { toast } from 'sonner'
  7. import { Button, Form, FormField, Input, Modal, Separator } from 'ui'
  8. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  9. import z from 'zod'
  10. import { formatWrapperTables } from '../Integrations/Wrappers/Wrappers.utils'
  11. import { SchemaEditor } from '../TableGridEditor/SidePanelEditor/SchemaEditor'
  12. import { getAnalyticsBucketFDWServerName } from './AnalyticsBuckets/AnalyticsBucketDetails/AnalyticsBucketDetails.utils'
  13. import { useAnalyticsBucketAssociatedEntities } from './AnalyticsBuckets/AnalyticsBucketDetails/useAnalyticsBucketAssociatedEntities'
  14. import { getDecryptedParameters } from './Storage.utils'
  15. import { useSchemaCreateMutation } from '@/data/database/schema-create-mutation'
  16. import { useSchemasQuery } from '@/data/database/schemas-query'
  17. import { useFDWImportForeignSchemaMutation } from '@/data/fdw/fdw-import-foreign-schema-mutation'
  18. import { useFDWUpdateMutation } from '@/data/fdw/fdw-update-mutation'
  19. import { getFDWs } from '@/data/fdw/fdws-query'
  20. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  21. export interface ImportForeignSchemaDialogProps {
  22. namespace: string
  23. circumstance?: 'fresh' | 'clash'
  24. visible: boolean
  25. onClose: () => void
  26. }
  27. export const ImportForeignSchemaDialog = ({
  28. namespace,
  29. visible,
  30. onClose,
  31. circumstance = 'fresh',
  32. }: ImportForeignSchemaDialogProps) => {
  33. const { ref, bucketId: bucketName } = useParams()
  34. const { data: project } = useSelectedProjectQuery()
  35. const [loading, setLoading] = useState(false)
  36. const [createSchemaSheetOpen, setCreateSchemaSheetOpen] = useState(false)
  37. const { data: schemas } = useSchemasQuery({ projectRef: project?.ref! })
  38. const { icebergWrapperMeta: wrapperMeta } = useAnalyticsBucketAssociatedEntities({
  39. projectRef: ref,
  40. bucketId: bucketName,
  41. })
  42. const { mutateAsync: createSchema } = useSchemaCreateMutation()
  43. const { mutateAsync: importForeignSchema } = useFDWImportForeignSchemaMutation({})
  44. const { mutateAsync: updateFDW } = useFDWUpdateMutation({
  45. onSuccess: () => {
  46. toast.success(`Successfully connected “${bucketName}” to the database.`)
  47. onClose()
  48. },
  49. })
  50. const FormSchema = z.object({
  51. bucketName: z.string().trim(),
  52. sourceNamespace: z.string().trim(),
  53. targetSchema: z
  54. .string()
  55. .trim()
  56. .min(1, 'Schema name is required')
  57. .refine(
  58. (val) => {
  59. return !schemas?.find((s) => s.name === val)
  60. },
  61. {
  62. message: 'This schema already exists. Please specify a unique schema name.',
  63. }
  64. ),
  65. })
  66. const form = useForm<z.infer<typeof FormSchema>>({
  67. resolver: zodResolver(FormSchema as any),
  68. defaultValues: {
  69. bucketName,
  70. sourceNamespace: namespace,
  71. targetSchema: `fdw_analytics_${namespace}`,
  72. },
  73. })
  74. const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
  75. const serverName = getAnalyticsBucketFDWServerName(values.bucketName)
  76. if (!ref) return console.error('Project ref is required')
  77. if (!wrapperMeta) return console.error('Wrapper meta is required')
  78. setLoading(true)
  79. try {
  80. await createSchema({
  81. projectRef: ref,
  82. connectionString: project?.connectionString,
  83. name: values.targetSchema,
  84. })
  85. await importForeignSchema({
  86. projectRef: ref,
  87. connectionString: project?.connectionString,
  88. serverName: serverName,
  89. sourceSchema: values.sourceNamespace,
  90. targetSchema: values.targetSchema,
  91. })
  92. const FDWs = await getFDWs({ projectRef: ref, connectionString: project?.connectionString })
  93. const wrapper = FDWs.find((fdw) => fdw.server_name === serverName)
  94. if (!wrapper) {
  95. throw new Error(`Foreign data wrapper with server name ${serverName} not found`)
  96. }
  97. const serverOptions = await getDecryptedParameters({
  98. ref: project?.ref,
  99. connectionString: project?.connectionString ?? undefined,
  100. wrapper,
  101. wrapperMeta,
  102. })
  103. const formValues: Record<string, string> = {
  104. wrapper_name: wrapper.name,
  105. server_name: wrapper.server_name,
  106. ...serverOptions,
  107. }
  108. const targetSchemas = (formValues['briven_target_schema'] || '')
  109. .split(',')
  110. .map((s) => s.trim())
  111. const wrapperTables = formatWrapperTables(wrapper, wrapperMeta)
  112. await updateFDW({
  113. projectRef: project?.ref,
  114. connectionString: project?.connectionString,
  115. wrapper: wrapper,
  116. wrapperMeta: wrapperMeta,
  117. formState: {
  118. ...formValues,
  119. server_name: serverName,
  120. briven_target_schema: uniq([...targetSchemas, values.targetSchema])
  121. .filter(Boolean)
  122. .join(','),
  123. },
  124. tables: wrapperTables,
  125. })
  126. } catch (error: any) {
  127. // error will be handled by the mutation onError callback
  128. } finally {
  129. setLoading(false)
  130. }
  131. }
  132. useEffect(() => {
  133. if (visible) {
  134. form.reset({
  135. bucketName,
  136. sourceNamespace: namespace,
  137. targetSchema: `fdw_analytics_${namespace}`,
  138. })
  139. }
  140. }, [visible, form, bucketName, namespace])
  141. return (
  142. <Modal
  143. hideFooter
  144. visible={visible}
  145. size="medium"
  146. header={<span>Create target schema</span>}
  147. onCancel={() => onClose()}
  148. >
  149. <Form {...form}>
  150. <form onSubmit={form.handleSubmit(onSubmit)}>
  151. <Modal.Content className="flex flex-col gap-y-4">
  152. <p className="text-sm">
  153. Namespace “<strong>{namespace}</strong>”{' '}
  154. {circumstance === 'fresh'
  155. ? 'must be linked to a new schema before tables can be paired.'
  156. : 'clashes with an existing database schema. Create a new schema to use as the destination for this data.'}
  157. </p>
  158. <Separator />
  159. <FormField
  160. control={form.control}
  161. name="targetSchema"
  162. render={({ field }) => (
  163. <FormItemLayout
  164. layout="vertical"
  165. label="Target schema"
  166. description="Where your analytics tables will be stored."
  167. >
  168. <Input {...field} placeholder="Enter schema name" />
  169. </FormItemLayout>
  170. )}
  171. />
  172. </Modal.Content>
  173. <Modal.Separator />
  174. <Modal.Content className="flex items-center space-x-2 justify-end">
  175. <Button type="default" htmlType="button" disabled={loading} onClick={onClose}>
  176. Cancel
  177. </Button>
  178. <Button type="primary" htmlType="submit" loading={loading}>
  179. Create
  180. </Button>
  181. </Modal.Content>
  182. </form>
  183. </Form>
  184. <SchemaEditor
  185. visible={createSchemaSheetOpen}
  186. closePanel={() => setCreateSchemaSheetOpen(false)}
  187. onSuccess={(schema) => {
  188. form.setValue('targetSchema', schema)
  189. setCreateSchemaSheetOpen(false)
  190. }}
  191. />
  192. </Modal>
  193. )
  194. }