InitializeForeignSchemaDialog.tsx134 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { useParams } from 'common'
3import { useState } from 'react'
4import { SubmitHandler, useForm } from 'react-hook-form'
5import { toast } from 'sonner'
6import {
7 Button,
8 Dialog,
9 DialogContent,
10 DialogFooter,
11 DialogHeader,
12 DialogSection,
13 DialogSectionSeparator,
14 DialogTitle,
15 DialogTrigger,
16 Form,
17 FormField,
18 Input,
19} from 'ui'
20import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
21import z from 'zod'
22
23import { getAnalyticsBucketFDWServerName } from './AnalyticsBucketDetails.utils'
24import { DocsButton } from '@/components/ui/DocsButton'
25import { useSchemaCreateMutation } from '@/data/database/schema-create-mutation'
26import { useSchemasQuery } from '@/data/database/schemas-query'
27import { useFDWImportForeignSchemaMutation } from '@/data/fdw/fdw-import-foreign-schema-mutation'
28import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
29import { DOCS_URL } from '@/lib/constants'
30
31// Create foreign tables for namespace tables
32export const InitializeForeignSchemaDialog = ({ namespace }: { namespace: string }) => {
33 const { ref: projectRef, bucketId } = useParams()
34 const { data: project } = useSelectedProjectQuery()
35 const { data: schemas } = useSchemasQuery({ projectRef })
36
37 const [isOpen, setIsOpen] = useState(false)
38 const [isCreating, setIsCreating] = useState(false)
39
40 const serverName = getAnalyticsBucketFDWServerName(bucketId ?? '')
41
42 const FormSchema = z.object({
43 schema: z
44 .string()
45 .trim()
46 .min(1, 'Schema name is required')
47 .refine((val) => !schemas?.find((s) => s.name === val), {
48 message: 'This schema already exists. Please specify a unique schema name.',
49 }),
50 })
51 const form = useForm<z.infer<typeof FormSchema>>({
52 resolver: zodResolver(FormSchema as any),
53 defaultValues: { schema: '' },
54 })
55
56 const { mutateAsync: createSchema } = useSchemaCreateMutation()
57 const { mutateAsync: importForeignSchema } = useFDWImportForeignSchemaMutation()
58
59 const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
60 if (!projectRef) return console.error('Project ref is required')
61
62 try {
63 setIsCreating(true)
64
65 await createSchema({
66 projectRef,
67 connectionString: project?.connectionString,
68 name: values.schema,
69 })
70
71 await importForeignSchema({
72 projectRef,
73 connectionString: project?.connectionString,
74 serverName: serverName,
75 sourceSchema: namespace,
76 targetSchema: values.schema,
77 })
78
79 toast.success(
80 `Successfully created "${values.schema}" schema! Data from tables in the "${namespace}" namespace can now be queried from there.`
81 )
82 setIsOpen(false)
83 } catch (error: any) {
84 toast.error(`Failed to expose tables: ${error.message}`)
85 } finally {
86 setIsCreating(false)
87 }
88 }
89
90 return (
91 <Dialog open={isOpen} onOpenChange={setIsOpen}>
92 <DialogTrigger asChild>
93 <Button type="default">Query from Postgres</Button>
94 </DialogTrigger>
95 <DialogContent size="medium" aria-describedby={undefined}>
96 <Form {...form}>
97 <form onSubmit={form.handleSubmit(onSubmit)}>
98 <DialogHeader>
99 <DialogTitle>Query this namespace from Postgres</DialogTitle>
100 </DialogHeader>
101 <DialogSectionSeparator />
102 <DialogSection className="flex flex-col gap-y-4">
103 <p className="text-sm">
104 Iceberg data can be queried from Postgres with the Iceberg Foreign Data Wrapper.
105 Create a Postgres schema to expose tables from the "{namespace}" namespace as
106 foreign tables.
107 </p>
108 <FormField
109 control={form.control}
110 name="schema"
111 render={({ field }) => (
112 <FormItemLayout layout="vertical" label="Schema name">
113 <Input {...field} placeholder="Provide a name for your schema" />
114 </FormItemLayout>
115 )}
116 />
117 </DialogSection>
118 <DialogFooter className="justify-between!">
119 <DocsButton href={`${DOCS_URL}/guides/storage/analytics/query-with-postgres`} />
120 <div className="flex items-center gap-x-2">
121 <Button type="default" disabled={isCreating} onClick={() => setIsOpen(false)}>
122 Cancel
123 </Button>
124 <Button htmlType="submit" type="primary" loading={isCreating}>
125 Create schema
126 </Button>
127 </div>
128 </DialogFooter>
129 </form>
130 </Form>
131 </DialogContent>
132 </Dialog>
133 )
134}