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, Card, CardContent, Form, FormControl, FormField, Input, SheetFooter, SheetHeader, SheetSection, SheetTitle, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { PageSection, PageSectionContent, PageSectionDescription, PageSectionMeta, PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' 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 { 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) }, }) 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 as any), }) 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) useEffect(() => { 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)) // [Joshen] ^ Validate UUID to filter out already decrypted values 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, }) encryptedOptions.forEach((option) => { const encryptedId = initialValues[option.name] resetField(option.name, { defaultValue: decryptedValues[encryptedId] }) }) } catch (error) { toast.error('Failed to fetch encrypted values') } finally { setIsLoadingSecrets(false) } } if (encryptedIdsToFetch.length > 0) { fetchEncryptedValues(encryptedIdsToFetch) } }, [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()}

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

Saving changes will drop the existing wrapper and recreate it. Foreign servers and tables will be recreated, and dependent objects like functions or views that reference those tables may need to be updated manually afterwards.

Are you sure you want to continue?

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