Refactor and implement ABAC for project settings page

This commit is contained in:
Joshen Lim committed 2022-07-25 11:30:32 +08:00
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
@@ -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)
@@ -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>
</>
)
})
@@ -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 }
+8 -151
View File
@@ -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>
)
})