From acc4707959b7127c54744a22fba9abb122986042 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 5 Jan 2023 15:46:22 +0700 Subject: [PATCH 01/11] Split FDW creation into 2 separate steps to allow creation of multiple instances of a wrapper type --- .../Database/Wrappers/CreateWrapper.tsx | 333 ++++++++++++++++++ .../Database/Wrappers/Wrappers.constants.ts | 7 + .../interfaces/Database/Wrappers/Wrappers.tsx | 237 +++---------- .../Wrappers/WrappersDisabledState.tsx | 156 ++++++++ .../Database/Wrappers/WrappersDropdown.tsx | 59 ++++ .../components/interfaces/Database/index.ts | 2 + .../{wrappers.tsx => wrappers/index.tsx} | 0 .../project/[ref]/database/wrappers/new.tsx | 26 ++ 8 files changed, 629 insertions(+), 191 deletions(-) create mode 100644 studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx create mode 100644 studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx create mode 100644 studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx rename studio/pages/project/[ref]/database/{wrappers.tsx => wrappers/index.tsx} (100%) create mode 100644 studio/pages/project/[ref]/database/wrappers/new.tsx diff --git a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx new file mode 100644 index 00000000000..cd456fd9e07 --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx @@ -0,0 +1,333 @@ +import Link from 'next/link' +import { isEmpty } from 'lodash' +import { FC, 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 { PermissionAction } from '@supabase/shared-types/out/constants' + +import { ServerOption } from './Wrappers.types' +import { checkPermissions, useParams, useStore } from 'hooks' +import { + FormPanel, + FormActions, + FormSection, + FormSectionLabel, + FormSectionContent, +} from 'components/ui/Forms' +import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation' + +import { WRAPPERS } from './Wrappers.constants' +import WrapperRow from './WrapperRow' +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 === 'name' ? '' : 'input-mono'}`} + type={!option.hidden ? 'text' : showHidden ? 'text' : 'password'} + actions={ + option.hidden ? ( +
+
+ ) : null + } + /> + ) + } +} + +const CreateWrapper = () => { + const formId = 'test-form' + const router = useRouter() + const { ui } = useStore() + const { ref, type } = useParams() + const { project } = useProjectContext() + const { mutateAsync: createFDW } = useFDWCreateMutation() + + const [newTables, setNewTables] = 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 wrapperMeta = WRAPPERS.find((wrapper) => wrapper.name === type) + const initialValues = + wrapperMeta !== undefined + ? Object.fromEntries( + wrapperMeta.server.options.map((option) => [option.name, option.defaultValue ?? '']) + ) + : {} + + if (wrapperMeta === undefined) { + return
Lol what?
+ } + + 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, { setSubmitting }: any) => { + const validate = makeValidateRequired(wrapperMeta.server.options) + + const errors: any = validate(values) + if (newTables.length === 0) errors.tables = 'Please add at least one table' + if (!isEmpty(errors)) return setFormErrors(errors) + + setSubmitting(true) + try { + await createFDW({ + projectRef: project?.ref, + connectionString: project?.connectionString, + wrapper: wrapperMeta, + formState: values, + newTables, + }) + ui.setNotification({ + category: 'success', + message: `Successfully created ${wrapperMeta.label} foreign data wrapper`, + }) + setNewTables([]) + 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 ( + <> +
+
+
+ + +
+ +

Back

+
+
+ +
+

Create a {wrapperMeta?.label} Wrapper

+
+ + + + + +
+
+ +
+ {({ isSubmitting, handleReset, values, initialValues }: any) => { + const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + return ( + + +
+ } + > + Server Configuration}> + + {wrapperMeta.server.options.map((option) => ( + + ))} + + + +

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} +

+

+ {wrapperMeta.tables[table.index].label}: {table.columns.join(', ')} +

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

{formErrors.tables}

+ )} +
+
+ + ) + }} + + + + { + setSelectedTableToEdit(undefined) + setIsEditingTable(false) + }} + onSave={onUpdateTable} + initialData={selectedTableToEdit} + /> + + ) +} + +export default observer(CreateWrapper) diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts index a987a4533af..3d61062903f 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts @@ -12,6 +12,13 @@ export const WRAPPERS: Wrapper[] = [ server: { name: 'stripe_server', options: [ + { + name: 'name', + label: 'Wrapper Name', + required: true, + encrypted: false, + hidden: false, + }, { name: 'api_key_id', label: 'Stripe Secret Key', diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx index bc6abe43bab..15a0236dc8f 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx @@ -2,34 +2,31 @@ import Link from 'next/link' import { useState } from 'react' import { Button, IconExternalLink } from 'ui' import { observer } from 'mobx-react-lite' -import * as Tooltip from '@radix-ui/react-tooltip' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { checkPermissions, useParams, useStore } from 'hooks' +import { useStore } from 'hooks' import { useFDWsQuery } from 'data/fdw/fdws-query' import { WRAPPERS } from './Wrappers.constants' import WrapperRow from './WrapperRow' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import WrappersDropdown from './WrappersDropdown' +import WrappersDisabledState from './WrappersDisabledState' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' const Wrappers = () => { - const { ref } = useParams() - const { ui, meta } = useStore() + const { meta } = useStore() const { project } = useProjectContext() const { data, isLoading } = useFDWsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const wrappers = data?.result ?? [] - const enabledWrapperNamesSet = new Set(data?.result.map((fdw) => fdw.name)) + const enabledWrapperNamesSet = new Set(wrappers.map((fdw) => fdw.name)) const [open, setOpen] = useState('') - const [isEnabling, setIsEnabling] = useState(false) const wrappersExtension = meta.extensions.byId('wrappers') const vaultExtension = meta.extensions.byId('supabase_vault') const isLoadingExtensions = meta.extensions.isLoading - const isNotAvailable = wrappersExtension === undefined || vaultExtension === undefined const isWrappersEnabled = wrappersExtension !== undefined && @@ -37,48 +34,6 @@ const Wrappers = () => { vaultExtension !== undefined && vaultExtension?.installed_version !== null - const canToggleWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions') - - const onEnableWrappers = async () => { - if (wrappersExtension === undefined || vaultExtension === undefined) return - setIsEnabling(true) - - const requiredExtensions = await Promise.all([ - await meta.extensions.create({ - schema: wrappersExtension.schema ?? 'extensions', - name: wrappersExtension.name, - version: wrappersExtension.default_version, - cascade: true, - }), - await meta.extensions.create({ - schema: vaultExtension.schema ?? 'vault', - name: vaultExtension.name, - version: vaultExtension.default_version, - cascade: true, - }), - ]) - const errors = requiredExtensions.filter( - (res) => res.error && !res.error.message.includes('already exists') - ) - - if (errors.length > 0) { - ui.setNotification({ - error: errors, - category: 'error', - message: `Failed to enable Wrappers for your project: ${errors - .map((x) => x.message) - .join(', ')}`, - }) - } else { - ui.setNotification({ - category: 'success', - message: 'Wrappers is now enabled for your project!', - }) - } - - setIsEnabling(false) - } - return (
@@ -88,39 +43,16 @@ const Wrappers = () => { Query your data warehouse directly from your database, or third-party APIs using SQL.
- {/* [Joshen TODO] For when we support multiple instances per wrapper */} - {/*
- - {WRAPPERS.map((wrapper, idx) => ( - <> - - } - onClick={() => {}} - > - {wrapper.label} - - {idx !== WRAPPERS.length - 1 && } - - ))} - - } - > - - -
*/} + {isLoadingExtensions ? ( @@ -131,116 +63,39 @@ const Wrappers = () => { ) : isWrappersEnabled ? (
- {WRAPPERS.map((wrapper, i) => { - return ( - w.name === wrapper.server.name)?.tables ?? []} - isLoading={isLoading} - isEnabled={enabledWrapperNamesSet.has(wrapper.server.name)} - isOpen={open === wrapper.name} - onOpen={(wrapperName) => { - if (open !== wrapperName) setOpen(wrapperName) - else setOpen('') - }} - /> - ) - })} + {wrappers.length === 0 ? ( +
+

No wrappers created yet

+ +
+ ) : ( + <> + {WRAPPERS.map((wrapper, i) => { + return ( + w.name === wrapper.server.name)?.tables ?? []} + isLoading={isLoading} + isEnabled={enabledWrapperNamesSet.has(wrapper.server.name)} + isOpen={open === wrapper.name} + onOpen={(wrapperName) => { + if (open !== wrapperName) setOpen(wrapperName) + else setOpen('') + }} + /> + ) + })} + + )}
) : ( -
-
-
-
-

Supabase Wrappers

-

- Supabase Wrappers is a framework for building Postgres Foreign Data Wrappers (FDW) - which connect Postgres to external systems. Query your data warehouse or - third-party APIs directly from your database. -

-
- {isNotAvailable ? ( -
-
-
-

- Wrappers is not available for this project yet. -

-

- Do reach out to us if you're interested! -

-
- -
- -
- ) : ( -
- - - - - - - - - - {!canToggleWrappers && ( - - -
- - You need additional permissions to enable Wrappers for this project - -
-
- )} -
-
- )} -
-
-
+ )} ) diff --git a/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx b/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx new file mode 100644 index 00000000000..54e4fa6a8b1 --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx @@ -0,0 +1,156 @@ +import Link from 'next/link' +import { useState } from 'react' +import { Button, IconExternalLink } from 'ui' +import { observer } from 'mobx-react-lite' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { checkPermissions, useParams, useStore } from 'hooks' + +const WrappersDisabledState = () => { + const { ui, meta } = useStore() + const { ref } = useParams() + const wrappersExtension = meta.extensions.byId('wrappers') + const vaultExtension = meta.extensions.byId('supabase_vault') + const isNotAvailable = wrappersExtension === undefined || vaultExtension === undefined + + const [isEnabling, setIsEnabling] = useState(false) + const canToggleWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions') + + const onEnableWrappers = async () => { + if (wrappersExtension === undefined || vaultExtension === undefined) return + setIsEnabling(true) + + const requiredExtensions = await Promise.all([ + await meta.extensions.create({ + schema: wrappersExtension.schema ?? 'extensions', + name: wrappersExtension.name, + version: wrappersExtension.default_version, + cascade: true, + }), + await meta.extensions.create({ + schema: vaultExtension.schema ?? 'vault', + name: vaultExtension.name, + version: vaultExtension.default_version, + cascade: true, + }), + ]) + const errors = requiredExtensions.filter( + (res) => res.error && !res.error.message.includes('already exists') + ) + + if (errors.length > 0) { + ui.setNotification({ + error: errors, + category: 'error', + message: `Failed to enable Wrappers for your project: ${errors + .map((x) => x.message) + .join(', ')}`, + }) + } else { + ui.setNotification({ + category: 'success', + message: 'Wrappers is now enabled for your project!', + }) + } + + setIsEnabling(false) + } + + return ( +
+
+
+
+

Supabase Wrappers

+

+ Supabase Wrappers is a framework for building Postgres Foreign Data Wrappers (FDW) + which connect Postgres to external systems. Query your data warehouse or third-party + APIs directly from your database. +

+
+ {isNotAvailable ? ( +
+
+
+

+ Wrappers is not available for this project yet. +

+

+ Do reach out to us if you're interested! +

+
+ +
+ +
+ ) : ( +
+ + + + + + + + + + {!canToggleWrappers && ( + + +
+ + You need additional permissions to enable Wrappers for this project + +
+
+ )} +
+
+ )} +
+
+
+ ) +} + +export default observer(WrappersDisabledState) diff --git a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx new file mode 100644 index 00000000000..88f6fd36404 --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx @@ -0,0 +1,59 @@ +import Link from 'next/link' +import Image from 'next/image' +import { FC } from 'react' +import { observer } from 'mobx-react-lite' +import { Button, Dropdown, IconPlus } from 'ui' + +import { useParams } from 'hooks' +import { WRAPPERS } from './Wrappers.constants' + +interface Props { + buttonText?: string + align?: 'center' | 'end' +} + +const WrapperDropdown: FC = ({ buttonText = 'Add wrapper', align = 'end' }) => { + const { ref } = useParams() + + return ( + + {WRAPPERS.map((wrapper, idx) => ( + <> + + + + } + > + {wrapper.label} + + + + {idx !== WRAPPERS.length - 1 && } + + ))} + + } + > + + + ) +} + +export default observer(WrapperDropdown) diff --git a/studio/components/interfaces/Database/index.ts b/studio/components/interfaces/Database/index.ts index b294434c18e..d8e78540e78 100644 --- a/studio/components/interfaces/Database/index.ts +++ b/studio/components/interfaces/Database/index.ts @@ -7,6 +7,7 @@ import RolesSettings from './Roles/RolesSettings' import Extensions from './Extensions/Extensions' import Wrappers from './Wrappers/Wrappers' +import CreateWrapper from './Wrappers/CreateWrapper' import PublicationsList from './Publications/PublicationsList' import PublicationsTables from './Publications/PublicationsTables' @@ -28,6 +29,7 @@ export { RolesSettings, Extensions, Wrappers, + CreateWrapper, PublicationsList, PublicationsTables, BackupsList, diff --git a/studio/pages/project/[ref]/database/wrappers.tsx b/studio/pages/project/[ref]/database/wrappers/index.tsx similarity index 100% rename from studio/pages/project/[ref]/database/wrappers.tsx rename to studio/pages/project/[ref]/database/wrappers/index.tsx diff --git a/studio/pages/project/[ref]/database/wrappers/new.tsx b/studio/pages/project/[ref]/database/wrappers/new.tsx new file mode 100644 index 00000000000..7d5987fef99 --- /dev/null +++ b/studio/pages/project/[ref]/database/wrappers/new.tsx @@ -0,0 +1,26 @@ +import { observer } from 'mobx-react-lite' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { NextPageWithLayout } from 'types' +import { checkPermissions } from 'hooks' +import { DatabaseLayout } from 'components/layouts' +import NoPermission from 'components/ui/NoPermission' +import { FormsContainer } from 'components/ui/Forms' +import { CreateWrapper } from 'components/interfaces/Database' + +const DatabaseWrappersNew: NextPageWithLayout = () => { + const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'wrappers') + if (!canReadWrappers) { + return + } + + return +} + +DatabaseWrappersNew.getLayout = (page) => ( + + {page} + +) + +export default observer(DatabaseWrappersNew) From 17111c85d62e1807cd48f0dce13f8316525a40cb Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 9 Jan 2023 12:33:24 +0800 Subject: [PATCH 02/11] Update create fdw ui flow --- .../Database/Wrappers/CreateWrapper.tsx | 52 +- .../Database/Wrappers/WrapperRow.tsx | 452 ++++-------------- .../Database/Wrappers/Wrappers.constants.ts | 9 - .../interfaces/Database/Wrappers/Wrappers.tsx | 40 +- .../Database/Wrappers/Wrappers.types.ts | 1 - .../Vault/Secrets/SecretsManagement.tsx | 25 +- studio/data/fdw/fdw-create-mutation.ts | 12 +- studio/data/fdw/fdws-query.ts | 11 +- 8 files changed, 197 insertions(+), 405 deletions(-) diff --git a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx index cd456fd9e07..1cb0eb0a55d 100644 --- a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx @@ -29,7 +29,6 @@ import { import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation' import { WRAPPERS } from './Wrappers.constants' -import WrapperRow from './WrapperRow' import WrapperTableEditor from './WrapperTableEditor' import { makeValidateRequired } from './Wrappers.utils' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -91,7 +90,7 @@ const InputField: FC<{ option: ServerOption; error: any }> = ({ option, error }) } defaultValue={option.defaultValue ?? ''} error={error} - className={`${option.name === 'name' ? '' : 'input-mono'}`} + className={`${option.name === 'server_name' ? '' : 'input-mono'}`} type={!option.hidden ? 'text' : showHidden ? 'text' : 'password'} actions={ option.hidden ? ( @@ -127,9 +126,13 @@ const CreateWrapper = () => { const wrapperMeta = WRAPPERS.find((wrapper) => wrapper.name === type) const initialValues = wrapperMeta !== undefined - ? Object.fromEntries( - wrapperMeta.server.options.map((option) => [option.name, option.defaultValue ?? '']) - ) + ? { + name: '', + server_name: '', + ...Object.fromEntries( + wrapperMeta.server.options.map((option) => [option.name, option.defaultValue ?? '']) + ), + } : {} if (wrapperMeta === undefined) { @@ -154,8 +157,10 @@ const CreateWrapper = () => { 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 (newTables.length === 0) errors.tables = 'Please add at least one table' if (!isEmpty(errors)) return setFormErrors(errors) @@ -165,7 +170,7 @@ const CreateWrapper = () => { projectRef: project?.ref, connectionString: project?.connectionString, wrapper: wrapperMeta, - formState: values, + formState: { ...values, server_name: `${wrapper_name}_server` }, newTables, }) ui.setNotification({ @@ -238,10 +243,35 @@ const CreateWrapper = () => { } > - Server Configuration}> + Wrapper Configuration}> + + 0 ? ( + <> + Your wrapper's server name will be{' '} + {values.wrapper_name}_server + + ) : ( + '' + ) + } + /> + + + {wrapperMeta.label} Configuration} + > {wrapperMeta.server.options.map((option) => ( - + ))} @@ -264,7 +294,7 @@ const CreateWrapper = () => { ) : ( - <> +
{newTables.map((table, i) => (
@@ -296,7 +326,7 @@ const CreateWrapper = () => {
))} - +
)} {newTables.length > 0 && (
diff --git a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx index 75368404eef..a7370a5eed4 100644 --- a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx @@ -1,222 +1,62 @@ import Link from 'next/link' import Image from 'next/image' -import { isEmpty } from 'lodash' import { FC, useState } from 'react' import { Collapsible, - Input, - IconCheck, IconChevronUp, Button, - IconEdit, - IconTrash, - IconEye, - IconEyeOff, - IconCheckCircle, IconExternalLink, IconLoader, - IconHelpCircle, + IconDatabase, + IconServer, + IconTrash, + IconEdit, } from 'ui' import { useParams, useStore } from 'hooks' -import { ServerOption, Wrapper } from './Wrappers.types' -import { makeValidateRequired } from './Wrappers.utils' -import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation' +import { Wrapper } from './Wrappers.types' import { useFDWDeleteMutation } from 'data/fdw/fdw-delete-mutation' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import WrapperTableEditor from './WrapperTableEditor' -import InformationBox from 'components/ui/InformationBox' import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' +import { partition } from 'lodash' interface Props { - wrapper: Wrapper - tables: any[] - isLoading: boolean - isEnabled: boolean + wrapperMeta: Wrapper + wrappers: any[] isOpen: boolean + isLoading: boolean onOpen: (wrapper: string) => void } -const InputField: FC<{ - option: ServerOption - value: any - error: any - onChange: (e: any) => void -}> = ({ option, value, error, onChange }) => { - const [showHidden, setShowHidden] = useState(!option.hidden) - - if (option.isTextArea) { - return ( -
- -

{option.label}

- {option.urlHelper !== undefined && ( - - - - - - )} -
- } - defaultValue={option.defaultValue ?? ''} - required={option.required ?? false} - value={value} - onChange={onChange} - error={error} - className="input-mono" - rows={6} - /> -
- ) - } else { - return ( - -

{option.label}

- {option.urlHelper !== undefined && ( - - - - - - )} - - } - defaultValue={option.defaultValue ?? ''} - required={option.required ?? false} - value={value} - onChange={onChange} - error={error} - className="input-mono" - type={!option.hidden ? 'text' : showHidden ? 'text' : 'password'} - actions={ - option.hidden ? ( -
-
- ) : null - } - /> - ) - } -} - -const WrapperRow: FC = ({ wrapper, tables, isLoading, isEnabled, isOpen, onOpen }) => { - const getInitialFormState = () => - Object.fromEntries( - wrapper.server.options.map((option) => [option.name, option.defaultValue ?? '']) - ) - +const WrapperRow: FC = ({ wrapperMeta, wrappers = [], isOpen, isLoading, onOpen }) => { const { ui } = useStore() const { ref } = useParams() const { project } = useProjectContext() - const { mutateAsync: createFDW } = useFDWCreateMutation() const { mutateAsync: deleteFDW } = useFDWDeleteMutation() - const [isSubmitting, setIsSubmitting] = useState(false) - const [isEditingTable, setIsEditingTable] = useState(false) - const [selectedTableToEdit, setSelectedTableToEdit] = useState() - const [newTables, setNewTables] = useState([]) - const [formState, setFormState] = useState(getInitialFormState) - const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({}) + console.log({ wrappers }) - const noChanges = JSON.stringify(getInitialFormState()) === JSON.stringify(formState) - - 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 onSaveWrapper = async () => { - const validate = makeValidateRequired(wrapper.server.options) - const errors: any = validate(formState) - if (newTables.length === 0) errors.tables = 'Please add at least one table' - - if (!isEmpty(errors)) { - setFormErrors(errors) - return - } - - setIsSubmitting(true) - try { - await createFDW({ - projectRef: project?.ref, - connectionString: project?.connectionString, - wrapper, - formState, - newTables, - }) - ui.setNotification({ - category: 'success', - message: `Successfully created ${wrapper.label} foreign data wrapper`, - }) - setNewTables([]) - setFormState(getInitialFormState) - } catch (error: any) { - ui.setNotification({ - error, - category: 'error', - message: `Failed to create ${wrapper.label} foreign data wrapper: ${error.message}`, - }) - } finally { - setIsSubmitting(false) - } - } - - const onDeleteWrapper = () => { + const onDeleteWrapper = (wrapper: any) => { confirmAlert({ - title: `Confirm to disable ${wrapper.label} wrapper`, - message: `Are you sure you want to disable the ${wrapper.label} wrapper? This will also remove all tables created with this wrapper.`, + title: `Confirm to disable ${wrapper.name}`, + message: `Are you sure you want to disable the ${wrapper.name} wrapper? This will also remove all tables created with this wrapper.`, onAsyncConfirm: async () => { try { await deleteFDW({ projectRef: project?.ref, connectionString: project?.connectionString, - wrapper, + wrapper: wrapperMeta, }) ui.setNotification({ category: 'success', - message: `Successfully removed ${wrapper.label} foreign data wrapper`, + message: `Successfully disabled ${wrapper.name} foreign data wrapper`, }) } catch (error: any) { ui.setNotification({ error, category: 'error', - message: `Disabling ${wrapper.name} failed: ${error.message}`, + message: `Failed to disable ${wrapper.name}: ${error.message}`, }) } }, @@ -227,7 +67,7 @@ const WrapperRow: FC = ({ wrapper, tables, isLoading, isEnabled, isOpen, <> onOpen(wrapper.name)} + onOpenChange={() => onOpen(wrapperMeta.name)} className={[ 'bg-scale-100 dark:bg-scale-300 ', 'hover:bg-scale-200 dark:hover:bg-scale-500', @@ -235,11 +75,11 @@ const WrapperRow: FC = ({ wrapper, tables, isLoading, isEnabled, isOpen, 'border-scale-300', 'dark:border-scale-500 hover:border-scale-500', 'dark:hover:border-scale-700 data-open:border-scale-700', - 'data-open:pb-px col-span-12 mx-auto', + 'col-span-12 mx-auto', '-space-y-px overflow-hidden', 'transition border shadow hover:z-50', - 'first:rounded-tl first:rounded-tr', - 'last:rounded-bl last:rounded-br', + 'first:rounded-tl first:rounded-tr first:!border-t', + 'last:rounded-bl last:rounded-br last:border-t-0', ].join(' ')} > @@ -254,209 +94,109 @@ const WrapperRow: FC = ({ wrapper, tables, isLoading, isEnabled, isOpen, width={14} /> {`${wrapper.name} - {wrapper.label} + {wrapperMeta.label}
{isLoading ? (
- ) : isEnabled ? ( -
- - - - Enabled -
) : ( -
- Disabled +
+ {wrappers.length} wrapper{wrappers.length > 1 ? 's' : ''}
)}
{!isLoading && ( - <> - {isEnabled ? ( - -
-
- - } - title={`${wrapper.label} foreign data wrapper is currently enabled`} - description="If you'd like to edit this wrapper, you'll need to disable the wrapper first and create it again with any updated configuration." - /> -
-

Foreign Tables

-
- {tables.map((table) => ( -
+
+ {wrappers.map((wrapper) => { + const serverOptions = Object.fromEntries( + wrapper.server_options.map((option: any) => option.split('=')) + ) + const [encryptedMetadata, visibleMetadata] = partition( + wrapperMeta.server.options, + 'hidden' + ) + + return ( +
+
+
+

{wrapper.name}

+
+ {visibleMetadata.map((metadata) => ( +
+

{metadata.label}:

+

{serverOptions[metadata.name]}

+
+ ))} + {encryptedMetadata.map((metadata) => ( +
+

{metadata.label}:

+ -
-

- {table.schema}.{table.name} -

-

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

-
+ + Encrypted in Vault + + + +
+ ))} +
+

+ Foreign tables: ({wrapper.tables.length}) +

+
+ {wrapper.tables.map((table: any) => ( - +
+ {table.name} +
-
- ))} + ))} +
-
-
- - - - - - -
-
-
-
- - ) : ( - -
-
- {wrapper.server.options.map((option) => ( - { - setFormState((prev) => ({ ...prev, [option.name]: e.target.value })) - setFormErrors((prev) => ({ ...prev, [option.name]: '' })) - }} +
+ -
-
-
- {newTables.length === 0 && ( -
-

- Add foreign tables to query from after the wrapper is enabled -

-
- )} - {newTables.map((table, i) => ( -
-
-

- {table.schema_name}.{table.table_name} -

-

- {wrapper.tables[table.index].label}: {table.columns.join(', ')} -

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

{formErrors.tables}

- )} -
-
- - - - - -
- - -
-
-
- - )} - + ) + })} +
+ )} - { - setSelectedTableToEdit(undefined) - setIsEditingTable(false) - }} - onSave={onUpdateTable} - initialData={selectedTableToEdit} - /> ) } diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts index 3d61062903f..75c10baf29a 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts @@ -10,15 +10,7 @@ export const WRAPPERS: Wrapper[] = [ label: 'Stripe', docsUrl: 'https://supabase.com/docs/guides/database/wrappers/stripe', server: { - name: 'stripe_server', options: [ - { - name: 'name', - label: 'Wrapper Name', - required: true, - encrypted: false, - hidden: false, - }, { name: 'api_key_id', label: 'Stripe Secret Key', @@ -422,7 +414,6 @@ export const WRAPPERS: Wrapper[] = [ label: 'Firebase', docsUrl: 'https://supabase.com/docs/guides/database/wrappers/firebase', server: { - name: 'firebase_server', options: [ { name: 'project_id', diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx index 15a0236dc8f..1cf7e0acca4 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx @@ -11,6 +11,7 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader' import WrappersDropdown from './WrappersDropdown' import WrappersDisabledState from './WrappersDisabledState' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { groupBy } from 'lodash' const Wrappers = () => { const { meta } = useStore() @@ -19,11 +20,11 @@ const Wrappers = () => { projectRef: project?.ref, connectionString: project?.connectionString, }) - const wrappers = data?.result ?? [] - const enabledWrapperNamesSet = new Set(wrappers.map((fdw) => fdw.name)) - const [open, setOpen] = useState('') + const wrappers = data?.result ?? [] + const groupedWrappers = groupBy(wrappers, 'handler') + const wrappersExtension = meta.extensions.byId('wrappers') const vaultExtension = meta.extensions.byId('supabase_vault') const isLoadingExtensions = meta.extensions.isLoading @@ -75,21 +76,26 @@ const Wrappers = () => {
) : ( <> + {/* [Joshen] This probably needs to change anyways so dont get too stuck with this */} {WRAPPERS.map((wrapper, i) => { - return ( - w.name === wrapper.server.name)?.tables ?? []} - isLoading={isLoading} - isEnabled={enabledWrapperNamesSet.has(wrapper.server.name)} - isOpen={open === wrapper.name} - onOpen={(wrapperName) => { - if (open !== wrapperName) setOpen(wrapperName) - else setOpen('') - }} - /> - ) + const createdWrappers = groupedWrappers[wrapper.handlerName] ?? [] + if (createdWrappers.length > 0) { + return ( + { + if (open !== wrapperName) setOpen(wrapperName) + else setOpen('') + }} + /> + ) + } else { + return <> + } })} )} diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts index 25ac12d203b..800e3d3cfd3 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts @@ -22,7 +22,6 @@ export type ServerOption = { } export type Server = { - name: string options: ServerOption[] } diff --git a/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx b/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx index 38d514c6c11..cbc6cbb9c11 100644 --- a/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx +++ b/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx @@ -1,25 +1,31 @@ import Link from 'next/link' import { observer } from 'mobx-react-lite' -import { FC, Fragment, useState } from 'react' -import { IconSearch, Input, Button, Listbox, IconLoader, IconExternalLink } from 'ui' +import { FC, Fragment, useState, useEffect } from 'react' +import { IconSearch, Input, Button, Listbox, IconLoader, IconExternalLink, IconX } from 'ui' +import { useStore, useParams } from 'hooks' import SecretRow from './SecretRow' import EditSecretModal from './EditSecretModal' import DeleteSecretModal from './DeleteSecretModal' import AddNewSecretModal from './AddNewSecretModal' import Divider from 'components/ui/Divider' -import { useStore } from 'hooks' interface Props {} const SecretsManagement: FC = ({}) => { const { vault } = useStore() + const { name } = useParams() + const [searchValue, setSearchValue] = useState('') const [selectedSort, setSelectedSort] = useState<'updated_at' | 'name'>('updated_at') const [showAddSecretModal, setShowAddSecretModal] = useState(false) const [selectedSecretToEdit, setSelectedSecretToEdit] = useState() const [selectedSecretToRemove, setSelectedSecretToRemove] = useState() + useEffect(() => { + if (name !== undefined) setSearchValue(name) + }, [name]) + const secrets = ( searchValue.length > 0 ? vault.listSecrets( @@ -48,6 +54,19 @@ const SecretsManagement: FC = ({}) => { value={searchValue} onChange={(event) => setSearchValue(event.target.value)} icon={} + actions={ + searchValue.length > 0 + ? [ +
- } - defaultValue={option.defaultValue ?? ''} - error={error} - className={`${option.name === 'server_name' ? '' : 'input-mono'}`} - type={!option.hidden ? 'text' : showHidden ? 'text' : 'password'} - actions={ - option.hidden ? ( -
-
- ) : 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}

+ +
+ +
+ {({ isSubmitting, handleReset, values, initialValues, resetForm }: any) => { + const [loadingSecrets, setLoadingSecrets] = useState(false) + const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted) + + useEffect(() => { + const fetchEncryptedValues = async () => { + setLoadingSecrets(true) + const res = await Promise.all( + encryptedOptions.map(async (option) => { + const [secret] = vault.listSecrets( + (secret: VaultSecret) => secret.name === `${wrapper.name}_${option.name}` + ) + if (secret !== undefined) { + const value = await vault.fetchSecretValue(secret.id) + return { [option.name]: value } + } 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() + }, []) + + return ( + + +
+ } + > + 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(', ')} +

+
+
+
+
+ ))} +
+ )} + {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 ? ( +
+
+ ) : 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,
- + + +
+ ) } const onUpdateTable = (values: any) => { @@ -105,7 +121,7 @@ const CreateWrapper = () => { } return ( - <> +
{ onSave={onUpdateTable} initialData={selectedTableToEdit} /> - + ) } diff --git a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx index c5ebac4f210..2ba5ba6c170 100644 --- a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx @@ -3,7 +3,16 @@ 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 { + Button, + Form, + Input, + IconArrowLeft, + IconExternalLink, + IconEdit, + IconTrash, + IconLoader, +} from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' import { VaultSecret } from 'types' @@ -27,6 +36,7 @@ import { FormSection, FormSectionLabel, FormSectionContent, + FormsContainer, } from 'components/ui/Forms' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -50,6 +60,7 @@ const EditWrapper = () => { const [wrapperTables, setWrapperTables] = useState([]) const [isEditingTable, setIsEditingTable] = useState(false) + const [isSaving, setIsSaving] = useState(false) const [selectedTableToEdit, setSelectedTableToEdit] = useState() const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({}) @@ -71,11 +82,41 @@ const EditWrapper = () => { }, [wrapper?.id]) if (isLoading) { - return + return ( +
+ +
+ ) } if (wrapper === undefined || wrapperMeta === undefined) { - return
Oh no
+ if (isSaving) { + return ( +
+
+ +

Updating wrapper

+
+
+ ) + } else { + return ( +
+
+

Unknown wrapper

+

+ The wrapper ID {id} cannot be found in your project. Head back to select another + wrapper. +

+
+ + + + + +
+ ) + } } const onUpdateTable = (values: any) => { @@ -103,6 +144,7 @@ const EditWrapper = () => { if (wrapperTables.length === 0) errors.tables = 'Please add at least one table' if (!isEmpty(errors)) return setFormErrors(errors) + setIsSaving(true) setSubmitting(true) try { await deleteFDW({ @@ -133,12 +175,13 @@ const EditWrapper = () => { message: `Failed to create ${wrapperMeta.label} foreign data wrapper: ${error.message}`, }) } finally { + setIsSaving(false) setSubmitting(false) } } return ( - <> +
{ onSave={onUpdateTable} initialData={selectedTableToEdit} /> - + ) } diff --git a/studio/pages/project/[ref]/database/wrappers/[id].tsx b/studio/pages/project/[ref]/database/wrappers/[id].tsx index 9d7cd002380..a5abeb5df61 100644 --- a/studio/pages/project/[ref]/database/wrappers/[id].tsx +++ b/studio/pages/project/[ref]/database/wrappers/[id].tsx @@ -5,7 +5,6 @@ import { NextPageWithLayout } from 'types' import { checkPermissions } from 'hooks' import { DatabaseLayout } from 'components/layouts' import NoPermission from 'components/ui/NoPermission' -import { FormsContainer } from 'components/ui/Forms' import { EditWrapper } from 'components/interfaces/Database' const DatabaseWrappersNew: NextPageWithLayout = () => { @@ -17,10 +16,6 @@ const DatabaseWrappersNew: NextPageWithLayout = () => { return } -DatabaseWrappersNew.getLayout = (page) => ( - - {page} - -) +DatabaseWrappersNew.getLayout = (page) => {page} export default observer(DatabaseWrappersNew) diff --git a/studio/pages/project/[ref]/database/wrappers/new.tsx b/studio/pages/project/[ref]/database/wrappers/new.tsx index 7d5987fef99..4db0245131f 100644 --- a/studio/pages/project/[ref]/database/wrappers/new.tsx +++ b/studio/pages/project/[ref]/database/wrappers/new.tsx @@ -5,7 +5,6 @@ import { NextPageWithLayout } from 'types' import { checkPermissions } from 'hooks' import { DatabaseLayout } from 'components/layouts' import NoPermission from 'components/ui/NoPermission' -import { FormsContainer } from 'components/ui/Forms' import { CreateWrapper } from 'components/interfaces/Database' const DatabaseWrappersNew: NextPageWithLayout = () => { @@ -17,10 +16,6 @@ const DatabaseWrappersNew: NextPageWithLayout = () => { return } -DatabaseWrappersNew.getLayout = (page) => ( - - {page} - -) +DatabaseWrappersNew.getLayout = (page) => {page} export default observer(DatabaseWrappersNew) From 58a18cfe950fe9a80f121a24664b995f0c42ee6d Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Mon, 9 Jan 2023 23:05:49 +0000 Subject: [PATCH 06/11] adds new update fdw mutation --- .../Database/Wrappers/EditWrapper.tsx | 30 +++---- studio/data/fdw/fdw-create-mutation.ts | 9 +- studio/data/fdw/fdw-delete-mutation.ts | 7 +- studio/data/fdw/fdw-update-mutation.ts | 84 +++++++++++++++++++ studio/data/sql/utils/transaction.ts | 9 ++ 5 files changed, 109 insertions(+), 30 deletions(-) create mode 100644 studio/data/fdw/fdw-update-mutation.ts create mode 100644 studio/data/sql/utils/transaction.ts diff --git a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx index 2ba5ba6c170..7fc186b1552 100644 --- a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx @@ -18,8 +18,7 @@ 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 { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation' import InputField from './InputField' import { WRAPPERS } from './Wrappers.constants' @@ -55,8 +54,7 @@ const EditWrapper = () => { 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 { mutateAsync: updateFDW } = useFDWUpdateMutation() const [wrapperTables, setWrapperTables] = useState([]) const [isEditingTable, setIsEditingTable] = useState(false) @@ -83,7 +81,7 @@ const EditWrapper = () => { if (isLoading) { return ( -
+
) @@ -92,8 +90,8 @@ const EditWrapper = () => { if (wrapper === undefined || wrapperMeta === undefined) { if (isSaving) { return ( -
-
+
+

Updating wrapper

@@ -101,10 +99,10 @@ const EditWrapper = () => { ) } else { return ( -
+

Unknown wrapper

-

+

The wrapper ID {id} cannot be found in your project. Head back to select another wrapper.

@@ -147,17 +145,11 @@ const EditWrapper = () => { setIsSaving(true) setSubmitting(true) try { - await deleteFDW({ - projectRef: project?.ref, - connectionString: project?.connectionString, - wrapper, - wrapperMeta, - }) - - await createFDW({ + await updateFDW({ projectRef: project?.ref, connectionString: project?.connectionString, wrapper: wrapperMeta, + wrapperMeta, formState: { ...values, server_name: `${wrapper_name}_server` }, tables: wrapperTables, }) @@ -252,7 +244,7 @@ const EditWrapper = () => { +
{
)} {wrapperTables.length === 0 && formErrors.tables && ( -

{formErrors.tables}

+

{formErrors.tables}

)} diff --git a/studio/data/fdw/fdw-create-mutation.ts b/studio/data/fdw/fdw-create-mutation.ts index 4d56cf7d863..1cc30aaf0a1 100644 --- a/studio/data/fdw/fdw-create-mutation.ts +++ b/studio/data/fdw/fdw-create-mutation.ts @@ -2,6 +2,7 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react import { AvailableColumn, Wrapper } from 'components/interfaces/Database/Wrappers/Wrappers.types' import { executeSql } from 'data/sql/execute-sql-query' import { sqlKeys } from 'data/sql/keys' +import { wrapWithTransaction } from 'data/sql/utils/transaction' import { useStore } from 'hooks' export type FDWCreateVariables = { @@ -14,7 +15,7 @@ export type FDWCreateVariables = { tables: any[] } -export function getFDWCreateSql({ +export function getCreateFDWSql({ wrapper, formState, tables, @@ -112,8 +113,6 @@ export function getFDWCreateSql({ .join('\n\n') const sql = /* SQL */ ` - begin; - ${newSchemasSql} ${createWrapperSql} @@ -123,8 +122,6 @@ export function getFDWCreateSql({ ${createServerSql} ${createTablesSql} - - commit; ` return sql @@ -137,7 +134,7 @@ export async function createFDW({ formState, tables, }: FDWCreateVariables) { - const sql = getFDWCreateSql({ wrapper, formState, tables }) + const sql = wrapWithTransaction(getCreateFDWSql({ wrapper, formState, tables })) const { result } = await executeSql({ projectRef, connectionString, sql }) diff --git a/studio/data/fdw/fdw-delete-mutation.ts b/studio/data/fdw/fdw-delete-mutation.ts index 4e7b98db373..c3d4940fc31 100644 --- a/studio/data/fdw/fdw-delete-mutation.ts +++ b/studio/data/fdw/fdw-delete-mutation.ts @@ -2,6 +2,7 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react import { Wrapper } from 'components/interfaces/Database/Wrappers/Wrappers.types' import { executeSql } from 'data/sql/execute-sql-query' import { sqlKeys } from 'data/sql/keys' +import { wrapWithTransaction } from 'data/sql/utils/transaction' import { useStore } from 'hooks' export type FDWDeleteVariables = { @@ -30,13 +31,9 @@ export const getDeleteFDWSql = ({ const deleteEncryptedSecretsSql = deleteEncryptedSecretsSqlArray.join('\n') const sql = /* SQL */ ` - begin; - drop foreign data wrapper if exists ${wrapper.name} cascade; ${deleteEncryptedSecretsSql} - - commit; ` return sql @@ -52,7 +49,7 @@ export async function deleteFDW({ throw new Error('projectRef is required') } - const sql = getDeleteFDWSql({ wrapper, wrapperMeta }) + const sql = wrapWithTransaction(getDeleteFDWSql({ wrapper, wrapperMeta })) const { result } = await executeSql({ projectRef, connectionString, sql }) diff --git a/studio/data/fdw/fdw-update-mutation.ts b/studio/data/fdw/fdw-update-mutation.ts new file mode 100644 index 00000000000..d3e2fe130da --- /dev/null +++ b/studio/data/fdw/fdw-update-mutation.ts @@ -0,0 +1,84 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { Wrapper } from 'components/interfaces/Database/Wrappers/Wrappers.types' +import { executeSql } from 'data/sql/execute-sql-query' +import { sqlKeys } from 'data/sql/keys' +import { wrapWithTransaction } from 'data/sql/utils/transaction' +import { useStore } from 'hooks' +import { getCreateFDWSql } from './fdw-create-mutation' +import { getDeleteFDWSql } from './fdw-delete-mutation' + +export type FDWUpdateVariables = { + projectRef?: string + connectionString?: string + wrapper: Wrapper + wrapperMeta: Wrapper + formState: { + [k: string]: string + } + tables: any[] +} + +export const getUpdateFDWSql = ({ + wrapper, + wrapperMeta, + formState, + tables, +}: Pick) => { + const deleteWrapperSql = getDeleteFDWSql({ wrapper, wrapperMeta }) + const createWrapperSql = getCreateFDWSql({ + wrapper: wrapperMeta, + formState, + tables, + }) + + const sql = /* SQL */ ` + ${deleteWrapperSql} + + ${createWrapperSql} + ` + + return sql +} + +export async function updateFDW({ + projectRef, + connectionString, + wrapper, + wrapperMeta, + formState, + tables, +}: FDWUpdateVariables) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const sql = wrapWithTransaction(getUpdateFDWSql({ wrapper, wrapperMeta, formState, tables })) + + const { result } = await executeSql({ projectRef, connectionString, sql }) + + return result +} + +type FDWUpdateData = Awaited> + +export const useFDWUpdateMutation = ({ + onSuccess, + ...options +}: Omit, 'mutationFn'> = {}) => { + const queryClient = useQueryClient() + const { vault } = useStore() + + return useMutation((vars) => updateFDW(vars), { + async onSuccess(data, variables, context) { + const { projectRef } = variables + + await Promise.all([ + queryClient.invalidateQueries(sqlKeys.query(projectRef, ['fdws'])), + vault.load(), + ]) + + await onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/studio/data/sql/utils/transaction.ts b/studio/data/sql/utils/transaction.ts new file mode 100644 index 00000000000..36fa24b2571 --- /dev/null +++ b/studio/data/sql/utils/transaction.ts @@ -0,0 +1,9 @@ +export function wrapWithTransaction(sql: string) { + return /* SQL */ ` + begin; + + ${sql} + + commit; + ` +} From 78854b06fa7c0652511c599a0d16f8eb11660604 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 10 Jan 2023 15:11:14 +0800 Subject: [PATCH 07/11] Small fixes --- .../Database/Wrappers/CreateWrapper.tsx | 2 +- .../Database/Wrappers/EditWrapper.tsx | 14 ++++++++--- .../Database/Wrappers/WrapperRow.tsx | 16 ++++++------ .../Database/Wrappers/Wrappers.constants.ts | 4 +-- .../Database/Wrappers/Wrappers.types.ts | 2 +- studio/data/fdw/fdw-create-mutation.ts | 25 +++++++++++-------- studio/data/fdw/fdw-delete-mutation.ts | 7 +++--- studio/data/fdw/fdw-update-mutation.ts | 13 ++++------ studio/data/fdw/fdws-query.ts | 2 +- 9 files changed, 46 insertions(+), 39 deletions(-) diff --git a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx index 4058fb2b0c2..e97ddf91e98 100644 --- a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx @@ -99,7 +99,7 @@ const CreateWrapper = () => { await createFDW({ projectRef: project?.ref, connectionString: project?.connectionString, - wrapper: wrapperMeta, + wrapperMeta, formState: { ...values, server_name: `${wrapper_name}_server` }, tables: newTables, }) diff --git a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx index 7fc186b1552..85519c91539 100644 --- a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx @@ -148,7 +148,7 @@ const EditWrapper = () => { await updateFDW({ projectRef: project?.ref, connectionString: project?.connectionString, - wrapper: wrapperMeta, + wrapper, wrapperMeta, formState: { ...values, server_name: `${wrapper_name}_server` }, tables: wrapperTables, @@ -206,7 +206,12 @@ const EditWrapper = () => {
{({ isSubmitting, handleReset, values, initialValues, resetForm }: any) => { const [loadingSecrets, setLoadingSecrets] = useState(false) - const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + + const initialTables = formatWrapperTables(wrapper?.tables ?? []) + const hasFormChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + const hasTableChanges = JSON.stringify(initialTables) !== JSON.stringify(wrapperTables) + const hasChanges = hasFormChanges || hasTableChanges + const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted) useEffect(() => { @@ -249,7 +254,10 @@ const EditWrapper = () => { form={formId} isSubmitting={isSubmitting} hasChanges={hasChanges} - handleReset={handleReset} + handleReset={() => { + handleReset() + setWrapperTables(initialTables) + }} helper={ !canCreateWrapper ? 'You need additional permissions to create a foreign data wrapper' diff --git a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx index 0e336cdf86d..a79ca3d972b 100644 --- a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' import Image from 'next/image' -import { FC, useState } from 'react' +import { FC } from 'react' import { Collapsible, IconChevronUp, @@ -10,28 +10,28 @@ import { IconTrash, IconEdit, } from 'ui' +import { partition } from 'lodash' import { useParams, useStore } from 'hooks' -import { Wrapper } from './Wrappers.types' +import { WrapperMeta } from './Wrappers.types' +import { FDW } from 'data/fdw/fdws-query' import { useFDWDeleteMutation } from 'data/fdw/fdw-delete-mutation' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' -import { partition } from 'lodash' interface Props { - wrapperMeta: Wrapper - wrappers: any[] + wrappers: FDW[] + wrapperMeta: WrapperMeta isOpen: boolean isLoading: boolean onOpen: (wrapper: string) => void } -const WrapperRow: FC = ({ wrapperMeta, wrappers = [], isOpen, isLoading, onOpen }) => { +const WrapperRow: FC = ({ wrappers = [], wrapperMeta, isOpen, isLoading, onOpen }) => { const { ui } = useStore() const { ref } = useParams() const { project } = useProjectContext() const { mutateAsync: deleteFDW } = useFDWDeleteMutation() - const [isSubmitting, setIsSubmitting] = useState(false) const onDeleteWrapper = (wrapper: any) => { confirmAlert({ @@ -176,7 +176,6 @@ const WrapperRow: FC = ({ wrapperMeta, wrappers = [], isOpen, isLoading, - + {isWrappersEnabled && }
{isLoadingExtensions ? ( -
+
diff --git a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx index 88f6fd36404..b74c9a9fffb 100644 --- a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' import Image from 'next/image' -import { FC } from 'react' +import { FC, Fragment } from 'react' import { observer } from 'mobx-react-lite' import { Button, Dropdown, IconPlus } from 'ui' @@ -23,7 +23,7 @@ const WrapperDropdown: FC = ({ buttonText = 'Add wrapper', align = 'end' overlay={ <> {WRAPPERS.map((wrapper, idx) => ( - <> + @@ -44,7 +44,7 @@ const WrapperDropdown: FC = ({ buttonText = 'Add wrapper', align = 'end' {idx !== WRAPPERS.length - 1 && } - + ))} } From ac3d086cd825ade40001c87fa3035bcd45fa4a2e Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 10 Jan 2023 15:58:53 +0000 Subject: [PATCH 09/11] removes intermediate saving screen on updating wrappers --- .../interfaces/Database/Wrappers/EditWrapper.tsx | 13 +++++++------ .../interfaces/Database/Wrappers/Wrappers.tsx | 4 ++-- studio/hooks/index.ts | 1 + studio/hooks/misc/useImmutableValue.tsx | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 studio/hooks/misc/useImmutableValue.tsx diff --git a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx index 85519c91539..2286ec04c7f 100644 --- a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx @@ -16,7 +16,7 @@ import { import { PermissionAction } from '@supabase/shared-types/out/constants' import { VaultSecret } from 'types' -import { checkPermissions, useParams, useStore } from 'hooks' +import { checkPermissions, useImmutableValue, useParams, useStore } from 'hooks' import { useFDWsQuery } from 'data/fdw/fdws-query' import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation' @@ -50,15 +50,18 @@ const EditWrapper = () => { projectRef: project?.ref, connectionString: project?.connectionString, }) + const wrappers = data?.result ?? [] - const wrapper = wrappers.find((w) => Number(w.id) === Number(id)) + const foundWrapper = wrappers.find((w) => Number(w.id) === Number(id)) + // this call to useImmutableValue should be removed if the redirect after update is also removed + const wrapper = useImmutableValue(foundWrapper) + const wrapperMeta = WRAPPERS.find((w) => w.handlerName === wrapper?.handler) - const { mutateAsync: updateFDW } = useFDWUpdateMutation() + const { mutateAsync: updateFDW, isLoading: isSaving } = useFDWUpdateMutation() const [wrapperTables, setWrapperTables] = useState([]) const [isEditingTable, setIsEditingTable] = useState(false) - const [isSaving, setIsSaving] = useState(false) const [selectedTableToEdit, setSelectedTableToEdit] = useState() const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({}) @@ -142,7 +145,6 @@ const EditWrapper = () => { if (wrapperTables.length === 0) errors.tables = 'Please add at least one table' if (!isEmpty(errors)) return setFormErrors(errors) - setIsSaving(true) setSubmitting(true) try { await updateFDW({ @@ -167,7 +169,6 @@ const EditWrapper = () => { message: `Failed to create ${wrapperMeta.label} foreign data wrapper: ${error.message}`, }) } finally { - setIsSaving(false) setSubmitting(false) } } diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx index 06ed9c800ed..92032b53870 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx @@ -56,7 +56,7 @@ const Wrappers = () => {
- {isLoadingExtensions ? ( + {isLoadingExtensions || isLoading ? (
@@ -94,7 +94,7 @@ const Wrappers = () => { /> ) } else { - return <> + return null } })} diff --git a/studio/hooks/index.ts b/studio/hooks/index.ts index 53628ed64e5..587af606a57 100644 --- a/studio/hooks/index.ts +++ b/studio/hooks/index.ts @@ -1,6 +1,7 @@ export * from './misc/useStore' export * from './misc/withAuth' export * from './misc/useOptimisticSqlSnippetCreate' +export * from './misc/useImmutableValue' export * from './misc/useNotifications' export * from './misc/usePlatformStatus' export * from './misc/useParams' diff --git a/studio/hooks/misc/useImmutableValue.tsx b/studio/hooks/misc/useImmutableValue.tsx new file mode 100644 index 00000000000..433e0099fef --- /dev/null +++ b/studio/hooks/misc/useImmutableValue.tsx @@ -0,0 +1,14 @@ +import { useRef } from 'react' + +/** + * Once a non-undefined value is passed, it will never change. + */ +export function useImmutableValue(value: T | undefined): T | undefined { + const ref = useRef(value) + + if (!ref.current && value) { + ref.current = value + } + + return ref.current +} From 2be9d05327c6ead846750985984f9d2b3899d3b6 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 10 Jan 2023 16:34:18 +0000 Subject: [PATCH 10/11] refetch fdw query on create --- package-lock.json | 212 +++++++++--------- .../Database/Wrappers/CreateWrapper.tsx | 8 +- studio/data/fdw/fdw-create-mutation.ts | 2 +- studio/package.json | 4 +- 4 files changed, 113 insertions(+), 113 deletions(-) diff --git a/package-lock.json b/package-lock.json index a623b4950f1..3c3c95cece7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9706,75 +9706,6 @@ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" } }, - "node_modules/@tanstack/match-sorter-utils": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz", - "integrity": "sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A==", - "dependencies": { - "remove-accents": "0.4.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kentcdodds" - } - }, - "node_modules/@tanstack/query-core": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.15.1.tgz", - "integrity": "sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.16.1.tgz", - "integrity": "sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q==", - "dependencies": { - "@tanstack/query-core": "4.15.1", - "use-sync-external-store": "^1.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/@tanstack/react-query-devtools": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.16.1.tgz", - "integrity": "sha512-VrDYLmG+OOcvGSZL5avG4R8jhqeMFP7pzW2sh2BWEV9UfI+aocG+CW8y8ygacxuKy48m8Tyo/xfe8H1z9BGb+g==", - "dependencies": { - "@tanstack/match-sorter-utils": "8.1.1", - "superjson": "^1.10.0", - "use-sync-external-store": "^1.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/react-query": "4.16.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@testdeck/core": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@testdeck/core/-/core-0.2.2.tgz", @@ -37583,8 +37514,8 @@ "@supabase/react-data-grid": "^7.1.0-beta.7", "@supabase/shared-types": "^0.1.29", "@supabase/supabase-js": "^2.1.1", - "@tanstack/react-query": "^4.16.1", - "@tanstack/react-query-devtools": "^4.16.1", + "@tanstack/react-query": "^4.22.0", + "@tanstack/react-query-devtools": "^4.22.0", "ajv": "^8.6.3", "analytics-node": "^3.5.0", "awesome-debounce-promise": "^2.1.0", @@ -37717,6 +37648,75 @@ "tailwindcss": ">=3.0.0 || insiders" } }, + "studio/node_modules/@tanstack/match-sorter-utils": { + "version": "8.7.6", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.7.6.tgz", + "integrity": "sha512-2AMpRiA6QivHOUiBpQAVxjiHAA68Ei23ZUMNaRJrN6omWiSFLoYrxGcT6BXtuzp0Jw4h6HZCmGGIM/gbwebO2A==", + "dependencies": { + "remove-accents": "0.4.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kentcdodds" + } + }, + "studio/node_modules/@tanstack/query-core": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.22.0.tgz", + "integrity": "sha512-OeLyBKBQoT265f5G9biReijeP8mBxNFwY7ZUu1dKL+YzqpG5q5z7J/N1eT8aWyKuhyDTiUHuKm5l+oIVzbtrjw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "studio/node_modules/@tanstack/react-query": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.22.0.tgz", + "integrity": "sha512-P9o+HjG42uB/xHR6dMsJaPhtZydSe4v0xdG5G/cEj1oHZAXelMlm67/rYJNQGKgBamKElKogj+HYGF+NY2yHYg==", + "dependencies": { + "@tanstack/query-core": "4.22.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "studio/node_modules/@tanstack/react-query-devtools": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.22.0.tgz", + "integrity": "sha512-YeYFBnfqvb+ZlA0IiJqiHNNSzepNhI1p2o9i8NlhQli9+Zrn230M47OBaBUs8qr3DD1dC2zGB1Dis50Ktz8gAA==", + "dependencies": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "4.22.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "studio/node_modules/@types/d3-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.2.tgz", @@ -44528,38 +44528,6 @@ "mini-svg-data-uri": "^1.2.3" } }, - "@tanstack/match-sorter-utils": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz", - "integrity": "sha512-IdmEekEYxQsoLOR0XQyw3jD1GujBpRRYaGJYQUw1eOT1eUugWxdc7jomh1VQ1EKHcdwDLpLaCz/8y4KraU4T9A==", - "requires": { - "remove-accents": "0.4.2" - } - }, - "@tanstack/query-core": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.15.1.tgz", - "integrity": "sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ==" - }, - "@tanstack/react-query": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.16.1.tgz", - "integrity": "sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q==", - "requires": { - "@tanstack/query-core": "4.15.1", - "use-sync-external-store": "^1.2.0" - } - }, - "@tanstack/react-query-devtools": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.16.1.tgz", - "integrity": "sha512-VrDYLmG+OOcvGSZL5avG4R8jhqeMFP7pzW2sh2BWEV9UfI+aocG+CW8y8ygacxuKy48m8Tyo/xfe8H1z9BGb+g==", - "requires": { - "@tanstack/match-sorter-utils": "8.1.1", - "superjson": "^1.10.0", - "use-sync-external-store": "^1.2.0" - } - }, "@testdeck/core": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@testdeck/core/-/core-0.2.2.tgz", @@ -63365,11 +63333,11 @@ "@supabase/shared-types": "^0.1.29", "@supabase/supabase-js": "^2.1.1", "@tailwindcss/typography": "^0.5.2", - "@tanstack/react-query": "^4.16.1", - "@tanstack/react-query-devtools": "^4.16.1", + "@tanstack/react-query": "^4.22.0", + "@tanstack/react-query-devtools": "^4.22.0", "@testing-library/dom": "^8.19.0", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "*", + "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^13.5.0", "@types/blueimp-md5": "^2.18.0", "@types/file-saver": "^2.0.2", @@ -63484,6 +63452,38 @@ "postcss-selector-parser": "6.0.10" } }, + "@tanstack/match-sorter-utils": { + "version": "8.7.6", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.7.6.tgz", + "integrity": "sha512-2AMpRiA6QivHOUiBpQAVxjiHAA68Ei23ZUMNaRJrN6omWiSFLoYrxGcT6BXtuzp0Jw4h6HZCmGGIM/gbwebO2A==", + "requires": { + "remove-accents": "0.4.2" + } + }, + "@tanstack/query-core": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.22.0.tgz", + "integrity": "sha512-OeLyBKBQoT265f5G9biReijeP8mBxNFwY7ZUu1dKL+YzqpG5q5z7J/N1eT8aWyKuhyDTiUHuKm5l+oIVzbtrjw==" + }, + "@tanstack/react-query": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.22.0.tgz", + "integrity": "sha512-P9o+HjG42uB/xHR6dMsJaPhtZydSe4v0xdG5G/cEj1oHZAXelMlm67/rYJNQGKgBamKElKogj+HYGF+NY2yHYg==", + "requires": { + "@tanstack/query-core": "4.22.0", + "use-sync-external-store": "^1.2.0" + } + }, + "@tanstack/react-query-devtools": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.22.0.tgz", + "integrity": "sha512-YeYFBnfqvb+ZlA0IiJqiHNNSzepNhI1p2o9i8NlhQli9+Zrn230M47OBaBUs8qr3DD1dC2zGB1Dis50Ktz8gAA==", + "requires": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + } + }, "@types/d3-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.2.tgz", diff --git a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx index e97ddf91e98..dbe8fe10c83 100644 --- a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx @@ -52,10 +52,10 @@ const CreateWrapper = () => { if (wrapperMeta === undefined) { return ( -
+

Unsupported wrapper type

-

+

The wrapper type {type} not supported by the dashboard. Head back to create a different wrapper.

@@ -158,7 +158,7 @@ const CreateWrapper = () => { +
{
)} {newTables.length === 0 && formErrors.tables && ( -

{formErrors.tables}

+

{formErrors.tables}

)} diff --git a/studio/data/fdw/fdw-create-mutation.ts b/studio/data/fdw/fdw-create-mutation.ts index 3611b08d305..5ebd1fb6893 100644 --- a/studio/data/fdw/fdw-create-mutation.ts +++ b/studio/data/fdw/fdw-create-mutation.ts @@ -158,7 +158,7 @@ export const useFDWCreateMutation = ({ const { projectRef } = variables await Promise.all([ - queryClient.invalidateQueries(sqlKeys.query(projectRef, ['fdws'])), + queryClient.invalidateQueries(sqlKeys.query(projectRef, ['fdws']), { refetchType: 'all' }), vault.load(), ]) diff --git a/studio/package.json b/studio/package.json index e35239fa471..400528424c4 100644 --- a/studio/package.json +++ b/studio/package.json @@ -30,8 +30,8 @@ "@supabase/react-data-grid": "^7.1.0-beta.7", "@supabase/shared-types": "^0.1.29", "@supabase/supabase-js": "^2.1.1", - "@tanstack/react-query": "^4.16.1", - "@tanstack/react-query-devtools": "^4.16.1", + "@tanstack/react-query": "^4.22.0", + "@tanstack/react-query-devtools": "^4.22.0", "ajv": "^8.6.3", "analytics-node": "^3.5.0", "awesome-debounce-promise": "^2.1.0", From e22c340fb61d0e275e881b04646b086aa1fb2e47 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 11 Jan 2023 12:27:24 +0800 Subject: [PATCH 11/11] Remove unnecessary isLoading prop --- .../Database/Wrappers/WrapperRow.tsx | 173 ++++++++---------- .../interfaces/Database/Wrappers/Wrappers.tsx | 1 - 2 files changed, 78 insertions(+), 96 deletions(-) diff --git a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx index a79ca3d972b..35b7efbed34 100644 --- a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx @@ -1,15 +1,7 @@ import Link from 'next/link' import Image from 'next/image' import { FC } from 'react' -import { - Collapsible, - IconChevronUp, - Button, - IconExternalLink, - IconLoader, - IconTrash, - IconEdit, -} from 'ui' +import { Collapsible, IconChevronUp, Button, IconExternalLink, IconTrash, IconEdit } from 'ui' import { partition } from 'lodash' import { useParams, useStore } from 'hooks' @@ -23,11 +15,10 @@ interface Props { wrappers: FDW[] wrapperMeta: WrapperMeta isOpen: boolean - isLoading: boolean onOpen: (wrapper: string) => void } -const WrapperRow: FC = ({ wrappers = [], wrapperMeta, isOpen, isLoading, onOpen }) => { +const WrapperRow: FC = ({ wrappers = [], wrapperMeta, isOpen, onOpen }) => { const { ui } = useStore() const { ref } = useParams() const { project } = useProjectContext() @@ -99,101 +90,93 @@ const WrapperRow: FC = ({ wrappers = [], wrapperMeta, isOpen, isLoading, {wrapperMeta.label}
- {isLoading ? ( -
- -
- ) : ( -
- {wrappers.length} wrapper{wrappers.length > 1 ? 's' : ''} -
- )} +
+ {wrappers.length} wrapper{wrappers.length > 1 ? 's' : ''} +
- {!isLoading && ( - -
- {wrappers.map((wrapper) => { - const serverOptions = Object.fromEntries( - wrapper.server_options.map((option: any) => option.split('=')) - ) - const [encryptedMetadata, visibleMetadata] = partition( - wrapperMeta.server.options, - 'hidden' - ) + +
+ {wrappers.map((wrapper) => { + const serverOptions = Object.fromEntries( + wrapper.server_options.map((option: any) => option.split('=')) + ) + const [encryptedMetadata, visibleMetadata] = partition( + wrapperMeta.server.options, + 'hidden' + ) - return ( -
-
-
-

{wrapper.name}

+ return ( +
+
+
+

{wrapper.name}

+
+ {visibleMetadata.map((metadata) => ( +
+

{metadata.label}:

+

{serverOptions[metadata.name]}

- {visibleMetadata.map((metadata) => ( -
( +
+

{metadata.label}:

+ -

{metadata.label}:

-

{serverOptions[metadata.name]}

-
- ))} - {encryptedMetadata.map((metadata) => ( - + ))} +
+

+ Foreign tables: ({wrapper.tables.length}) +

+
+ {wrapper.tables.map((table: any) => ( + + +
+ {table.name} +
-
- ))} -
-

- Foreign tables: ({wrapper.tables.length}) -

-
- {wrapper.tables.map((table: any) => ( - - -
- {table.name} -
-
- - ))} -
+ ))}
-
- - -
- ) - })} -
- - )} +
+ + +
+
+ ) + })} +
+ ) diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx index 92032b53870..d05146b786d 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx @@ -85,7 +85,6 @@ const Wrappers = () => { key={i} wrapperMeta={wrapper} wrappers={createdWrappers} - isLoading={isLoading} isOpen={open === wrapper.name} onOpen={(wrapperName) => { if (open !== wrapperName) setOpen(wrapperName)