import { zodResolver } from '@hookform/resolvers/zod' import { useEffect, useRef } from 'react' import { SubmitHandler, useForm, useWatch } from 'react-hook-form' import { toast } from 'sonner' import { Button, Form, FormControl, FormField, Input, RadioGroupStacked, RadioGroupStackedItem, SheetFooter, SheetHeader, SheetSection, SheetTitle, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' import { CreateWrapperSheetProps } from './CreateWrapperSheet' import InputField from './InputField' import { FormSection, FormSectionContent, FormSectionLabel, } from '@/components/ui/Forms/FormSection' import { useSchemaCreateMutation } from '@/data/database/schema-create-mutation' import { useSchemasQuery } from '@/data/database/schemas-query' import { useFDWCreateMutation } from '@/data/fdw/fdw-create-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useTrack } from '@/lib/telemetry/track' const FORM_ID = 'create-wrapper-form' const S3TableSchema = z.object({ target: z.literal('S3Tables'), source_schema: z.string().min(1, 'Please provide a namespace name'), wrapper_name: z.string().min(1, 'Please provide a name for your wrapper'), target_schema: z.string().min(1, 'Please provide an unique target schema'), vault_aws_access_key_id: z.string().min(1, 'Required'), vault_aws_secret_access_key: z.string().min(1, 'Required'), region_name: z.string().min(1, 'Required'), vault_aws_s3table_bucket_arn: z.string().min(1, 'Required'), }) const R2CatalogSchema = z.object({ target: z.literal('R2Catalog'), source_schema: z.string().min(1, 'Please provide a namespace name'), wrapper_name: z.string().min(1, 'Please provide a name for your wrapper'), target_schema: z.string().min(1, 'Please provide an unique target schema'), vault_aws_access_key_id: z.string().min(1, 'Required'), vault_aws_secret_access_key: z.string().min(1, 'Required'), vault_token: z.string().min(1, 'Required'), warehouse: z.string().min(1, 'Required'), s3: z.object({ endpoint: z.string().min(1, 'Required') }), catalog_uri: z.string().min(1, 'Required'), }) const IcebergRestCatalogSchema = z.object({ target: z.literal('IcebergRestCatalog'), source_schema: z.string().min(1, 'Please provide a namespace name'), wrapper_name: z.string().min(1, 'Please provide a name for your wrapper'), target_schema: z.string().min(1, 'Please provide an unique target schema'), vault_aws_access_key_id: z.string().optional(), vault_aws_secret_access_key: z.string().optional(), region_name: z.string().optional(), vault_aws_s3table_bucket_arn: z.string().optional(), vault_token: z.string().optional(), warehouse: z.string().optional(), s3: z.object({ endpoint: z.string().min(1, 'Required') }), catalog_uri: z.string().optional(), }) const formSchema = z.discriminatedUnion('target', [ S3TableSchema, R2CatalogSchema, IcebergRestCatalogSchema, ]) type FormSchema = z.infer const targetFields: Record = { S3Tables: [ { name: 'vault_aws_access_key_id', required: true }, { name: 'vault_aws_secret_access_key', required: true }, { name: 'region_name', required: true }, { name: 'vault_aws_s3table_bucket_arn', required: true }, ], R2Catalog: [ { name: 'vault_aws_access_key_id', required: true }, { name: 'vault_aws_secret_access_key', required: true }, { name: 'vault_token', required: true }, { name: 'warehouse', required: true }, { name: 's3.endpoint', required: true }, { name: 'catalog_uri', required: true }, ], IcebergRestCatalog: [ { name: 'vault_aws_access_key_id', required: false }, { name: 'vault_aws_secret_access_key', required: false }, { name: 'region_name', required: false }, { name: 'vault_aws_s3table_bucket_arn', required: false }, { name: 'vault_token', required: false }, { name: 'warehouse', required: false }, { name: 's3.endpoint', required: false }, { name: 'catalog_uri', required: false }, ], } as const type Target = 'S3Tables' | 'R2Catalog' | 'IcebergRestCatalog' const INITIAL_VALUES = { wrapper_name: '', source_schema: '', target_schema: '', target: 'S3Tables', vault_aws_access_key_id: '', vault_aws_s3table_bucket_arn: '', vault_aws_secret_access_key: '', region_name: '', } satisfies FormSchema export const CreateIcebergWrapperSheet = ({ wrapperMeta, onDirty, onClose, onCloseWithConfirmation, }: CreateWrapperSheetProps) => { const { data: project } = useSelectedProjectQuery() const track = useTrack() const { mutateAsync: createFDW, isPending: isCreatingWrapper } = useFDWCreateMutation({ onSuccess: () => { toast.success(`Successfully created ${wrapperMeta?.label} foreign data wrapper`) onClose() }, }) const { data: schemas } = useSchemasQuery({ projectRef: project?.ref!, connectionString: project?.connectionString, }) const { mutateAsync: createSchema } = useSchemaCreateMutation() const form = useForm({ resolver: zodResolver(formSchema), defaultValues: INITIAL_VALUES, }) const { resetField, formState, setError } = form const { isDirty, isSubmitting } = formState useEffect(() => { onDirty(isDirty) }, [onDirty, isDirty]) const currentTarget = useRef(INITIAL_VALUES.target) useEffect(() => { return form.subscribe({ name: 'target', formState: { values: true }, callback: ({ values }) => { if (!values.target || values.target === currentTarget.current) return currentTarget.current = values.target const fields = targetFields[values.target] if (!fields) return wrapperMeta.server.options.forEach((option) => { // @ts-expect-error Can't reconcile with form schema resetField(option.name, { defaultValue: option.defaultValue ?? '' }) }) }, }) }, [form, resetField, wrapperMeta]) const onSubmit: SubmitHandler = async (values) => { const foundSchema = schemas?.find((s) => s.name === values.target_schema) if (foundSchema) { setError('target_schema', { type: 'validate', message: 'This schema already exists. Please specify a unique schema name.', }) return } let formValues: Record = {} if (values.target === 'R2Catalog' || values.target === 'IcebergRestCatalog') { const { s3, ...otherFormValues } = values formValues = otherFormValues formValues['s3.endpoint'] = s3.endpoint } else { formValues = values } try { await createSchema({ projectRef: project?.ref, connectionString: project?.connectionString, name: values.target_schema, }) await createFDW({ projectRef: project?.ref, connectionString: project?.connectionString, wrapperMeta, formState: { ...formValues, server_name: `${values.wrapper_name}_server`, supabase_target_schema: values.target_schema, }, mode: 'schema', tables: [], sourceSchema: values.source_schema, targetSchema: values.target_schema, }) track('foreign_data_wrapper_created', { wrapperType: wrapperMeta.label }) } catch (error) { console.error(error) // The error will be handled by the mutation onError callback (toast.error) } } const isLoading = isCreatingWrapper || isSubmitting const wrapperName = useWatch({ name: 'wrapper_name', control: form.control }) const target = useWatch({ name: 'target', control: form.control }) const targetOptions = wrapperMeta.server.options .filter((option) => targetFields[target].find((field) => field.name === option.name)) .map((option) => { return { ...option, required: !!targetFields[target].find((field) => field.name === option.name)?.required, } }) return ( <>
Create a {wrapperMeta.label} wrapper Wrapper Configuration}> ( 0 ? ( <> Your wrapper's server name will be{' '} {wrapperName}_server ) : ( '' ) } > )} /> Data target}> (

AWS S3 storage that's optimized for analytics workloads.

Managed Apache Iceberg built directly into your R2 bucket.

Can be used with any S3-compatible storage.

)} />
{wrapperMeta.label} Configuration} > {targetOptions.map((option) => option.hidden ? ( ) : ( ) )}

Foreign Schema

You can query your data from the foreign tables in the specified schema after the wrapper is created.

} > {wrapperMeta.sourceSchemaOption && ( )}
) }