mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 21:59:16 +03:00
Refactor and implement ABAC for project settings page
This commit is contained in:
1 parent
52947392ca
commit
e6007929e3
8 files changed
+277
-180
No files matched your search
@@ -71,7 +71,6 @@ const GeneralSettings = observer(() => {
|
||||
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
|
||||
return (
|
||||
<FormPanel
|
||||
disabled
|
||||
footer={
|
||||
<div
|
||||
className={`flex py-4 px-8 ${
|
||||
@@ -80,7 +79,7 @@ const GeneralSettings = observer(() => {
|
||||
>
|
||||
{!canUpdateOrganization && (
|
||||
<p className="text-sm text-scale-1000">
|
||||
You need additional permissions to manage your organization's settings
|
||||
You need additional permissions to manage this organization's settings
|
||||
</p>
|
||||
)}
|
||||
<FormActions
|
||||
|
||||
+33
-14
@@ -1,21 +1,19 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Button } from '@supabase/ui'
|
||||
import { Alert, Button } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { delete_ } from 'lib/common/fetch'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import { Project } from 'types'
|
||||
|
||||
interface Props {
|
||||
project: Project
|
||||
type?: 'danger' | 'default'
|
||||
}
|
||||
interface Props {}
|
||||
|
||||
const DeleteProjectButton: FC<Props> = ({ project, type = 'danger' }) => {
|
||||
const DeleteProjectPanel: FC<Props> = ({}) => {
|
||||
const router = useRouter()
|
||||
const { ui, app } = useStore()
|
||||
const { app, ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -26,6 +24,8 @@ const DeleteProjectButton: FC<Props> = ({ project, type = 'danger' }) => {
|
||||
}
|
||||
|
||||
async function handleDeleteProject() {
|
||||
if (project === undefined) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await delete_(`${API_URL}/projects/${project.ref}`)
|
||||
@@ -42,13 +42,32 @@ const DeleteProjectButton: FC<Props> = ({ project, type = 'danger' }) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (project === undefined) return <></>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<Button onClick={toggle} type={type}>
|
||||
Delete project
|
||||
</Button>
|
||||
</div>
|
||||
<section>
|
||||
<Panel title={<p className="uppercase">Danger Zone</p>}>
|
||||
<Panel.Content>
|
||||
<Alert
|
||||
variant="danger"
|
||||
withIcon
|
||||
title="Deleting this project will also remove your database."
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<p className="mb-4 block">
|
||||
Make sure you have made a backup if you want to keep your data.
|
||||
</p>
|
||||
<div className="flex items-center">
|
||||
<Button onClick={toggle} type="danger">
|
||||
Delete project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
<TextConfirmModal
|
||||
visible={isOpen}
|
||||
loading={loading}
|
||||
@@ -65,4 +84,4 @@ const DeleteProjectButton: FC<Props> = ({ project, type = 'danger' }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteProjectButton
|
||||
export default DeleteProjectPanel
|
||||
@@ -0,0 +1,89 @@
|
||||
import { FC } from 'react'
|
||||
import { Form, Input } from '@supabase/ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import {
|
||||
FormHeader,
|
||||
FormPanel,
|
||||
FormActions,
|
||||
FormSection,
|
||||
FormSectionLabel,
|
||||
FormSectionContent,
|
||||
} from 'components/ui/Forms'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const General: FC<Props> = ({}) => {
|
||||
const { app, ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
const formId = 'project-general-settings'
|
||||
const initialValues = { name: project?.name ?? '' }
|
||||
const canUpdateProject = checkPermissions(PermissionAction.SQL_UPDATE, 'postgres.public.projects')
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting, resetForm }: any) => {
|
||||
const response = await post(`${API_URL}/projects/${project?.ref}/update`, values)
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Update project failed: ${response.error.message}`,
|
||||
})
|
||||
} else {
|
||||
const { name } = response
|
||||
resetForm({ values: { name }, initialValues: { name } })
|
||||
|
||||
app.onProjectUpdated(response)
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully saved settings',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form id={formId} initialValues={initialValues} onSubmit={onSubmit}>
|
||||
{({ isSubmitting, handleReset, values, initialValues }: any) => {
|
||||
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
|
||||
return (
|
||||
<>
|
||||
<FormHeader title="Project Settings" description="" />
|
||||
|
||||
<FormPanel
|
||||
disabled={!canUpdateProject}
|
||||
footer={
|
||||
<div
|
||||
className={`flex py-4 px-8 ${
|
||||
canUpdateProject ? 'justify-end' : 'justify-between'
|
||||
}`}
|
||||
>
|
||||
{!canUpdateProject && (
|
||||
<p className="text-sm text-scale-1000">
|
||||
You need additional permissions to manage this project's settings
|
||||
</p>
|
||||
)}
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isSubmitting}
|
||||
hasChanges={hasChanges}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormSection header={<FormSectionLabel>General settings</FormSectionLabel>}>
|
||||
<FormSectionContent loading={false}>
|
||||
<Input id="name" size="small" label="Project name" disabled={false} />
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
</FormPanel>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export default General
|
||||
@@ -0,0 +1,76 @@
|
||||
import { FC } from 'react'
|
||||
import { Input } from '@supabase/ui'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { useStore, useFlag } from 'hooks'
|
||||
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import {
|
||||
FormHeader,
|
||||
FormPanel,
|
||||
FormSection,
|
||||
FormSectionLabel,
|
||||
FormSectionContent,
|
||||
} from 'components/ui/Forms'
|
||||
import PauseProjectButton from './PauseProjectButton'
|
||||
import RestartServerButton from './RestartServerButton'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const Infrastructure: FC<Props> = ({}) => {
|
||||
const { ui } = useStore()
|
||||
const isProjectPauseEnabled = useFlag('projectPausing')
|
||||
|
||||
const project = ui.selectedProject
|
||||
const isFreeProject = project?.subscription_tier === PRICING_TIER_PRODUCT_IDS.FREE
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormHeader title="Infrastructure" description="" />
|
||||
<FormPanel
|
||||
footer={
|
||||
<>
|
||||
<div className="flex w-full items-center justify-between px-8 py-4">
|
||||
<div>
|
||||
<p className="text-sm">Restart server</p>
|
||||
<div className="max-w-[420px]">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Your project will not be available for a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{project && <RestartServerButton projectId={project.id} projectRef={project.ref} />}
|
||||
</div>
|
||||
|
||||
{isProjectPauseEnabled && isFreeProject && (
|
||||
<>
|
||||
<div className="border-t border-scale-400" />
|
||||
<div className="flex w-full items-center justify-between px-8 py-4">
|
||||
<div>
|
||||
<p className="text-sm">Pause project</p>
|
||||
<div className="max-w-[420px]">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Your project will not be accessible while it is paused.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{project && (
|
||||
<PauseProjectButton projectId={project.id} projectRef={project.ref} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FormSection header={<FormSectionLabel>Configuration</FormSectionLabel>}>
|
||||
<FormSectionContent loading={project === undefined}>
|
||||
<Input readOnly disabled value={project?.cloud_provider} label="Cloud provider" />
|
||||
<Input readOnly disabled value={project?.region} label="Region" />
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
</FormPanel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Infrastructure)
|
||||
+33
-4
@@ -1,9 +1,11 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, IconPause } from '@supabase/ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { checkPermissions, useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
@@ -22,6 +24,8 @@ const PauseProjectButton: FC<Props> = observer(({ projectRef, projectId }) => {
|
||||
const openModal = () => setIsModalOpen(true)
|
||||
const closeModal = () => setIsModalOpen(false)
|
||||
|
||||
const canReboot = checkPermissions(PermissionAction.INFRA_EXECUTE, 'reboot')
|
||||
|
||||
const requestPauseProject = async () => {
|
||||
setLoading(true)
|
||||
const res = await post(`${API_URL}/projects/${projectRef}/pause`, {})
|
||||
@@ -45,6 +49,34 @@ const PauseProjectButton: FC<Props> = observer(({ projectRef, projectId }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconPause />}
|
||||
onClick={openModal}
|
||||
loading={loading}
|
||||
disabled={!canReboot}
|
||||
>
|
||||
Pause Project
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canReboot && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'bg-scale-100 rounded py-1 px-2 leading-none shadow', // background
|
||||
'border-scale-200 border ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-scale-1200 text-xs">
|
||||
You need additional permissions to pause this project
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
<ConfirmModal
|
||||
danger
|
||||
visible={isModalOpen}
|
||||
@@ -55,9 +87,6 @@ const PauseProjectButton: FC<Props> = observer(({ projectRef, projectId }) => {
|
||||
onSelectCancel={closeModal}
|
||||
onSelectConfirm={requestPauseProject}
|
||||
/>
|
||||
<Button type="default" icon={<IconPause />} onClick={openModal} loading={loading}>
|
||||
Pause Project
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
})
|
||||
+33
-4
@@ -1,9 +1,11 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, IconRefreshCcw } from '@supabase/ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
@@ -22,6 +24,8 @@ const RestartServerButton: FC<Props> = observer(({ projectRef, projectId }) => {
|
||||
const openModal = () => setIsModalOpen(true)
|
||||
const closeModal = () => setIsModalOpen(false)
|
||||
|
||||
const canReboot = checkPermissions(PermissionAction.INFRA_EXECUTE, 'reboot')
|
||||
|
||||
const requestServerRestart = async () => {
|
||||
setLoading(true)
|
||||
const res = await post(`${API_URL}/projects/${projectRef}/restart`, {})
|
||||
@@ -43,6 +47,34 @@ const RestartServerButton: FC<Props> = observer(({ projectRef, projectId }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconRefreshCcw />}
|
||||
onClick={openModal}
|
||||
loading={loading}
|
||||
disabled={!canReboot}
|
||||
>
|
||||
Restart server
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canReboot && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'bg-scale-100 rounded py-1 px-2 leading-none shadow', // background
|
||||
'border-scale-200 border ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-scale-1200 text-xs">
|
||||
You need additional permissions to restart this project
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
<ConfirmModal
|
||||
danger
|
||||
visible={isModalOpen}
|
||||
@@ -53,9 +85,6 @@ const RestartServerButton: FC<Props> = observer(({ projectRef, projectId }) => {
|
||||
onSelectCancel={closeModal}
|
||||
onSelectConfirm={requestServerRestart}
|
||||
/>
|
||||
<Button type="default" icon={<IconRefreshCcw />} onClick={openModal} loading={loading}>
|
||||
Restart server
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import PauseProjectButton from './PauseProjectButton'
|
||||
import RestartServerButton from './RestartServerButton'
|
||||
import General from './General'
|
||||
import Infrastructure from './Infrastructure/Infrastructure'
|
||||
import DeleteProjectPanel from './DeleteProjectPanel'
|
||||
|
||||
import DeleteProjectButton from './DeleteProjectButton'
|
||||
|
||||
export { PauseProjectButton, RestartServerButton, DeleteProjectButton }
|
||||
export { General, Infrastructure, DeleteProjectPanel }
|
||||
@@ -1,163 +1,20 @@
|
||||
import { Alert, Input } from '@supabase/ui'
|
||||
import { toJS } from 'mobx'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { projects } from 'stores/jsonSchema'
|
||||
import { AutoField } from 'uniforms-bootstrap4'
|
||||
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { checkPermissions, useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { pluckJsonSchemaFields, pluckObjectFields } from 'lib/helpers'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
import {
|
||||
DeleteProjectButton,
|
||||
PauseProjectButton,
|
||||
RestartServerButton,
|
||||
} from 'components/interfaces/Settings/General'
|
||||
import { useFlag } from 'hooks'
|
||||
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import { FormsContainer } from 'components/ui/Forms'
|
||||
import { General, Infrastructure, DeleteProjectPanel } from 'components/interfaces/Settings/General'
|
||||
|
||||
const ProjectSettings: NextPageWithLayout = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="content h-full w-full overflow-y-auto">
|
||||
<div className="mx-auto w-full">
|
||||
<GeneralSettings />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormsContainer>
|
||||
<General />
|
||||
<Infrastructure />
|
||||
<DeleteProjectPanel />
|
||||
</FormsContainer>
|
||||
)
|
||||
}
|
||||
|
||||
ProjectSettings.getLayout = (page) => <SettingsLayout title="General">{page}</SettingsLayout>
|
||||
|
||||
export default observer(ProjectSettings)
|
||||
|
||||
const GeneralSettings = observer(() => {
|
||||
const { app, ui } = useStore()
|
||||
const isProjectPauseEnabled = useFlag('projectPausing')
|
||||
|
||||
const project = ui.selectedProject
|
||||
const formModel = toJS(project)
|
||||
const BASIC_FIELDS = ['name']
|
||||
|
||||
// @ts-ignore
|
||||
const canReboot = checkPermissions(PermissionAction.TENANT_INFRA_EXECUTE, 'reboot')
|
||||
const canUpdateProject = checkPermissions(PermissionAction.SQL_UPDATE, 'postgres.public.projects')
|
||||
const isFreeProject = project?.subscription_tier === PRICING_TIER_PRODUCT_IDS.FREE
|
||||
|
||||
const handleUpdateProject = async (model: any) => {
|
||||
const response = await post(`${API_URL}/projects/${project?.ref}/update`, model)
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Update project failed: ${response.error.message}`,
|
||||
})
|
||||
} else {
|
||||
const updatedProject = response
|
||||
app.onProjectUpdated(updatedProject)
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully saved settings',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="max-w-4xl p-4">
|
||||
<section>
|
||||
<SchemaFormPanel
|
||||
title="General"
|
||||
schema={pluckJsonSchemaFields(projects, BASIC_FIELDS)}
|
||||
model={formModel}
|
||||
onSubmit={(model: any) => handleUpdateProject(pluckObjectFields(model, BASIC_FIELDS))}
|
||||
>
|
||||
<AutoField
|
||||
name="name"
|
||||
showInlineError
|
||||
errorMessage="Please enter a project name"
|
||||
disabled={!canUpdateProject}
|
||||
/>
|
||||
</SchemaFormPanel>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0 text-base">
|
||||
Infrastructure
|
||||
</h5>
|
||||
}
|
||||
>
|
||||
<Panel.Content>
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
value={project?.cloud_provider}
|
||||
label="Cloud provider"
|
||||
layout="horizontal"
|
||||
/>
|
||||
</Panel.Content>
|
||||
<Panel.Content className="border-panel-border-interior-light dark:border-panel-border-interior-dark border-t">
|
||||
<Input readOnly disabled value={project?.region} label="Region" layout="horizontal" />
|
||||
</Panel.Content>
|
||||
<Panel.Content className="border-panel-border-interior-light dark:border-panel-border-interior-dark border-t">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<p>Restart server</p>
|
||||
<div className="max-w-[420px]">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Your project will not be available for a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
{project && <RestartServerButton projectId={project.id} projectRef={project.ref} />}
|
||||
</div>
|
||||
{project && <RestartServerButton projectId={project.id} projectRef={project.ref} />}
|
||||
</div>
|
||||
</Panel.Content>
|
||||
{isProjectPauseEnabled && isFreeProject && (
|
||||
<Panel.Content className="border-panel-border-interior-light dark:border-panel-border-interior-dark border-t">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<p>Pause project</p>
|
||||
<div className="max-w-[420px]">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Your project will not be accessible while it is paused.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{project && <PauseProjectButton projectId={project.id} projectRef={project.ref} />}
|
||||
</div>
|
||||
</Panel.Content>
|
||||
)}
|
||||
</Panel>
|
||||
</section>
|
||||
|
||||
{project !== undefined && (
|
||||
<section>
|
||||
<Panel title={<p className="uppercase">Danger Zone</p>}>
|
||||
<Panel.Content>
|
||||
<Alert
|
||||
variant="danger"
|
||||
withIcon
|
||||
title="Deleting this project will also remove your database."
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<p className="mb-4 block">
|
||||
Make sure you have made a backup if you want to keep your data.
|
||||
</p>
|
||||
<DeleteProjectButton project={project} />
|
||||
</div>
|
||||
</Alert>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user