ImportForeignSchemaDialog.tsx213 lines · main
| 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 | |
| 11 | import { formatWrapperTables } from '../Integrations/Wrappers/Wrappers.utils' |
| 12 | import { SchemaEditor } from '../TableGridEditor/SidePanelEditor/SchemaEditor' |
| 13 | import { getAnalyticsBucketFDWServerName } from './AnalyticsBuckets/AnalyticsBucketDetails/AnalyticsBucketDetails.utils' |
| 14 | import { useAnalyticsBucketAssociatedEntities } from './AnalyticsBuckets/AnalyticsBucketDetails/useAnalyticsBucketAssociatedEntities' |
| 15 | import { getDecryptedParameters } from './Storage.utils' |
| 16 | import { useSchemaCreateMutation } from '@/data/database/schema-create-mutation' |
| 17 | import { useSchemasQuery } from '@/data/database/schemas-query' |
| 18 | import { useFDWImportForeignSchemaMutation } from '@/data/fdw/fdw-import-foreign-schema-mutation' |
| 19 | import { useFDWUpdateMutation } from '@/data/fdw/fdw-update-mutation' |
| 20 | import { getFDWs } from '@/data/fdw/fdws-query' |
| 21 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 22 | |
| 23 | export interface ImportForeignSchemaDialogProps { |
| 24 | namespace: string |
| 25 | circumstance?: 'fresh' | 'clash' |
| 26 | visible: boolean |
| 27 | onClose: () => void |
| 28 | } |
| 29 | |
| 30 | export 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 | } |