diff --git a/.misspell-fixer.ignore b/.misspell-fixer.ignore index 7c90f6e478..e2098177c1 100644 --- a/.misspell-fixer.ignore +++ b/.misspell-fixer.ignore @@ -1 +1,2 @@ ^./i18n +^/packages/ui/internals \ No newline at end of file diff --git a/packages/ui/src/components/Form/Form.tsx b/packages/ui/src/components/Form/Form.tsx index 3be7eb4a3a..487dbca604 100644 --- a/packages/ui/src/components/Form/Form.tsx +++ b/packages/ui/src/components/Form/Form.tsx @@ -88,6 +88,8 @@ export default function Form({ validate, ...props }: Props) { handleReset: formik.handleReset, /** Resets the form with custom values */ resetForm: formik.resetForm, + /** Manually sets a fields value */ + setFieldValue: formik.setFieldValue, })} diff --git a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx index 5cb53d4059..b2693a6ee2 100644 --- a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx @@ -140,9 +140,9 @@ const CreateWrapper = () => { -

Create a {wrapperMeta?.label} Wrapper

+

Create a {wrapperMeta.label} Wrapper

- +
diff --git a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx index 18782f8a2a..bb82a567cd 100644 --- a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx +++ b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx @@ -56,7 +56,6 @@ const EditWrapper = () => { 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() @@ -342,7 +341,8 @@ const EditWrapper = () => { {table.schema_name}.{table.table_name}

- {wrapperMeta.tables[table.index].label}: {table.columns.join(', ')} + {wrapperMeta.tables[table.index].label}:{' '} + {table.columns.map((column: any) => column.name).join(', ')}

diff --git a/studio/components/interfaces/Database/Wrappers/WrapperDynamicColumns.tsx b/studio/components/interfaces/Database/Wrappers/WrapperDynamicColumns.tsx new file mode 100644 index 0000000000..62ca0eff63 --- /dev/null +++ b/studio/components/interfaces/Database/Wrappers/WrapperDynamicColumns.tsx @@ -0,0 +1,164 @@ +import ColumnType from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType' +import useLatest from 'hooks/misc/useLatest' +import { useEffect, useReducer } from 'react' +import { Button, IconX, IconXCircle, Input } from 'ui' + +export type SimpleColumn = { + id: number + name: string + type: string +} + +export type WrapperDynamicColumnsProps = { + initialColumns?: Pick[] + onChange?: (columns: SimpleColumn[]) => void + errors?: any +} + +const DEFAULT_INITIAL_COLUMNS: WrapperDynamicColumnsProps['initialColumns'] = [ + { name: '', type: 'text' }, +] + +type State = { + columns: { + [key: number]: SimpleColumn + } + nextId: number +} + +type Action = + | { + type: 'ADD_COLUMN' + } + | { + type: 'REMOVE_COLUMN' + payload: { + id: number + } + } + | { + type: 'UPDATE_COLUMN' + payload: { + id: number + key: keyof SimpleColumn + value: string + } + } + +const WrapperDynamicColumns = ({ + initialColumns = DEFAULT_INITIAL_COLUMNS, + onChange, + errors = {}, +}: WrapperDynamicColumnsProps) => { + const [state, dispatch] = useReducer( + (state: State, action: Action) => { + switch (action.type) { + case 'ADD_COLUMN': + return { + ...state, + columns: { + ...state.columns, + [state.nextId]: { id: state.nextId, name: '', type: 'text' }, + }, + nextId: state.nextId + 1, + } + case 'REMOVE_COLUMN': + return { + ...state, + columns: Object.fromEntries( + Object.entries(state.columns).filter(([key]) => Number(key) !== action.payload.id) + ), + } + case 'UPDATE_COLUMN': + return { + ...state, + columns: { + ...state.columns, + [action.payload.id]: { + ...state.columns[action.payload.id], + [action.payload.key]: action.payload.value, + }, + }, + } + default: + return state + } + }, + { + columns: Object.fromEntries( + initialColumns.map((column, index) => [index, { ...column, id: index }]) + ), + nextId: initialColumns.length, + } + ) + + const onChangeRef = useLatest(onChange) + useEffect(() => { + onChangeRef.current?.(getColumns(state.columns)) + }, [state.columns]) + + const onAddColumn = () => { + dispatch({ type: 'ADD_COLUMN' }) + } + + const onRemoveColumn = (id: number) => { + dispatch({ type: 'REMOVE_COLUMN', payload: { id } }) + } + + const onUpdateValue = (id: number, key: keyof SimpleColumn, value: string) => { + dispatch({ type: 'UPDATE_COLUMN', payload: { id, key, value } }) + } + + const columns = getColumns(state.columns) + + return ( +
+
+ {columns.map((column, idx) => ( +
+
+ onUpdateValue(column.id, 'name', e.target.value)} + /> + +
+ onUpdateValue(column.id, 'type', value)} + layout="vertical" + className="[&_label]:!p-0" + /> +
+ +
+ + {errors[`columns.${idx}`] && ( + {errors[`columns.${idx}`]} + )} +
+ ))} +
+ + +
+ ) +} + +export default WrapperDynamicColumns + +function getColumns(columns: State['columns']) { + return Object.values(columns).sort((a, b) => a.id - b.id) +} diff --git a/studio/components/interfaces/Database/Wrappers/WrapperTableEditor.tsx b/studio/components/interfaces/Database/Wrappers/WrapperTableEditor.tsx index 7aa5d2fefa..42d4f0c4a1 100644 --- a/studio/components/interfaces/Database/Wrappers/WrapperTableEditor.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrapperTableEditor.tsx @@ -1,9 +1,10 @@ -import { useEffect, useState } from 'react' -import { Form, IconDatabase, Input, Listbox, SidePanel, Modal, IconPlus } from 'ui' +import ActionBar from 'components/interfaces/TableGridEditor/SidePanelEditor/ActionBar' import { useStore } from 'hooks' +import { useEffect, useState } from 'react' +import { Form, IconDatabase, IconPlus, Input, Listbox, Modal, SidePanel } from 'ui' +import WrapperDynamicColumns from './WrapperDynamicColumns' import { Table, TableOption } from './Wrappers.types' import { makeValidateRequired } from './Wrappers.utils' -import ActionBar from 'components/interfaces/TableGridEditor/SidePanelEditor/ActionBar' export type WrapperTableEditorProps = { visible: boolean @@ -66,8 +67,9 @@ const WrapperTableEditor = ({ } > -
+
setSelectedTableIndex(value)} @@ -105,6 +107,38 @@ const WrapperTableEditor = ({ export default WrapperTableEditor const Option = ({ option }: { option: TableOption }) => { + if (option.type === 'select') { + return ( + + {[ + ...(!option.required + ? [ + + --- + , + ] + : []), + ...option.options.map((subOption) => ( + + {subOption.label} + + )), + ]} + + ) + } + return ( column.name), + columns: table.availableColumns ?? [], ...Object.fromEntries(table.options.map((option) => [option.name, option.defaultValue ?? ''])), schema: 'public', schema_name: '', @@ -149,6 +183,7 @@ const TableForm = ({ ...table.options, { name: 'table_name', required: true }, { name: 'columns', required: true }, + ...(table.availableColumns ? [] : [{ name: 'columns.name', required: true }]), ]) return ( @@ -159,7 +194,7 @@ const TableForm = ({ onSubmit={onSubmit} enableReinitialize={true} > - {({ errors, values, resetForm }: any) => { + {({ errors, values, setFieldValue }: any) => { return (
@@ -202,36 +237,49 @@ const TableForm = ({ ))}
- -
- {table.availableColumns.map((column) => { - const isSelected = values.columns.includes(column.name) - return ( -
{ - if (isSelected) { - resetForm({ - values: { - ...values, - columns: values.columns.filter((x: string) => x !== column.name), - }, - }) - } else { - resetForm({ - values: { ...values, columns: values.columns.concat([column.name]) }, - }) - } - }} - > -

{column.name}

-
- ) - })} + +
+ {table.availableColumns ? ( + table.availableColumns.map((column) => { + const isSelected = Boolean( + values.columns.find((col: any) => col.name === column.name) + ) + + return ( +
{ + if (isSelected) { + setFieldValue( + 'columns', + values.columns.filter((col: any) => col.name !== column.name) + ) + } else { + setFieldValue('columns', values.columns.concat([column])) + } + }} + > +

{column.name}

+
+ ) + }) + ) : ( + { + setFieldValue('columns', columns) + }} + errors={errors} + /> + )}
{errors.columns && ( {errors.columns} diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts index fe05d8bb3a..feb16cbd55 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.constants.ts @@ -9,7 +9,7 @@ export const WRAPPERS: WrapperMeta[] = [ icon: `${BASE_PATH}/img/icons/stripe-icon.svg`, extensionName: 'StripeFdw', label: 'Stripe', - docsUrl: 'https://supabase.com/docs/guides/database/wrappers/stripe', + docsUrl: 'https://supabase.github.io/wrappers/stripe/', server: { options: [ { @@ -58,6 +58,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'balance', editable: false, required: true, + type: 'text', }, ], }, @@ -112,6 +113,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'balance_transactions', editable: false, required: true, + type: 'text', }, ], }, @@ -166,6 +168,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'charges', editable: false, required: true, + type: 'text', }, ], }, @@ -204,6 +207,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'customers', editable: false, required: true, + type: 'text', }, // { // name: 'rowid_column', @@ -261,6 +265,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'invoices', editable: false, required: true, + type: 'text', }, ], }, @@ -303,6 +308,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'payment_intents', editable: false, required: true, + type: 'text', }, ], }, @@ -349,6 +355,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'products', editable: false, required: true, + type: 'text', }, // { // name: 'rowid_column', @@ -394,6 +401,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'subscriptions', editable: false, required: true, + type: 'text', }, // { // name: 'rowid_column', @@ -413,7 +421,7 @@ export const WRAPPERS: WrapperMeta[] = [ icon: `${BASE_PATH}/img/icons/firebase-icon.svg`, extensionName: 'FirebaseFdw', label: 'Firebase', - docsUrl: 'https://supabase.com/docs/guides/database/wrappers/firebase', + docsUrl: 'https://supabase.github.io/wrappers/firebase/', server: { options: [ { @@ -471,6 +479,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'auth/users', editable: false, required: true, + type: 'text', }, { name: 'base_url', @@ -478,6 +487,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'https://identitytoolkit.googleapis.com/v1/projects', editable: true, required: true, + type: 'text', }, { name: 'limit', @@ -485,6 +495,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: '10000', editable: true, required: true, + type: 'text', }, ], }, @@ -516,6 +527,7 @@ export const WRAPPERS: WrapperMeta[] = [ placeholder: 'firestore/[collection_id]', editable: true, required: true, + type: 'text', }, { name: 'base_url', @@ -523,6 +535,7 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: 'https://firestore.googleapis.com/v1beta1/projects', editable: true, required: true, + type: 'text', }, { name: 'limit', @@ -530,6 +543,134 @@ export const WRAPPERS: WrapperMeta[] = [ defaultValue: '10000', editable: true, required: true, + type: 'text', + }, + ], + }, + ], + }, + { + name: 's3_wrapper', + handlerName: 's3_fdw_handler', + validatorName: 's3_fdw_validator', + icon: '/img/icons/s3-icon.svg', + extensionName: 'S3Fdw', + label: 'S3', + docsUrl: 'https://supabase.github.io/wrappers/s3/', + server: { + options: [ + { + name: 'vault_access_key_id', + label: 'Access Key ID', + required: true, + encrypted: true, + hidden: true, + }, + { + name: 'vault_secret_access_key', + label: 'Access Key Secret', + required: true, + encrypted: true, + hidden: true, + }, + { + name: 'aws_region', + label: 'AWS Region', + required: true, + encrypted: false, + hidden: false, + defaultValue: 'us-east-1', + }, + ], + }, + tables: [ + { + label: 'S3 File', + description: 'Map to a file in S3 (CSV or JSON only)', + options: [ + { + name: 'uri', + label: 'URI', + editable: true, + required: true, + placeholder: 's3://bucket/s3_table.csv', + type: 'text', + }, + { + name: 'format', + label: 'Format', + editable: true, + required: true, + type: 'select', + defaultValue: 'csv', + options: [ + { label: 'CSV', value: 'csv' }, + { label: 'JSONL (JSON Lines)', value: 'jsonl' }, + ], + }, + { + name: 'has_header', + label: 'Has Header', + editable: true, + required: true, + type: 'select', + defaultValue: 'true', + options: [ + { label: 'True', value: 'true' }, + { label: 'False', value: 'false' }, + ], + }, + { + name: 'compress', + label: 'Compression', + editable: true, + required: false, + type: 'select', + options: [{ label: 'GZIP', value: 'gzip' }], + }, + ], + }, + ], + }, + { + name: 'clickhouse_wrapper', + handlerName: 'click_house_fdw_handler', + validatorName: 'click_house_fdw_validator', + icon: '/img/icons/clickhouse-icon.svg', + extensionName: 'ClickHouseFdw', + label: 'ClickHouse', + docsUrl: 'https://supabase.github.io/wrappers/clickhouse/', + server: { + options: [ + { + name: 'conn_string_id', + label: 'ClickHouse Connection String', + required: true, + encrypted: true, + hidden: true, + }, + ], + }, + tables: [ + { + label: 'ClickHouse Table', + description: 'Map to a ClickHouse Table', + options: [ + { + name: 'table', + label: 'ClickHouse Table Name', + editable: true, + required: true, + placeholder: 'my_clickhouse_table', + type: 'text', + }, + { + name: 'rowid_column', + label: 'Row ID Column', + defaultValue: 'id', + editable: true, + required: true, + type: 'text', }, ], }, diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts index c0da532e9f..d5b57e6693 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.types.ts @@ -25,19 +25,33 @@ export type Server = { options: ServerOption[] } -export type TableOption = { - name: string - defaultValue?: string - editable: boolean - required: boolean - label?: string - placeholder?: string -} +export type TableOption = + | { + name: string + defaultValue?: string + editable: boolean + required: boolean + label?: string + placeholder?: string + type: 'text' + } + | { + name: string + defaultValue?: string + editable: boolean + required: boolean + label?: string + type: 'select' + options: { + label: string + value: string + }[] + } export type Table = { label: string description?: string - availableColumns: AvailableColumn[] + availableColumns?: AvailableColumn[] options: TableOption[] } diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts b/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts index 97545babb4..3c83b64b9a 100644 --- a/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts +++ b/studio/components/interfaces/Database/Wrappers/Wrappers.utils.ts @@ -1,15 +1,40 @@ -export const makeValidateRequired = - (options: { name: string; required: boolean }[]) => (values: any) => { - const requiredOptionsSet = new Set( - options.filter((option) => option.required).map((option) => option.name) - ) +export const makeValidateRequired = (options: { name: string; required: boolean }[]) => { + const requiredOptionsSet = new Set( + options.filter((option) => option.required).map((option) => option.name) + ) + const requiredArrayOptionsSet = new Set( + Array.from(requiredOptionsSet).filter((option) => option.includes('.')) + ) + const requiredArrayOptions = Array.from(requiredArrayOptionsSet) + + return (values: any) => { const errors = Object.fromEntries( Object.entries(values) - .filter( - ([key, value]) => - requiredOptionsSet.has(key) && (Array.isArray(value) ? value.length < 1 : !value) + .flatMap(([key, value]) => + Array.isArray(value) + ? [[key, value], ...value.map((v, i) => [`${key}.${i}`, v])] + : [[key, value]] ) + .filter(([_key, value]) => { + const [key, idx] = _key.split('.') + + if ( + idx !== undefined && + requiredOptionsSet.has(key) && + Object.keys(value).some((subKey) => requiredArrayOptionsSet.has(`${key}.${subKey}`)) + ) { + const arrayOption = requiredArrayOptions.find((option) => option.startsWith(`${key}.`)) + if (arrayOption) { + const subKey = arrayOption.split('.')[1] + return !value[subKey] + } + + return false + } + + return requiredOptionsSet.has(key) && (Array.isArray(value) ? value.length < 1 : !value) + }) .map(([key]) => { if (key === 'table_name') return [key, 'Please provide a name for your table'] else if (key === 'columns') return [key, 'Please select at least one column'] @@ -19,17 +44,15 @@ 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 { + ...Object.fromEntries(table.options.map((option: string) => option.split('='))), index, - columns: table.columns.map((column: any) => column.name), + columns: table.columns, is_new_schema: false, - object: objectValue, schema: table.schema, schema_name: table.schema, table_name: table.name, diff --git a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index 95dbd1011f..6c721abe77 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -16,7 +16,9 @@ interface Props { const DatabaseLayout: FC = ({ title, children }) => { const { meta, ui, vault, backups } = useStore() - const { isLoading } = meta.schemas + const { isLoading: isSchemasLoading } = meta.schemas + const { isLoading: isVaultLoading } = vault + const { isInitialized, error } = meta.tables const project = ui.selectedProject @@ -28,6 +30,7 @@ const DatabaseLayout: FC = ({ title, children }) => { const foreignDataWrappersEnabled = useFlag('foreignDataWrappers') const pgNetExtensionExists = meta.extensions.byId('pg_net') !== undefined + const isLoading = isSchemasLoading || (isVaultEnabled && isVaultLoading) const [loaded, setLoaded] = useState(isInitialized) useEffect(() => { diff --git a/studio/data/fdw/fdw-create-mutation.ts b/studio/data/fdw/fdw-create-mutation.ts index 5ebd1fb689..7066db7776 100644 --- a/studio/data/fdw/fdw-create-mutation.ts +++ b/studio/data/fdw/fdw-create-mutation.ts @@ -88,15 +88,11 @@ export function getCreateFDWSql({ const createTablesSql = tables .map((newTable) => { - const table = wrapperMeta.tables[newTable.index] - const columns: AvailableColumn[] = newTable.columns - .map((name: string) => table.availableColumns.find((c) => c.name === name)) - .filter(Boolean) return /* SQL */ ` - create foreign table ${newTable.schema_name}.${newTable.table_name} ( - ${columns.map((column) => `${column.name} ${column.type}`).join(',\n ')} + create foreign table "${newTable.schema_name}"."${newTable.table_name}" ( + ${columns.map((column) => `"${column.name}" ${column.type}`).join(',\n ')} ) server ${formState.server_name} options ( diff --git a/studio/pages/project/[ref]/database/backups/pitr.tsx b/studio/pages/project/[ref]/database/backups/pitr.tsx index 694e62f97c..16b78736f2 100644 --- a/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -1,6 +1,7 @@ import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' import { Tabs } from 'ui' +import clsx from 'clsx' import { PermissionAction } from '@supabase/shared-types/out/constants' import { NextPageWithLayout } from 'types' @@ -19,23 +20,30 @@ const DatabasePhysicalBackups: NextPageWithLayout = () => { const ref = ui.selectedProject?.ref ?? 'default' return ( -
-

Backups

+
+
+

Backups

- { - if (id === 'scheduled') router.push(`/project/${ref}/database/backups/scheduled`) - }} - > - - - + { + if (id === 'scheduled') router.push(`/project/${ref}/database/backups/scheduled`) + }} + > + + + -
- +
+ +
) diff --git a/studio/pages/project/[ref]/database/backups/scheduled.tsx b/studio/pages/project/[ref]/database/backups/scheduled.tsx index 526108e42f..1f5b875cbc 100644 --- a/studio/pages/project/[ref]/database/backups/scheduled.tsx +++ b/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -1,6 +1,7 @@ import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' -import { IconAlertCircle, IconHelpCircle, IconInfo, IconMessageCircle, Tabs } from 'ui' +import { IconInfo, Tabs } from 'ui' +import clsx from 'clsx' import { PermissionAction } from '@supabase/shared-types/out/constants' import { NextPageWithLayout } from 'types' @@ -8,7 +9,6 @@ import { checkPermissions, useStore } from 'hooks' import { DatabaseLayout } from 'components/layouts' import { BackupsList } from 'components/interfaces/Database' import NoPermission from 'components/ui/NoPermission' -import { FormsContainer } from 'components/ui/Forms' import InformationBox from 'components/ui/InformationBox' const DatabaseScheduledBackups: NextPageWithLayout = () => { @@ -21,7 +21,12 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => { const canReadScheduledBackups = checkPermissions(PermissionAction.READ, 'back_ups') return ( - +

Backups

@@ -74,7 +79,7 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => { )}
-
+
) } diff --git a/studio/pages/project/[ref]/database/wrappers/index.tsx b/studio/pages/project/[ref]/database/wrappers/index.tsx index c5454dff1f..3499414d11 100644 --- a/studio/pages/project/[ref]/database/wrappers/index.tsx +++ b/studio/pages/project/[ref]/database/wrappers/index.tsx @@ -7,7 +7,6 @@ import { checkPermissions } from 'hooks' import { DatabaseLayout } from 'components/layouts' import { Wrappers } from 'components/interfaces/Database' import NoPermission from 'components/ui/NoPermission' -import { FormsContainer } from 'components/ui/Forms' const DatabaseWrappers: NextPageWithLayout = () => { const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables') @@ -20,7 +19,14 @@ const DatabaseWrappers: NextPageWithLayout = () => { DatabaseWrappers.getLayout = (page) => ( - {page} +
+ {page} +
) diff --git a/studio/public/img/icons/clickhouse-icon.svg b/studio/public/img/icons/clickhouse-icon.svg new file mode 100644 index 0000000000..5c61225e33 --- /dev/null +++ b/studio/public/img/icons/clickhouse-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/studio/public/img/icons/s3-icon.svg b/studio/public/img/icons/s3-icon.svg new file mode 100644 index 0000000000..00dbbd82a5 --- /dev/null +++ b/studio/public/img/icons/s3-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file