import { zodResolver } from '@hookform/resolvers/zod' import { useQueryClient } from '@tanstack/react-query' import { compact } from 'lodash' import { Edit, Trash } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form' import { toast } from 'sonner' import { Button, Form, FormControl, FormField, Input, Separator, SheetFooter, SheetHeader, SheetTitle, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' import InputField from './InputField' import { WrapperMeta } from './Wrappers.types' import { convertKVStringArrayToJson, FormattedWrapperTable, formatWrapperTables, getEditionFormSchema, NewTable, } from './Wrappers.utils' import WrapperTableEditor from './WrapperTableEditor' import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { FormSection, FormSectionContent, FormSectionLabel, } from '@/components/ui/Forms/FormSection' import { invalidateSchemasQuery } from '@/data/database/schemas-query' import { useFDWUpdateMutation } from '@/data/fdw/fdw-update-mutation' import { FDW } from '@/data/fdw/fdws-query' import { getDecryptedValues } from '@/data/vault/vault-secret-decrypted-value-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' import { UUID_REGEX } from '@/lib/constants' export interface EditWrapperSheetProps { wrapper: FDW isClosing: boolean wrapperMeta: WrapperMeta setIsClosing: (v: boolean) => void onClose: () => void } const FORM_ID = 'edit-wrapper-form' export const EditWrapperSheet = ({ wrapper, wrapperMeta, isClosing, setIsClosing, onClose, }: EditWrapperSheetProps) => { const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const { mutate: updateFDW, isPending: isSaving } = useFDWUpdateMutation({ onSuccess: () => { toast.success(`Successfully updated ${wrapperMeta?.label} foreign data wrapper`) const { tables } = getValues() const hasNewSchema = (tables as Record[]).some((table) => table.is_new_schema) if (hasNewSchema) invalidateSchemasQuery(queryClient, project?.ref) onClose() }, }) const initialValues: Record = useMemo( () => ({ wrapper_name: wrapper?.name, server_name: wrapper?.server_name, ...convertKVStringArrayToJson(wrapper?.server_options ?? []), tables: formatWrapperTables(wrapper, wrapperMeta), }), [wrapper, wrapperMeta] ) const formSchema = getEditionFormSchema(wrapperMeta) type FormSchema = z.infer const form = useForm({ defaultValues: initialValues, resolver: zodResolver(formSchema), }) const { getValues, resetField, setError } = form const { errors, isDirty, isSubmitting } = form.formState const { fields: tablesField, append: appendTable, remove: removeTable, update: updateTable, } = useFieldArray({ control: form.control, name: 'tables', }) const [selectedTableToEdit, setSelectedTableToEdit] = useState( undefined ) const [isUpdateConfirmationOpen, setIsUpdateConfirmationOpen] = useState(false) const onUpdateTable = (values: FormattedWrapperTable) => { if (values.index !== undefined) { updateTable(values.index, values) } else { appendTable(values) } setSelectedTableToEdit(undefined) } const onSubmit: SubmitHandler = async (values) => { const { tables } = values if (tables.length === 0) { setError('tables', { type: 'validate', message: 'Please provide at least one table.', }) return } setIsUpdateConfirmationOpen(true) } const { confirmOnClose, modalProps } = useConfirmOnClose({ checkIsDirty: () => isDirty, onClose, }) useEffect(() => { if (!isClosing) return if (isDirty) { confirmOnClose() } else { onClose() } setIsClosing(false) }, [isDirty, confirmOnClose, isClosing, onClose, setIsClosing]) const wrapper_name = useWatch({ name: 'wrapper_name', control: form.control }) const [isLoadingSecrets, setIsLoadingSecrets] = useState(false) const [secretsReady, setSecretsReady] = useState(false) useEffect(() => { let isCurrent = true const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted) const encryptedIdsToFetch = compact( encryptedOptions.map((option) => { const value = initialValues[option.name] return value ?? null }) ).filter((x) => UUID_REGEX.test(x)) if (encryptedIdsToFetch.length === 0) { setSecretsReady(true) setIsLoadingSecrets(false) return } setSecretsReady(false) const fetchEncryptedValues = async (ids: string[]) => { try { setIsLoadingSecrets(true) // If the secrets haven't loaded, escape and run the effect again when they're loaded const decryptedValues = await getDecryptedValues({ projectRef: project?.ref, connectionString: project?.connectionString, ids: ids, }) if (!isCurrent) return encryptedOptions.forEach((option) => { const encryptedId = initialValues[option.name] resetField(option.name, { defaultValue: decryptedValues[encryptedId] }) }) setSecretsReady(true) } catch (error) { if (!isCurrent) return toast.error('Failed to fetch encrypted values') } finally { if (isCurrent) setIsLoadingSecrets(false) } } fetchEncryptedValues(encryptedIdsToFetch) return () => { isCurrent = false } }, [initialValues, wrapperMeta, resetField, project?.ref, project?.connectionString]) return ( <>
Edit {wrapperMeta.label} wrapper: {wrapper.name}
Wrapper Configuration}> ( Your wrapper's server name will be updated to{' '} {wrapper_name}_server ) : ( <> Your wrapper's server name is{' '} {wrapper_name}_server ) } > )} /> {wrapperMeta.label} Configuration} > {wrapperMeta.server.options .filter((option) => !option.hidden) .map((option) => ( ))}

Foreign Tables

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

} > {tablesField.map((t, tableIndex) => { // FIXME: make inference work const table = t as unknown as FormattedWrapperTable return (

{table.schema_name}.{table.table_name}

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

) })}
{tablesField.length === 0 && errors.tables && (

{errors.tables.message?.toString()}

)}
Save wrapper
{ setIsUpdateConfirmationOpen(false) onClose() }} onConfirm={() => { const { tables, ...values } = getValues() updateFDW({ projectRef: project?.ref, connectionString: project?.connectionString, wrapper, wrapperMeta, formState: values, tables, }) setIsUpdateConfirmationOpen(false) }} >

Removing a table or retyping a column may break views or functions that reference it.

Are you sure you want to continue?

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