diff --git a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx
index 1cb0eb0a55d..a0e8a64fd50 100644
--- a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx
+++ b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx
@@ -1,23 +1,11 @@
import Link from 'next/link'
import { isEmpty } from 'lodash'
-import { FC, useState } from 'react'
+import { useState } from 'react'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
-import {
- Button,
- Form,
- Input,
- IconArrowLeft,
- IconExternalLink,
- IconHelpCircle,
- IconEdit,
- IconEye,
- IconEyeOff,
- IconTrash,
-} from 'ui'
+import { Button, Form, Input, IconArrowLeft, IconExternalLink, IconEdit, IconTrash } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { ServerOption } from './Wrappers.types'
import { checkPermissions, useParams, useStore } from 'hooks'
import {
FormPanel,
@@ -28,88 +16,14 @@ import {
} from 'components/ui/Forms'
import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation'
+import InputField from './InputField'
import { WRAPPERS } from './Wrappers.constants'
import WrapperTableEditor from './WrapperTableEditor'
import { makeValidateRequired } from './Wrappers.utils'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
-const InputField: FC<{ option: ServerOption; error: any }> = ({ option, error }) => {
- const [showHidden, setShowHidden] = useState(!option.hidden)
- if (option.isTextArea) {
- return (
-
-
- {option.label}
- {option.urlHelper !== undefined && (
-
-
-
-
-
- )}
-
- }
- defaultValue={option.defaultValue ?? ''}
- required={option.required ?? false}
- error={error}
- className="input-mono"
- rows={6}
- />
-
- )
- } else {
- return (
-
- {option.label}
- {option.urlHelper !== undefined && (
-
-
-
-
-
- )}
-
- }
- defaultValue={option.defaultValue ?? ''}
- error={error}
- className={`${option.name === 'server_name' ? '' : 'input-mono'}`}
- type={!option.hidden ? 'text' : showHidden ? 'text' : 'password'}
- actions={
- option.hidden ? (
-
- : }
- onClick={() => setShowHidden(!showHidden)}
- />
-
- ) : null
- }
- />
- )
- }
-}
-
const CreateWrapper = () => {
- const formId = 'test-form'
+ const formId = 'create-wrapper-form'
const router = useRouter()
const { ui } = useStore()
const { ref, type } = useParams()
@@ -127,7 +41,7 @@ const CreateWrapper = () => {
const initialValues =
wrapperMeta !== undefined
? {
- name: '',
+ wrapper_name: '',
server_name: '',
...Object.fromEntries(
wrapperMeta.server.options.map((option) => [option.name, option.defaultValue ?? ''])
@@ -171,7 +85,7 @@ const CreateWrapper = () => {
connectionString: project?.connectionString,
wrapper: wrapperMeta,
formState: { ...values, server_name: `${wrapper_name}_server` },
- newTables,
+ tables: newTables,
})
ui.setNotification({
category: 'success',
@@ -270,6 +184,7 @@ const CreateWrapper = () => {
))}
diff --git a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx
new file mode 100644
index 00000000000..c5ebac4f210
--- /dev/null
+++ b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx
@@ -0,0 +1,348 @@
+import Link from 'next/link'
+import { isEmpty } from 'lodash'
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+import { observer } from 'mobx-react-lite'
+import { Button, Form, Input, IconArrowLeft, IconExternalLink, IconEdit, IconTrash } from 'ui'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+
+import { VaultSecret } from 'types'
+import { checkPermissions, useParams, useStore } from 'hooks'
+import { useFDWsQuery } from 'data/fdw/fdws-query'
+import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation'
+import { useFDWDeleteMutation } from 'data/fdw/fdw-delete-mutation'
+
+import InputField from './InputField'
+import { WRAPPERS } from './Wrappers.constants'
+import WrapperTableEditor from './WrapperTableEditor'
+import {
+ formatWrapperTables,
+ makeValidateRequired,
+ convertKVStringArrayToJson,
+} from './Wrappers.utils'
+import Loading from 'components/ui/Loading'
+import {
+ FormPanel,
+ FormActions,
+ FormSection,
+ FormSectionLabel,
+ FormSectionContent,
+} from 'components/ui/Forms'
+import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
+
+const EditWrapper = () => {
+ const formId = 'edit-wrapper-form'
+ const router = useRouter()
+ const { ui, vault } = useStore()
+ const { ref, id } = useParams()
+ const { project } = useProjectContext()
+
+ const { data, isLoading } = useFDWsQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ })
+ const wrappers = data?.result ?? []
+ const wrapper = wrappers.find((w) => Number(w.id) === Number(id))
+ const wrapperMeta = WRAPPERS.find((w) => w.handlerName === wrapper?.handler)
+
+ const { mutateAsync: createFDW } = useFDWCreateMutation()
+ const { mutateAsync: deleteFDW } = useFDWDeleteMutation()
+
+ const [wrapperTables, setWrapperTables] = useState([])
+ const [isEditingTable, setIsEditingTable] = useState(false)
+ const [selectedTableToEdit, setSelectedTableToEdit] = useState()
+ const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({})
+
+ const canCreateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions')
+
+ const initialValues =
+ wrapperMeta !== undefined
+ ? {
+ wrapper_name: wrapper?.name,
+ server_name: wrapper?.server_name,
+ ...convertKVStringArrayToJson(wrapper?.server_options ?? []),
+ }
+ : {}
+
+ useEffect(() => {
+ if (wrapper?.id) {
+ setWrapperTables(formatWrapperTables(wrapper?.tables ?? []))
+ }
+ }, [wrapper?.id])
+
+ if (isLoading) {
+ return
+ }
+
+ if (wrapper === undefined || wrapperMeta === undefined) {
+ return Oh no
+ }
+
+ 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, { setSubmitting }: 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 (wrapperTables.length === 0) errors.tables = 'Please add at least one table'
+ if (!isEmpty(errors)) return setFormErrors(errors)
+
+ setSubmitting(true)
+ try {
+ await deleteFDW({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ wrapper,
+ wrapperMeta,
+ })
+
+ await createFDW({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ wrapper: wrapperMeta,
+ formState: { ...values, server_name: `${wrapper_name}_server` },
+ tables: wrapperTables,
+ })
+
+ ui.setNotification({
+ category: 'success',
+ message: `Successfully updated ${wrapperMeta.label} foreign data wrapper`,
+ })
+ setWrapperTables([])
+ router.push(`/project/${ref}/database/wrappers`)
+ } catch (error: any) {
+ ui.setNotification({
+ error,
+ category: 'error',
+ message: `Failed to create ${wrapperMeta.label} foreign data wrapper: ${error.message}`,
+ })
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ return (
+ <>
+
+
+
+
Edit 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.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) => (
+
+
+
+ {table.schema_name}.{table.table_name}
+
+
+ {wrapperMeta.tables[table.index].label}: {table.columns.join(', ')}
+
+
+
+ }
+ onClick={() => {
+ setIsEditingTable(true)
+ setSelectedTableToEdit({ ...table, tableIndex: i })
+ }}
+ />
+ }
+ onClick={() => {
+ setWrapperTables((prev) => prev.filter((_, j) => j !== i))
+ }}
+ />
+
+
+ ))}
+
+ )}
+ {wrapperTables.length > 0 && (
+
+
+
+ )}
+ {wrapperTables.length === 0 && formErrors.tables && (
+ {formErrors.tables}
+ )}
+
+
+
+ )
+ }}
+
+
+
+ {
+ setSelectedTableToEdit(undefined)
+ setIsEditingTable(false)
+ }}
+ onSave={onUpdateTable}
+ initialData={selectedTableToEdit}
+ />
+ >
+ )
+}
+
+export default observer(EditWrapper)
diff --git a/studio/components/interfaces/Database/Wrappers/InputField.tsx b/studio/components/interfaces/Database/Wrappers/InputField.tsx
new file mode 100644
index 00000000000..a3839cc8fad
--- /dev/null
+++ b/studio/components/interfaces/Database/Wrappers/InputField.tsx
@@ -0,0 +1,94 @@
+import Link from 'next/link'
+import { FC, useState } from 'react'
+import { Button, Input, IconHelpCircle, IconEye, IconEyeOff, IconLoader } from 'ui'
+import { ServerOption } from './Wrappers.types'
+
+interface Props {
+ option: ServerOption
+ loading: boolean
+ error: any
+}
+
+const InputField: FC = ({ option, loading, error }) => {
+ const [showHidden, setShowHidden] = useState(!option.hidden)
+ if (option.isTextArea) {
+ return (
+
+
+ {option.label}
+ {option.urlHelper !== undefined && (
+
+
+
+
+
+ )}
+
+ }
+ value={loading ? 'Fetching value from Vault...' : undefined}
+ defaultValue={option.defaultValue ?? ''}
+ required={option.required ?? false}
+ />
+
+ )
+ } else {
+ return (
+
+ {option.label}
+ {option.urlHelper !== undefined && (
+
+
+
+
+
+ )}
+
+ }
+ defaultValue={option.defaultValue ?? ''}
+ error={error}
+ value={loading ? 'Fetching value from Vault...' : undefined}
+ type={!option.hidden || loading ? 'text' : showHidden ? 'text' : 'password'}
+ disabled={loading}
+ actions={
+ loading ? (
+
+ } />
+
+ ) : option.hidden ? (
+
+ : }
+ onClick={() => setShowHidden(!showHidden)}
+ />
+
+ ) : null
+ }
+ />
+ )
+ }
+}
+
+export default InputField
diff --git a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx
index 2df0ce425a4..0e336cdf86d 100644
--- a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx
+++ b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx
@@ -7,8 +7,6 @@ import {
Button,
IconExternalLink,
IconLoader,
- IconDatabase,
- IconServer,
IconTrash,
IconEdit,
} from 'ui'
@@ -35,8 +33,6 @@ const WrapperRow: FC = ({ wrapperMeta, wrappers = [], isOpen, isLoading,
const { mutateAsync: deleteFDW } = useFDWDeleteMutation()
const [isSubmitting, setIsSubmitting] = useState(false)
- console.log({ wrappers })
-
const onDeleteWrapper = (wrapper: any) => {
confirmAlert({
title: `Confirm to disable ${wrapper.name}`,
@@ -146,7 +142,7 @@ const WrapperRow: FC = ({ wrapperMeta, wrappers = [], isOpen, isLoading,
-
}
- className="py-2"
- onClick={() => onDeleteWrapper(wrapper)}
- />
+
+
+ }
+ className="py-2"
+ />
+
+