import { useQueryClient } from '@tanstack/react-query' import { isEmpty } from 'lodash' import { Edit, Trash } from 'lucide-react' import { useCallback, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' 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 { getDecryptedValue } from 'data/vault/vault-secret-decrypted-value-query' import { useVaultSecretsQuery } from 'data/vault/vault-secrets-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' import { Button, Form, Input, SheetFooter, SheetHeader, SheetTitle } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import InputField from './InputField' import { WrapperMeta } from './Wrappers.types' import { convertKVStringArrayToJson, FormattedWrapperTable, formatWrapperTables, makeValidateRequired, } from './Wrappers.utils' import WrapperTableEditor from './WrapperTableEditor' 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 { data: secrets, isPending: isSecretsLoading } = useVaultSecretsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { mutate: updateFDW, isPending: isSaving } = useFDWUpdateMutation({ onSuccess: () => { toast.success(`Successfully updated ${wrapperMeta?.label} foreign data wrapper`) setWrapperTables([]) const hasNewSchema = wrapperTables.some((table) => table.is_new_schema) if (hasNewSchema) invalidateSchemasQuery(queryClient, project?.ref) }, }) const [wrapperTables, setWrapperTables] = useState(() => formatWrapperTables(wrapper, wrapperMeta) ) const [isEditingTable, setIsEditingTable] = useState(false) const [selectedTableToEdit, setSelectedTableToEdit] = useState( undefined ) const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({}) const hasChangesRef = useRef(false) const initialValues = { wrapper_name: wrapper?.name, server_name: wrapper?.server_name, ...convertKVStringArrayToJson(wrapper?.server_options ?? []), } const onUpdateTable = (values: any) => { setWrapperTables((prev) => { // if the new values have tableIndex, we are editing an existing table if (values.tableIndex !== undefined) { const tableIndex = values.tableIndex const wrapperTables = [...prev] delete values.tableIndex wrapperTables[tableIndex] = values return wrapperTables } return [...prev, values] }) setIsEditingTable(false) setSelectedTableToEdit(undefined) } const onSubmit = async (values: any) => { const validate = makeValidateRequired(wrapperMeta.server.options) const errors: any = validate(values) const { wrapper_name } = values if (wrapper_name.length === 0) errors.name = 'Please provide a name for your wrapper' if (!wrapperMeta.canTargetSchema && wrapperTables.length === 0) errors.tables = 'Please add at least one table' if (!isEmpty(errors)) return setFormErrors(errors) updateFDW({ projectRef: project?.ref, connectionString: project?.connectionString, wrapper, wrapperMeta, formState: { ...values, server_name: `${wrapper_name}_server` }, tables: wrapperTables, }) } const checkIsDirty = useCallback(() => hasChangesRef.current, []) const { confirmOnClose, modalProps: closeConfirmationModalProps } = useConfirmOnClose({ checkIsDirty, onClose, }) useEffect(() => { if (!isClosing) return if (checkIsDirty()) { confirmOnClose() } else { onClose() } setIsClosing(false) }, [checkIsDirty, confirmOnClose, isClosing, onClose, setIsClosing]) return ( <>
{({ values, initialValues, resetForm }: any) => { // [Alaister] although this "technically" is breaking the rules of React hooks // it won't error because the hooks are always rendered in the same order // eslint-disable-next-line react-hooks/rules-of-hooks const [loadingSecrets, setLoadingSecrets] = useState(false) const initialTables = formatWrapperTables({ handler: wrapper.handler, tables: wrapper?.tables ?? [], }) const hasFormChanges = JSON.stringify(values) !== JSON.stringify(initialValues) const hasTableChanges = JSON.stringify(initialTables) !== JSON.stringify(wrapperTables) const hasChanges = hasFormChanges || hasTableChanges hasChangesRef.current = hasChanges const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted) // [Alaister] although this "technically" is breaking the rules of React hooks // it won't error because the hooks are always rendered in the same order // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { const fetchEncryptedValues = async () => { setLoadingSecrets(true) // If the secrets haven't loaded, escape and run the effect again when they're loaded if (isSecretsLoading) { return } const res = await Promise.all( encryptedOptions.map(async (option) => { const secret = secrets?.find( (secret) => secret.name === `${wrapper.name}_${option.name}` ) if (secret !== undefined) { const value = await getDecryptedValue({ projectRef: project?.ref, connectionString: project?.connectionString, id: secret.id, }) return { [option.name]: value[0]?.decrypted_secret ?? '' } } else { return { [option.name]: '' } } }) ) const secretValues = res.reduce((a: any, b: any) => { const [key] = Object.keys(b) return { ...a, [key]: b[key] } }, {}) resetForm({ values: { ...values, ...secretValues }, initialValues: { ...initialValues, ...secretValues }, }) setLoadingSecrets(false) } if (encryptedOptions.length > 0) fetchEncryptedValues() }, [isSecretsLoading]) return ( <> Edit {wrapperMeta.label} wrapper: {wrapper.name}
Wrapper Configuration}> Your wrapper's server name will be updated to{' '} {values.wrapper_name}_server ) : ( <> Your wrapper's server name is{' '} {values.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

} > {wrapperTables.length === 0 ? (
) : (
{wrapperTables.map((table, i) => { const target = table?.table ?? table.object return (

{table.schema_name}.{table.table_name}{' '}

Target: {target}

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

{/* Wrappers which import foreign schema don't have tables and their tables can't be edited */} {wrapperMeta.tables.length !== 0 && (
) })}
)} {wrapperTables.length > 0 && (
)} {wrapperTables.length === 0 && formErrors.tables && (

{formErrors.tables}

)}
) }}
{ setSelectedTableToEdit(undefined) setIsEditingTable(false) }} onSave={onUpdateTable} initialData={selectedTableToEdit} /> ) } const CloseConfirmationModal = ({ visible, onClose, onCancel }: ConfirmOnCloseModalProps) => (

There are unsaved changes. Are you sure you want to close the panel? Your changes will be lost.

)