- {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
+ ? 'Select the columns to be added to your table'
+ : 'Add columns to your table'}
+
+
+ {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