Merge pull request #13720 from supabase/feat/moar-wrappers

feat: s3 + clickhouse wrapper ui
This commit is contained in:
Alaister Young
2023-05-25 16:51:14 +10:00
committed by GitHub
16 changed files with 507 additions and 93 deletions

View File

@@ -1 +1,2 @@
^./i18n
^/packages/ui/internals

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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',
},
],
},

View File

@@ -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[]
}

View File

@@ -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,

View File

@@ -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(() => {

View File

@@ -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 (

View File

@@ -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>
)

View File

@@ -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>
)
}

View File

@@ -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>
)

View 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

View 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