diff --git a/package-lock.json b/package-lock.json index 51dde1c8beb..95377a815ed 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", @@ -37592,8 +37523,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", "@zip.js/zip.js": "^2.6.61", "ajv": "^8.6.3", "analytics-node": "^3.5.0", @@ -37727,6 +37658,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", @@ -44538,38 +44538,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", @@ -63380,8 +63348,8 @@ "@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": "^8.0.1", @@ -63500,6 +63468,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 new file mode 100644 index 00000000000..dbe8fe10c83 --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx @@ -0,0 +1,294 @@ +import Link from 'next/link' +import { isEmpty } from 'lodash' +import { 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 { checkPermissions, useParams, useStore } from 'hooks' +import { + FormPanel, + FormActions, + FormSection, + FormsContainer, + FormSectionLabel, + FormSectionContent, +} 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 CreateWrapper = () => { + const formId = 'create-wrapper-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 + ? { + wrapper_name: '', + server_name: '', + ...Object.fromEntries( + wrapperMeta.server.options.map((option) => [option.name, option.defaultValue ?? '']) + ), + } + : {} + + if (wrapperMeta === undefined) { + return ( +
+
+

Unsupported wrapper type

+

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

+
+ + + + + +
+ ) + } + + 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) + + 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) + + setSubmitting(true) + try { + await createFDW({ + projectRef: project?.ref, + connectionString: project?.connectionString, + wrapperMeta, + formState: { ...values, server_name: `${wrapper_name}_server` }, + tables: 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 ( + + +
+ } + > + Wrapper Configuration}> + + 0 ? ( + <> + Your wrapper's server name will be{' '} + {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 +

+ + } + > + + {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/EditWrapper.tsx b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx new file mode 100644 index 00000000000..2286ec04c7f --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx @@ -0,0 +1,392 @@ +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, + IconLoader, +} from 'ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { VaultSecret } from 'types' +import { checkPermissions, useImmutableValue, useParams, useStore } from 'hooks' +import { useFDWsQuery } from 'data/fdw/fdws-query' +import { useFDWUpdateMutation } from 'data/fdw/fdw-update-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, + FormsContainer, +} 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 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, isLoading: isSaving } = useFDWUpdateMutation() + + 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) { + 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) => { + 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 updateFDW({ + 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 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(() => { + 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 ( + + { + handleReset() + setWrapperTables(initialTables) + }} + helper={ + !canCreateWrapper + ? 'You need additional permissions to create a foreign data wrapper' + : undefined + } + /> +
+ } + > + 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 75368404eef..35b7efbed34 100644 --- a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx @@ -1,222 +1,50 @@ 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, -} from 'ui' +import { FC } from 'react' +import { Collapsible, IconChevronUp, Button, IconExternalLink, IconTrash, IconEdit } from 'ui' +import { partition } from 'lodash' 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 { 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 WrapperTableEditor from './WrapperTableEditor' -import InformationBox from 'components/ui/InformationBox' import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' interface Props { - wrapper: Wrapper - tables: any[] - isLoading: boolean - isEnabled: boolean + wrappers: FDW[] + wrapperMeta: WrapperMeta isOpen: 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 = ({ wrappers = [], wrapperMeta, isOpen, 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 }>({}) - - 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, + 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 +55,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 +63,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 +82,102 @@ 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) => ( -
-
-

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

-

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

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

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

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

+
+ {wrapper.tables.map((table: any) => ( + + +
+ {table.name} +
+
+ ))}
- + icon={} + className="py-2" + /> + + +
- - ) : ( - -
-
- {wrapper.server.options.map((option) => ( - { - setFormState((prev) => ({ ...prev, [option.name]: e.target.value })) - setFormErrors((prev) => ({ ...prev, [option.name]: '' })) - }} - /> - ))} -
-
-
-

Foreign Tables

- -
-
-
- {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 a987a4533af..cbb51c09408 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts @@ -1,6 +1,6 @@ -import { Wrapper } from './Wrappers.types' +import { WrapperMeta } from './Wrappers.types' -export const WRAPPERS: Wrapper[] = [ +export const WRAPPERS: WrapperMeta[] = [ { name: 'stripe_wrapper', handlerName: 'stripe_fdw_handler', @@ -10,7 +10,6 @@ export const WRAPPERS: Wrapper[] = [ label: 'Stripe', docsUrl: 'https://supabase.com/docs/guides/database/wrappers/stripe', server: { - name: 'stripe_server', options: [ { name: 'api_key_id', @@ -415,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 bc6abe43bab..d05146b786d 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx @@ -2,34 +2,32 @@ 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' +import { groupBy } from 'lodash' 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 [open, setOpen] = useState('') - const [isEnabling, setIsEnabling] = useState(false) + + 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 - const isNotAvailable = wrappersExtension === undefined || vaultExtension === undefined const isWrappersEnabled = wrappersExtension !== undefined && @@ -37,48 +35,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,159 +44,63 @@ 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 && } - - ))} - - } - > - - -
*/} +
+ + + + + + {isWrappersEnabled && } +
- {isLoadingExtensions ? ( -
+ {isLoadingExtensions || isLoading ? ( +
) : 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

+ +
+ ) : ( + <> + {/* [Joshen] This probably needs to change anyways so dont get too stuck with this */} + {WRAPPERS.map((wrapper, i) => { + const createdWrappers = groupedWrappers[wrapper.handlerName] ?? [] + if (createdWrappers.length > 0) { + return ( + { + if (open !== wrapperName) setOpen(wrapperName) + else setOpen('') + }} + /> + ) + } else { + return null + } + })} + + )}
) : ( -
-
-
-
-

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/Wrappers.types.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts index 25ac12d203b..c0da532e9f1 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts @@ -1,4 +1,4 @@ -export type Wrapper = { +export type WrapperMeta = { name: string handlerName: string validatorName: string @@ -22,7 +22,6 @@ export type ServerOption = { } export type Server = { - name: string options: ServerOption[] } diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts index 19b48c207f1..97545babb4f 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts @@ -19,3 +19,24 @@ export const makeValidateRequired = return errors } + +export const formatWrapperTables = (tables: any[]) => { + return tables.map((table, index: number) => { + const object = table.options.find((option: string) => option.startsWith('object=')) + const objectValue = object !== undefined ? object.split('=')[1] : undefined + + return { + index, + columns: table.columns.map((column: any) => column.name), + is_new_schema: false, + object: objectValue, + schema: table.schema, + schema_name: table.schema, + table_name: table.name, + } + }) +} + +export const convertKVStringArrayToJson = (values: string[]) => { + return Object.fromEntries(values.map((value) => value.split('='))) +} 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..b74c9a9fffb --- /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, Fragment } 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..d866b7d8c8c 100644 --- a/studio/components/interfaces/Database/index.ts +++ b/studio/components/interfaces/Database/index.ts @@ -7,6 +7,8 @@ import RolesSettings from './Roles/RolesSettings' import Extensions from './Extensions/Extensions' import Wrappers from './Wrappers/Wrappers' +import CreateWrapper from './Wrappers/CreateWrapper' +import EditWrapper from './Wrappers/EditWrapper' import PublicationsList from './Publications/PublicationsList' import PublicationsTables from './Publications/PublicationsTables' @@ -28,6 +30,8 @@ export { RolesSettings, Extensions, Wrappers, + CreateWrapper, + EditWrapper, PublicationsList, PublicationsTables, BackupsList, diff --git a/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx b/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx index 38d514c6c11..468773ef358 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 { search } = 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 (search !== undefined) setSearchValue(search) + }, [search]) + 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 + ? [ +