mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Update database extensions page to enforce enabling some extensions in their own schema
This commit is contained in:
1 parent
d7b9cb8310
commit
e2e208cecc
6 files changed
+206
-49
No files matched your search
@@ -116,6 +116,7 @@ const RedirectDomains = () => {
|
||||
form="new-domain-form"
|
||||
htmlType="submit"
|
||||
size="medium"
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Add domain
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { FC } from 'react'
|
||||
import { Button, Input, Form, Modal, Listbox, IconPlus, IconDatabase } from '@supabase/ui'
|
||||
import { PostgresExtension, PostgresSchema } from '@supabase/postgres-meta'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
extension: PostgresExtension
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const EnableExtensionModal: FC<Props> = ({ visible, extension, onCancel }) => {
|
||||
const { ui, meta } = useStore()
|
||||
const schemas = meta.schemas.list()
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
if (values.schema === 'custom' && !values.name) errors.name = 'Required field'
|
||||
return errors
|
||||
}
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting }: any) => {
|
||||
setSubmitting(true)
|
||||
if (values.schema === 'custom') {
|
||||
const { error } = await meta.query(`create schema if not exists ${values.name}`)
|
||||
if (error) {
|
||||
return ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Failed to create schema: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { error } = await meta.extensions.create({
|
||||
name: extension.name,
|
||||
schema: values.schema === 'custom' ? values.name : values.schema,
|
||||
version: extension.default_version,
|
||||
cascade: true,
|
||||
})
|
||||
if (error) {
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Failed to toggle ${extension.name.toUpperCase()}: ${error.message}`,
|
||||
})
|
||||
} else {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `${extension.name.toUpperCase()} is on.`,
|
||||
})
|
||||
}
|
||||
|
||||
setSubmitting(false)
|
||||
onCancel()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closable
|
||||
hideFooter
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
size="small"
|
||||
header={
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<h5 className="text-sm text-scale-1200">Confirm to enable</h5>
|
||||
<code className="text-xs">{extension.name}</code>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
initialValues={{
|
||||
name: extension.name, // Name of new schema, if creating new
|
||||
schema: 'custom',
|
||||
}}
|
||||
validate={validate}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting, values, handleReset }: any) => {
|
||||
return (
|
||||
<div className="space-y-4 py-4">
|
||||
<Modal.Content>
|
||||
<Listbox
|
||||
size="small"
|
||||
name="schema"
|
||||
label="Select a schema to enable the extension for"
|
||||
>
|
||||
<Listbox.Option
|
||||
key="custom"
|
||||
id="custom"
|
||||
label={`Create a new schema "${extension.name}"`}
|
||||
value="custom"
|
||||
addOnBefore={() => <IconPlus size={16} strokeWidth={1.5} />}
|
||||
>
|
||||
Create a new schema "{extension.name}"
|
||||
</Listbox.Option>
|
||||
<Modal.Seperator />
|
||||
{/* @ts-ignore */}
|
||||
{schemas.map((schema: PostgresSchema) => {
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={schema.id}
|
||||
id={schema.name}
|
||||
label={schema.name}
|
||||
value={schema.name}
|
||||
addOnBefore={() => <IconDatabase size={16} strokeWidth={1.5} />}
|
||||
>
|
||||
{schema.name}
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
</Modal.Content>
|
||||
|
||||
{values.schema === 'custom' && (
|
||||
<Modal.Content>
|
||||
<Input id="name" name="name" label="Schema name" />
|
||||
</Modal.Content>
|
||||
)}
|
||||
|
||||
<Modal.Seperator />
|
||||
<Modal.Content>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button type="default" disabled={isSubmitting} onClick={() => onCancel()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit" disabled={isSubmitting} loading={isSubmitting}>
|
||||
Enable extension
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnableExtensionModal
|
||||
@@ -4,6 +4,8 @@ import { Badge, IconLoader, Toggle } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
import { EXTENSIONS_IN_OWN_SCHEMA } from './Extensions.constants'
|
||||
import EnableExtensionModal from './EnableExtensionModal'
|
||||
|
||||
interface Props {
|
||||
extension: any
|
||||
@@ -14,10 +16,15 @@ const ExtensionCard: FC<Props> = ({ extension }) => {
|
||||
|
||||
const isOn = extension.installed_version !== null
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showConfirmEnableModal, setShowConfirmEnableModal] = useState(false)
|
||||
|
||||
async function enableExtension() {
|
||||
if (EXTENSIONS_IN_OWN_SCHEMA.includes(extension.name)) {
|
||||
return setShowConfirmEnableModal(true)
|
||||
}
|
||||
|
||||
confirmAlert({
|
||||
title: 'Confirm to turn on',
|
||||
title: 'Confirm to enable extension',
|
||||
message: `Are you sure you want to turn ON "${extension.name}" extension?`,
|
||||
onAsyncConfirm: async () => {
|
||||
try {
|
||||
@@ -50,7 +57,7 @@ const ExtensionCard: FC<Props> = ({ extension }) => {
|
||||
|
||||
async function disableExtension() {
|
||||
confirmAlert({
|
||||
title: 'Confirm to turn off',
|
||||
title: 'Confirm to disable extension',
|
||||
message: `Are you sure you want to turn OFF "${extension.name}" extension?`,
|
||||
onAsyncConfirm: async () => {
|
||||
try {
|
||||
@@ -80,57 +87,62 @@ const ExtensionCard: FC<Props> = ({ extension }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
border-panel-border-light dark:border-panel-border-dark flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
rounded
|
||||
border shadow-sm
|
||||
"
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className="
|
||||
bg-panel-header-light dark:bg-panel-header-dark border-panel-border-light
|
||||
dark:border-panel-border-dark flex
|
||||
border-b p-4 px-6
|
||||
"
|
||||
className={[
|
||||
'border-panel-border-light dark:border-panel-border-dark flex',
|
||||
'flex-col overflow-hidden rounded border shadow-sm',
|
||||
].join(' ')}
|
||||
>
|
||||
<h3 className="text-scale-1200 m-0 h-5 flex-1 truncate text-base uppercase">
|
||||
{extension.name}
|
||||
</h3>
|
||||
{loading ? (
|
||||
<IconLoader className="animate-spin" size={16} />
|
||||
) : (
|
||||
<Toggle
|
||||
size="tiny"
|
||||
checked={isOn}
|
||||
onChange={() => (isOn ? disableExtension() : enableExtension())}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
bg-panel-header-light dark:bg-panel-header-dark
|
||||
bg-panel-secondary-light dark:bg-panel-secondary-dark
|
||||
flex h-full flex-col"
|
||||
>
|
||||
<div className="p-4 px-6">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
<span className="flex-grow capitalize">{extension.comment}</span>
|
||||
</p>
|
||||
<div
|
||||
className={[
|
||||
'bg-panel-header-light dark:bg-panel-header-dark border-panel-border-light',
|
||||
'dark:border-panel-border-dark flex border-b p-4 px-6',
|
||||
].join(' ')}
|
||||
>
|
||||
<h3
|
||||
title={extension.name}
|
||||
className="text-scale-1200 m-0 h-5 flex-1 truncate text-base uppercase"
|
||||
>
|
||||
{extension.name}
|
||||
</h3>
|
||||
{loading ? (
|
||||
<IconLoader className="animate-spin" size={16} />
|
||||
) : (
|
||||
<Toggle
|
||||
size="tiny"
|
||||
checked={isOn}
|
||||
onChange={() => (isOn ? disableExtension() : enableExtension())}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isOn && extension.schema && (
|
||||
<div
|
||||
className={[
|
||||
'bg-panel-header-light dark:bg-panel-header-dark',
|
||||
'bg-panel-secondary-light dark:bg-panel-secondary-dark flex h-full flex-col justify-between',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="p-4 px-6">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
<span className="flex-grow">
|
||||
Schema: <Badge>{`${extension.schema}`}</Badge>
|
||||
</span>
|
||||
<span className="flex-grow capitalize">{extension.comment}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isOn && extension.schema && (
|
||||
<div className="p-4 px-6">
|
||||
<div className="text-scale-1100 text-sm flex-grow space-x-2 flex items-center">
|
||||
<span>Schema:</span>
|
||||
<Badge>{`${extension.schema}`}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EnableExtensionModal
|
||||
visible={showConfirmEnableModal}
|
||||
extension={extension}
|
||||
onCancel={() => setShowConfirmEnableModal(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,3 +12,5 @@ export const HIDDEN_EXTENSIONS = [
|
||||
'pg_visibility',
|
||||
'pgstattuple',
|
||||
]
|
||||
|
||||
export const EXTENSIONS_IN_OWN_SCHEMA = ['postgis', 'pgsodium', 'vault']
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { partition, isNull } from 'lodash'
|
||||
import { Input, IconSearch, Typography } from '@supabase/ui'
|
||||
import { Input, IconSearch } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import ExtensionCard from './ExtensionCard'
|
||||
@@ -45,7 +45,7 @@ const Extensions: FC<Props> = ({}) => {
|
||||
<div className="my-8 w-full space-y-12">
|
||||
{enabledExtensions.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-lg">Enabled</h4>
|
||||
<h4 className="text-lg">Enabled extensions</h4>
|
||||
<div className="mb-4 grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{enabledExtensions.map((extension) => (
|
||||
<ExtensionCard key={extension.name} extension={extension} />
|
||||
@@ -56,7 +56,7 @@ const Extensions: FC<Props> = ({}) => {
|
||||
|
||||
{disabledExtensions.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-lg">Extensions</h4>
|
||||
<h4 className="text-lg">Available extensions</h4>
|
||||
<div className="mb-4 grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{disabledExtensions.map((extension) => (
|
||||
<ExtensionCard key={extension.name} extension={extension} />
|
||||
|
||||
@@ -53,7 +53,7 @@ const CreateBucketModal = ({
|
||||
<Button type="default" onClick={onSelectCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" loading={saving} onClick={onCreateBucket}>
|
||||
<Button type="primary" disabled={saving} loading={saving} onClick={onCreateBucket}>
|
||||
{saving ? 'Creating bucket' : 'Create bucket'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user