Merge pull request #13720 from supabase/feat/moar-wrappers
feat: s3 + clickhouse wrapper ui
This commit is contained in:
@@ -1 +1,2 @@
|
||||
^./i18n
|
||||
^/packages/ui/internals
|
||||
@@ -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,
|
||||
})}
|
||||
</FormContextProvider>
|
||||
</form>
|
||||
|
||||
@@ -140,9 +140,9 @@ const CreateWrapper = () => {
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl text-scale-1200">Create a {wrapperMeta?.label} Wrapper</h3>
|
||||
<h3 className="mb-2 text-xl text-scale-1200">Create a {wrapperMeta.label} Wrapper</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.github.io/wrappers/stripe/">
|
||||
<Link href={wrapperMeta.docsUrl}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
Documentation
|
||||
@@ -238,7 +238,8 @@ const CreateWrapper = () => {
|
||||
{table.schema_name}.{table.table_name}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
{wrapperMeta.tables[table.index].label}: {table.columns.join(', ')}
|
||||
{wrapperMeta.tables[table.index].label}:{' '}
|
||||
{table.columns.map((column: any) => column.name).join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
|
||||
@@ -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}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
{wrapperMeta.tables[table.index].label}: {table.columns.join(', ')}
|
||||
{wrapperMeta.tables[table.index].label}:{' '}
|
||||
{table.columns.map((column: any) => column.name).join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
|
||||
@@ -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<SimpleColumn, 'name' | 'type'>[]
|
||||
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 (
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
{columns.map((column, idx) => (
|
||||
<div key={column.id} className="flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
className="flex-1 [&_label]:!p-0"
|
||||
layout="vertical"
|
||||
label="Name"
|
||||
value={column.name}
|
||||
onChange={(e) => onUpdateValue(column.id, 'name', e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="flex-1">
|
||||
<ColumnType
|
||||
value={column.type}
|
||||
enumTypes={[]}
|
||||
onOptionSelect={(value) => onUpdateValue(column.id, 'type', value)}
|
||||
layout="vertical"
|
||||
className="[&_label]:!p-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => onRemoveColumn(column.id)}
|
||||
icon={<IconX size={14} strokeWidth={1.5} />}
|
||||
type="outline"
|
||||
className="self-end -translate-y-1 !px-2 !py-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errors[`columns.${idx}`] && (
|
||||
<span className="text-red-900 text-sm mt-2">{errors[`columns.${idx}`]}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button type="default" onClick={() => onAddColumn()} className="self-start">
|
||||
Add column
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WrapperDynamicColumns
|
||||
|
||||
function getColumns(columns: State['columns']) {
|
||||
return Object.values(columns).sort((a, b) => a.id - b.id)
|
||||
}
|
||||
@@ -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 = ({
|
||||
}
|
||||
>
|
||||
<SidePanel.Content>
|
||||
<div className="mt-4 space-y-6">
|
||||
<div className="my-4 space-y-6">
|
||||
<Listbox
|
||||
size="small"
|
||||
label="Select a target the table will point to"
|
||||
value={selectedTableIndex}
|
||||
onChange={(value) => setSelectedTableIndex(value)}
|
||||
@@ -105,6 +107,38 @@ const WrapperTableEditor = ({
|
||||
export default WrapperTableEditor
|
||||
|
||||
const Option = ({ option }: { option: TableOption }) => {
|
||||
if (option.type === 'select') {
|
||||
return (
|
||||
<Listbox
|
||||
key={option.name}
|
||||
id={option.name}
|
||||
name={option.name}
|
||||
label={option.label}
|
||||
defaultValue={option.defaultValue ?? ''}
|
||||
>
|
||||
{[
|
||||
...(!option.required
|
||||
? [
|
||||
<Listbox.Option key="empty" value="" label="---">
|
||||
---
|
||||
</Listbox.Option>,
|
||||
]
|
||||
: []),
|
||||
...option.options.map((subOption) => (
|
||||
<Listbox.Option
|
||||
key={subOption.value}
|
||||
id={option.name + subOption.value}
|
||||
value={subOption.value}
|
||||
label={subOption.label}
|
||||
>
|
||||
{subOption.label}
|
||||
</Listbox.Option>
|
||||
)),
|
||||
]}
|
||||
</Listbox>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
key={option.name}
|
||||
@@ -139,7 +173,7 @@ const TableForm = ({
|
||||
|
||||
const initialValues = initialData ?? {
|
||||
table_name: '',
|
||||
columns: table.availableColumns.map((column) => 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 (
|
||||
<div className="space-y-4">
|
||||
<Listbox size="small" name="schema" label="Select a schema for the foreign table">
|
||||
@@ -202,36 +237,49 @@ const TableForm = ({
|
||||
))}
|
||||
|
||||
<div className="form-group">
|
||||
<label className="!w-full">Select the columns to be added to your table</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{table.availableColumns.map((column) => {
|
||||
const isSelected = values.columns.includes(column.name)
|
||||
return (
|
||||
<div
|
||||
key={column.name}
|
||||
className={[
|
||||
'px-2 py-1 bg-scale-500 rounded cursor-pointer transition',
|
||||
`${isSelected ? 'bg-brand-800' : 'hover:bg-scale-700'}`,
|
||||
].join(' ')}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
resetForm({
|
||||
values: {
|
||||
...values,
|
||||
columns: values.columns.filter((x: string) => x !== column.name),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
resetForm({
|
||||
values: { ...values, columns: values.columns.concat([column.name]) },
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className="text-sm">{column.name}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<label className="!w-full">
|
||||
{table.availableColumns
|
||||
? 'Select the columns to be added to your table'
|
||||
: 'Add columns to your table'}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 w-full">
|
||||
{table.availableColumns ? (
|
||||
table.availableColumns.map((column) => {
|
||||
const isSelected = Boolean(
|
||||
values.columns.find((col: any) => col.name === column.name)
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={column.name}
|
||||
className={[
|
||||
'px-2 py-1 rounded cursor-pointer transition',
|
||||
`${isSelected ? 'bg-brand-800' : 'bg-scale-500 hover:bg-scale-700'}`,
|
||||
].join(' ')}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
setFieldValue(
|
||||
'columns',
|
||||
values.columns.filter((col: any) => col.name !== column.name)
|
||||
)
|
||||
} else {
|
||||
setFieldValue('columns', values.columns.concat([column]))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className="text-sm">{column.name}</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<WrapperDynamicColumns
|
||||
initialColumns={values.columns}
|
||||
onChange={(columns) => {
|
||||
setFieldValue('columns', columns)
|
||||
}}
|
||||
errors={errors}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{errors.columns && (
|
||||
<span className="text-red-900 text-sm mt-2">{errors.columns}</span>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,7 +16,9 @@ interface Props {
|
||||
|
||||
const DatabaseLayout: FC<Props> = ({ 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<Props> = ({ title, children }) => {
|
||||
const foreignDataWrappersEnabled = useFlag('foreignDataWrappers')
|
||||
const pgNetExtensionExists = meta.extensions.byId('pg_net') !== undefined
|
||||
|
||||
const isLoading = isSchemasLoading || (isVaultEnabled && isVaultLoading)
|
||||
const [loaded, setLoaded] = useState<boolean>(isInitialized)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
<div className="mx-auto max-w-4xl space-y-6 px-5 pt-12 pb-20">
|
||||
<h3 className="text-xl text-scale-1200">Backups</h3>
|
||||
<div
|
||||
className={clsx(
|
||||
'mx-auto flex flex-col px-5 pt-6 pb-14',
|
||||
'lg:pt-8 lg:px-14 1xl:px-28 2xl:px-32 h-full'
|
||||
)}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-xl text-scale-1200">Backups</h3>
|
||||
|
||||
<Tabs
|
||||
type="underlined"
|
||||
size="small"
|
||||
activeId="pitr"
|
||||
onChange={(id: any) => {
|
||||
if (id === 'scheduled') router.push(`/project/${ref}/database/backups/scheduled`)
|
||||
}}
|
||||
>
|
||||
<Tabs.Panel id="scheduled" label="Scheduled backups" />
|
||||
<Tabs.Panel id="pitr" label="Point in Time" />
|
||||
</Tabs>
|
||||
<Tabs
|
||||
type="underlined"
|
||||
size="small"
|
||||
activeId="pitr"
|
||||
onChange={(id: any) => {
|
||||
if (id === 'scheduled') router.push(`/project/${ref}/database/backups/scheduled`)
|
||||
}}
|
||||
>
|
||||
<Tabs.Panel id="scheduled" label="Scheduled backups" />
|
||||
<Tabs.Panel id="pitr" label="Point in Time" />
|
||||
</Tabs>
|
||||
|
||||
<div className="space-y-8">
|
||||
<PITR />
|
||||
<div className="space-y-8">
|
||||
<PITR />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<FormsContainer>
|
||||
<div
|
||||
className={clsx(
|
||||
'mx-auto flex flex-col px-5 pt-6 pb-14',
|
||||
'lg:pt-8 lg:px-14 1xl:px-28 2xl:px-32 h-full'
|
||||
)}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-xl text-scale-1200">Backups</h3>
|
||||
|
||||
@@ -74,7 +79,7 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</FormsContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
<DatabaseLayout title="Wrappers">
|
||||
<FormsContainer>{page}</FormsContainer>
|
||||
<div
|
||||
className={clsx(
|
||||
'mx-auto flex flex-col px-5 pt-6 pb-14',
|
||||
'lg:pt-8 lg:px-14 1xl:px-28 2xl:px-32 h-full'
|
||||
)}
|
||||
>
|
||||
{page}
|
||||
</div>
|
||||
</DatabaseLayout>
|
||||
)
|
||||
|
||||
|
||||
1
studio/public/img/icons/clickhouse-icon.svg
Normal file
1
studio/public/img/icons/clickhouse-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" style="enable-background:new 0 0 96 96" version="1.1" viewBox="0 0 96 96"><style>.st0{fill:#ffb200}</style><path d="M2.6 0h5.5c1.4 0 2.6 1.2 2.6 2.6v90.8c0 1.4-1.2 2.6-2.6 2.6H2.6C1.2 96 0 94.8 0 93.4V2.6C0 1.2 1.2 0 2.6 0z" class="st0"/><path d="M0 85.3h10.7v8.1c0 1.4-1.2 2.6-2.6 2.6H2.6C1.2 96 0 94.8 0 93.4v-8.1z" style="fill:red"/><path d="M23.9 0h5.5C30.8 0 32 1.2 32 2.6v90.8c0 1.4-1.2 2.6-2.6 2.6h-5.5c-1.4 0-2.6-1.2-2.6-2.6V2.6c0-1.4 1.2-2.6 2.6-2.6zM45.3 0h5.5c1.4 0 2.6 1.2 2.6 2.6v90.8c0 1.4-1.2 2.6-2.6 2.6h-5.5c-1.4 0-2.6-1.2-2.6-2.6V2.6c0-1.4 1.1-2.6 2.6-2.6zM66.6 0h5.5c1.4 0 2.6 1.2 2.6 2.6v90.8c0 1.4-1.2 2.6-2.6 2.6h-5.5c-1.4 0-2.6-1.2-2.6-2.6V2.6C64 1.2 65.2 0 66.6 0zM87.9 37.3h5.5c1.4 0 2.6 1.2 2.6 2.6V56c0 1.4-1.2 2.6-2.6 2.6h-5.5c-1.4 0-2.6-1.2-2.6-2.6V39.9c0-1.4 1.2-2.6 2.6-2.6z" class="st0"/></svg>
|
||||
|
After Width: | Height: | Size: 912 B |
1
studio/public/img/icons/s3-icon.svg
Normal file
1
studio/public/img/icons/s3-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" preserveAspectRatio="xMidYMid" viewBox="-27 0 310 310"><path fill="#8C3123" d="M20.624 53.686 0 64v181.02l20.624 10.254.124-.149V53.828l-.124-.142"/><path fill="#E05243" d="M131 229 20.624 255.274V53.686L131 79.387V229"/><path fill="#8C3123" d="m81.178 187.866 46.818 5.96.294-.678.263-76.77-.557-.6-46.818 5.874v66.214"/><path fill="#8C3123" d="m127.996 229.295 107.371 26.035.169-.269-.003-201.195-.17-.18-107.367 25.996v149.613"/><path fill="#E05243" d="m174.827 187.866-46.831 5.96v-78.048l46.831 5.874v66.214"/><path fill="#5E1F18" d="m174.827 89.631-46.831 8.535-46.818-8.535 46.759-12.256 46.89 12.256"/><path fill="#F2B0A9" d="m174.827 219.801-46.831-8.591-46.818 8.591 46.761 13.053 46.888-13.053"/><path fill="#8C3123" d="m81.178 89.631 46.818-11.586.379-.117V.313L127.996 0 81.178 23.413v66.218"/><path fill="#E05243" d="m174.827 89.631-46.831-11.586V0l46.831 23.413v66.218"/><path fill="#8C3123" d="m127.996 309.428-46.823-23.405v-66.217l46.823 11.582.689.783-.187 75.906-.502 1.351"/><path fill="#E05243" d="m127.996 309.428 46.827-23.405v-66.217l-46.827 11.582v78.04M235.367 53.686 256 64v181.02l-20.633 10.31V53.686"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Reference in New Issue
Block a user