import { useQueryClient } from '@tanstack/react-query' import { isEmpty } from 'lodash' import { Edit, Trash } from 'lucide-react' import { useState } from 'react' import { toast } from 'sonner' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSchemaCreateMutation } from 'data/database/schema-create-mutation' import { invalidateSchemasQuery, useSchemasQuery } from 'data/database/schemas-query' import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, Form, Input, RadioGroupStacked, RadioGroupStackedItem, Separator, SheetFooter, SheetHeader, SheetTitle, WarningIcon, } from 'ui' import InputField from './InputField' import { WrapperMeta } from './Wrappers.types' import { makeValidateRequired } from './Wrappers.utils' import WrapperTableEditor from './WrapperTableEditor' const FORM_ID = 'create-wrapper-form' export interface CreateWrapperSheetProps { wrapperMeta: WrapperMeta onDirty: (isDirty: boolean) => void onClose: () => void onCloseWithConfirmation: () => void } export const CreateWrapperSheet = ({ wrapperMeta, onDirty, onClose, onCloseWithConfirmation, }: CreateWrapperSheetProps) => { const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const { mutate: sendEvent } = useSendEventMutation() const [newTables, setNewTables] = useState([]) const [isEditingTable, setIsEditingTable] = useState(false) const [selectedTableToEdit, setSelectedTableToEdit] = useState() const [selectedMode, setSelectedMode] = useState<'tables' | 'schema'>( wrapperMeta.tables.length > 0 ? 'tables' : 'schema' ) const { data: extensions } = useDatabaseExtensionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const wrappersExtension = extensions?.find((ext) => ext.name === 'wrappers') // The import foreign schema requires a minimum extension version of 0.5.0 const hasRequiredVersionForeignSchema = wrappersExtension?.installed_version ? wrappersExtension?.installed_version >= '0.5.0' : false const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({}) const { mutateAsync: createFDW, isPending: isCreatingWrapper } = useFDWCreateMutation({ onSuccess: () => { toast.success(`Successfully created ${wrapperMeta?.label} foreign data wrapper`) setNewTables([]) const hasNewSchema = newTables.some((table) => table.is_new_schema) if (hasNewSchema) invalidateSchemasQuery(queryClient, project?.ref) onClose() }, }) const { data: schemas } = useSchemasQuery({ projectRef: project?.ref!, connectionString: project?.connectionString, }) const initialValues = { wrapper_name: '', server_name: '', source_schema: wrapperMeta.sourceSchemaOption?.defaultValue ?? '', target_schema: '', ...Object.fromEntries( wrapperMeta.server.options.map((option) => [option.name, option.defaultValue ?? '']) ), } const { mutateAsync: createSchema, isPending: isCreatingSchema } = useSchemaCreateMutation() const onUpdateTable = (values: any) => { setNewTables((prev) => { // if the new values have tableIndex, we are editing an existing table if (values.tableIndex !== undefined) { const tableIndex = values.tableIndex const newTables = [...prev] delete values.tableIndex newTables[tableIndex] = values return newTables } return [...prev, values] }) setIsEditingTable(false) setSelectedTableToEdit(undefined) } const onSubmit = async (values: any) => { const validate = makeValidateRequired(wrapperMeta.server.options) const errors: any = validate(values) if (values.wrapper_name.length === 0) { errors.wrapper_name = 'Please provide a name for your wrapper' } if (selectedMode === 'tables') { if (newTables.length === 0) { errors.tables = 'Please provide at least one table' } } if (selectedMode === 'schema') { if (wrapperMeta.sourceSchemaOption && values.source_schema.length === 0) { errors.source_schema = 'Please provide a source schema' } if (values.target_schema.length === 0) { errors.target_schema = 'Please provide an unique target schema' } const foundSchema = schemas?.find((s) => s.name === values.target_schema) if (foundSchema) { errors.target_schema = 'This schema already exists. Please specify a unique schema name.' } } setFormErrors(errors) if (!isEmpty(errors)) return try { if (selectedMode === 'schema') { await createSchema({ projectRef: project?.ref, connectionString: project?.connectionString, name: values.target_schema, }) } await createFDW({ projectRef: project?.ref, connectionString: project?.connectionString, wrapperMeta, formState: { ...values, server_name: `${values.wrapper_name}_server`, supabase_target_schema: selectedMode === 'schema' ? values.target_schema : undefined, }, mode: selectedMode === 'schema' ? wrapperMeta.sourceSchemaOption ? 'schema' : 'skip' : 'tables', tables: newTables, sourceSchema: values.source_schema, targetSchema: values.target_schema, }) sendEvent({ action: 'foreign_data_wrapper_created', properties: { wrapperType: wrapperMeta.label, }, groups: { project: project?.ref ?? 'Unknown', organization: org?.slug ?? 'Unknown', }, }) } catch (error) { console.error(error) // The error will be handled by the mutation onError callback (toast.error) } } const isLoading = isCreatingWrapper || isCreatingSchema return ( <>
{({ values, initialValues }: any) => { const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) onDirty(hasChanges) return ( <> Create a {wrapperMeta.label} wrapper
Wrapper Configuration}> 0 ? ( <> Your wrapper's server name will be{' '} {values.wrapper_name}_server ) : ( '' ) } /> {wrapperMeta.label} Configuration} > {wrapperMeta.server.options .filter((option) => !option.hidden) .map((option) => ( ))} Data target}> setSelectedMode(value as 'tables' | 'schema')} >

Create foreign tables to query data from {wrapperMeta.label}.

{wrapperMeta.tables.length === 0 ? (
This wrapper doesn't support using foreign tables.
) : null}

Create all foreign tables from {wrapperMeta.label} in a specified schema.

{wrapperMeta.canTargetSchema ? ( hasRequiredVersionForeignSchema ? null : (
This feature requires the{' '} wrappers extension to be of minimum version of 0.5.0.
) ) : (
This wrapper doesn't support using a foreign schema.
)}
{selectedMode === 'tables' && (

Foreign Tables

You can query your data from these foreign tables after the wrapper is created

} > {newTables.length === 0 ? (
) : (
{newTables.map((table, i) => (

{table.schema_name}.{table.table_name}

Columns:{' '} {table.columns.map((column: any) => column.name).join(', ')}

))}
)} {newTables.length > 0 && (
)} {newTables.length === 0 && formErrors.tables && (

{formErrors.tables}

)}
)} {selectedMode === 'schema' && (

Foreign Schema

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

} > {wrapperMeta.sourceSchemaOption && !wrapperMeta.sourceSchemaOption?.readOnly && ( // Hide the field if the source schema is read-only

{wrapperMeta.sourceSchemaOption.description}

)}

A new schema will be created. For security purposes, the wrapper tables from the foreign schema cannot be created within an existing schema.

)}
) }}
{ setSelectedTableToEdit(undefined) setIsEditingTable(false) }} onSave={onUpdateTable} initialData={selectedTableToEdit} /> ) }