UpdateForeignSchemaDialog.tsx171 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 Select,
19 SelectContent,
20 SelectItem,
21 SelectTrigger,
22 SelectValue,
23 Tooltip,
24 TooltipContent,
25 TooltipTrigger,
26} from 'ui'
27import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
28import z from 'zod'
29
30import { getAnalyticsBucketFDWServerName } from './AnalyticsBucketDetails.utils'
31import { useAnalyticsBucketAssociatedEntities } from './useAnalyticsBucketAssociatedEntities'
32import { DocsButton } from '@/components/ui/DocsButton'
33import { InlineLinkClassName } from '@/components/ui/InlineLink'
34import { useFDWImportForeignSchemaMutation } from '@/data/fdw/fdw-import-foreign-schema-mutation'
35import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
36import { DOCS_URL } from '@/lib/constants'
37
38export const UpdateForeignSchemaDialog = ({
39 namespace,
40 tables,
41}: {
42 namespace: string
43 tables: string[]
44}) => {
45 const { ref: projectRef, bucketId } = useParams()
46 const { data: project } = useSelectedProjectQuery()
47
48 const [isOpen, setIsOpen] = useState(false)
49
50 const { icebergWrapper } = useAnalyticsBucketAssociatedEntities({ bucketId })
51 const connectedForeignTablesForNamespace = (icebergWrapper?.tables ?? []).filter((x) =>
52 x.options[0].startsWith(`table=${namespace}.`)
53 )
54 const schemasAssociatedWithNamespace = [
55 ...new Set(connectedForeignTablesForNamespace.map((x) => x.schema)),
56 ]
57
58 const serverName = getAnalyticsBucketFDWServerName(bucketId ?? '')
59
60 const FormSchema = z.object({
61 schema: z.string().trim().min(1, 'Schema name is required'),
62 })
63 const form = useForm<z.infer<typeof FormSchema>>({
64 resolver: zodResolver(FormSchema as any),
65 defaultValues: { schema: schemasAssociatedWithNamespace[0] },
66 values: { schema: schemasAssociatedWithNamespace[0] },
67 })
68
69 const { mutateAsync: importForeignSchema, isPending: isUpdating } =
70 useFDWImportForeignSchemaMutation()
71
72 const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
73 if (!projectRef) return console.error('Project ref is required')
74
75 try {
76 await importForeignSchema({
77 projectRef,
78 connectionString: project?.connectionString,
79 serverName: serverName,
80 sourceSchema: namespace,
81 targetSchema: values.schema,
82 })
83
84 toast.success(`Successfully updated "${values.schema}" schema!`)
85 setIsOpen(false)
86 } catch (error: any) {
87 toast.error(`Failed to update schema: ${error.message}`)
88 }
89 }
90
91 return (
92 <Dialog open={isOpen} onOpenChange={setIsOpen}>
93 <DialogTrigger asChild>
94 <Button type="default">Update schema tables</Button>
95 </DialogTrigger>
96 <DialogContent size="medium" aria-describedby={undefined}>
97 <Form {...form}>
98 <form onSubmit={form.handleSubmit(onSubmit)}>
99 <DialogHeader>
100 <DialogTitle>Update schema to expose foreign tables</DialogTitle>
101 </DialogHeader>
102 <DialogSectionSeparator />
103 <DialogSection className="flex flex-col gap-y-4">
104 <p className="text-sm">
105 {tables.length > 1 ? (
106 <>
107 There are{' '}
108 <Tooltip>
109 <TooltipTrigger>
110 <span className={InlineLinkClassName}>{tables.length} tables</span>
111 </TooltipTrigger>
112 <TooltipContent className="max-w-64 text-center" side="bottom">
113 {tables.join(', ')}
114 </TooltipContent>
115 </Tooltip>{' '}
116 that aren't included in the Iceberg Foreign Data Wrapper. Update the wrapper to
117 create foreign tables for all unexposed tables. This will let you query the
118 tables from Postgres.
119 </>
120 ) : (
121 `The table "${tables[0]}" in the "${namespace}" namespace is not yet included in the Iceberg Foreign Data Wrapper. The schema can be updated to expose this table as a foreign table.`
122 )}
123 </p>
124
125 {schemasAssociatedWithNamespace.length > 1 ? (
126 <FormField
127 control={form.control}
128 name="schema"
129 render={({ field }) => (
130 <FormItemLayout
131 layout="vertical"
132 label="Select which Postgres schema to update"
133 >
134 <Select value={field.value} onValueChange={(val) => field.onChange(val)}>
135 <SelectTrigger>
136 <SelectValue />
137 </SelectTrigger>
138 <SelectContent>
139 {schemasAssociatedWithNamespace.map((x) => (
140 <SelectItem key={x} value={x}>
141 {x}
142 </SelectItem>
143 ))}
144 </SelectContent>
145 </Select>
146 </FormItemLayout>
147 )}
148 />
149 ) : (
150 <p className="text-sm">
151 Confirm to update foreign schema on the "{namespace}" namespace?
152 </p>
153 )}
154 </DialogSection>
155 <DialogFooter className="justify-between!">
156 <DocsButton href={`${DOCS_URL}/guides/storage/analytics/query-with-postgres`} />
157 <div className="flex items-center gap-x-2">
158 <Button type="default" disabled={isUpdating} onClick={() => setIsOpen(false)}>
159 Cancel
160 </Button>
161 <Button htmlType="submit" type="primary" loading={isUpdating}>
162 Update schema
163 </Button>
164 </div>
165 </DialogFooter>
166 </form>
167 </Form>
168 </DialogContent>
169 </Dialog>
170 )
171}