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