mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 15:19:08 +03:00
Implement create and delete roles
This commit is contained in:
1 parent
1291d6d18a
commit
c5be23964c
12 files changed
+473
-198
No files matched your search
@@ -125,7 +125,12 @@ const Modal = ({
|
||||
<Button type="default" onClick={onCancel} disabled={loading}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} loading={loading} danger={variant === 'danger'}>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
danger={variant === 'danger'}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'size'> {
|
||||
layout?: 'horizontal' | 'vertical' | 'flex'
|
||||
error?: string
|
||||
descriptionText?: string
|
||||
label?: string
|
||||
label?: string | React.ReactNode
|
||||
afterLabel?: string
|
||||
beforeLabel?: string
|
||||
labelOptional?: string
|
||||
|
||||
@@ -86,9 +86,7 @@ export function FormLayout({
|
||||
} else {
|
||||
dataInputContainerClasses.push(__styles.data_input_vertical_layout)
|
||||
if (align === 'right') {
|
||||
dataInputContainerClasses.push(
|
||||
__styles.data_input_vertical_layout__align_right
|
||||
)
|
||||
dataInputContainerClasses.push(__styles.data_input_vertical_layout__align_right)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,10 +126,7 @@ export function FormLayout({
|
||||
|
||||
const renderDescription = descriptionText && (
|
||||
<p
|
||||
className={[
|
||||
__styles.description.base,
|
||||
__styles.description.size[size],
|
||||
].join(' ')}
|
||||
className={[__styles.description.base, __styles.description.size[size]].join(' ')}
|
||||
id={id + '-description'}
|
||||
>
|
||||
{descriptionText}
|
||||
@@ -153,19 +148,14 @@ export function FormLayout({
|
||||
>
|
||||
{labelled && (
|
||||
<label
|
||||
className={[
|
||||
__styles.label.base,
|
||||
__styles.label.size[size],
|
||||
'break-all',
|
||||
].join(' ')}
|
||||
className={[__styles.label.base, __styles.label.size[size], 'break-all'].join(' ')}
|
||||
htmlFor={id}
|
||||
>
|
||||
{beforeLabel && (
|
||||
<span
|
||||
className={[
|
||||
__styles.label_before.base,
|
||||
__styles.label_before.size[size],
|
||||
].join(' ')}
|
||||
className={[__styles.label_before.base, __styles.label_before.size[size]].join(
|
||||
' '
|
||||
)}
|
||||
id={id + '-before'}
|
||||
>
|
||||
{beforeLabel}
|
||||
@@ -174,10 +164,7 @@ export function FormLayout({
|
||||
{label}
|
||||
{afterLabel && (
|
||||
<span
|
||||
className={[
|
||||
__styles.label_after.base,
|
||||
__styles.label_after.size[size],
|
||||
].join(' ')}
|
||||
className={[__styles.label_after.base, __styles.label_after.size[size]].join(' ')}
|
||||
id={id + '-after'}
|
||||
>
|
||||
{afterLabel}
|
||||
@@ -187,10 +174,9 @@ export function FormLayout({
|
||||
)}
|
||||
{labelOptional && (
|
||||
<span
|
||||
className={[
|
||||
__styles.label_optional.base,
|
||||
__styles.label_optional.size[size],
|
||||
].join(' ')}
|
||||
className={[__styles.label_optional.base, __styles.label_optional.size[size]].join(
|
||||
' '
|
||||
)}
|
||||
id={id + '-optional'}
|
||||
>
|
||||
{labelOptional}
|
||||
|
||||
@@ -1146,13 +1146,13 @@ export default {
|
||||
},
|
||||
},
|
||||
label_before: {
|
||||
base: 'text-scale-500 ',
|
||||
base: 'text-scale-1000 ',
|
||||
size: {
|
||||
...defaults.size.text,
|
||||
},
|
||||
},
|
||||
label_after: {
|
||||
base: 'text-scale-500',
|
||||
base: 'text-scale-1000',
|
||||
size: {
|
||||
...defaults.size.text,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, Form, Input, IconArrowLeft, IconExternalLink, Toggle, IconHelpCircle } from 'ui'
|
||||
|
||||
import { useParams, useStore } from 'hooks'
|
||||
import {
|
||||
FormActions,
|
||||
FormPanel,
|
||||
FormsContainer,
|
||||
FormSection,
|
||||
FormSectionLabel,
|
||||
FormSectionContent,
|
||||
} from 'components/ui/Forms'
|
||||
import { ROLE_PERMISSIONS } from './Roles.constants'
|
||||
|
||||
interface Props {}
|
||||
|
||||
// [Joshen] There are a lot of other parameters that pg/pg-meta supports for creating a role
|
||||
// but these are just the minimal parameters for now
|
||||
// https://github.com/supabase/postgres-meta/blob/3b4c3a0ea052389599cf5e2900f7185870d6f60c/src/lib/types.ts#L224
|
||||
|
||||
const CreateRole: FC<Props> = ({}) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { ui, meta } = useStore()
|
||||
|
||||
const formId = 'create-new-role'
|
||||
const initialValues = {
|
||||
name: '',
|
||||
is_superuser: false,
|
||||
can_login: false,
|
||||
can_create_role: false,
|
||||
can_create_db: false,
|
||||
is_replication_role: false,
|
||||
can_bypass_rls: false,
|
||||
}
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting }: any) => {
|
||||
setSubmitting(true)
|
||||
const res = await meta.roles.create(values)
|
||||
if (res.error) {
|
||||
setSubmitting(false)
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to create role: ${res.error.message}`,
|
||||
})
|
||||
} else {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully created new role: ${res.name}`,
|
||||
})
|
||||
router.push(`/project/${ref}/database/roles`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormsContainer>
|
||||
<div>
|
||||
<div className="relative flex items-center justify-between mb-6">
|
||||
<div
|
||||
className={[
|
||||
'transition cursor-pointer',
|
||||
'absolute -left-20 top-1 opacity-75 hover:opacity-100',
|
||||
].join(' ')}
|
||||
>
|
||||
<Link href={`/project/${ref}/database/roles`}>
|
||||
<a>
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconArrowLeft strokeWidth={1.5} size={14} />
|
||||
<p className="text-sm">Back</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl text-scale-1200">Create a new role</h3>
|
||||
{/* <div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.github.io/wrappers/stripe/">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<Form id={formId} initialValues={initialValues} onSubmit={onSubmit}>
|
||||
{({ isSubmitting, handleReset, values, initialValues }: any) => {
|
||||
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
|
||||
return (
|
||||
<FormPanel
|
||||
disabled={false}
|
||||
footer={
|
||||
<div className="flex px-8 py-4">
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isSubmitting}
|
||||
hasChanges={hasChanges}
|
||||
handleReset={handleReset}
|
||||
helper={
|
||||
!true
|
||||
? 'You need additional permissions to create a database role'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormSection header={<FormSectionLabel>Role Configuration</FormSectionLabel>}>
|
||||
<FormSectionContent loading={false}>
|
||||
<Input id="name" label="Name" />
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<FormSection header={<FormSectionLabel>Role Privileges</FormSectionLabel>}>
|
||||
<FormSectionContent loading={false}>
|
||||
<div className="space-y-[9px]">
|
||||
{Object.keys(ROLE_PERMISSIONS).map((permission) => (
|
||||
<Toggle
|
||||
size="small"
|
||||
disabled={ROLE_PERMISSIONS[permission].disabled}
|
||||
className={[
|
||||
'roles-toggle',
|
||||
ROLE_PERMISSIONS[permission].disabled ? 'opacity-50' : '',
|
||||
].join(' ')}
|
||||
key={permission}
|
||||
id={permission}
|
||||
name={permission}
|
||||
label={ROLE_PERMISSIONS[permission].description}
|
||||
afterLabel={
|
||||
ROLE_PERMISSIONS[permission].disabled && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger type="button">
|
||||
<IconHelpCircle
|
||||
size="tiny"
|
||||
strokeWidth={2}
|
||||
className="ml-2 relative top-[3px]"
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content align="center" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200 space-y-1',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs">
|
||||
This privilege cannot be granted via the dashboard
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
</FormPanel>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</div>
|
||||
</FormsContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateRole
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { Modal } from 'ui'
|
||||
import { PostgresRole } from '@supabase/postgres-meta'
|
||||
import { useStore } from 'hooks'
|
||||
|
||||
interface Props {
|
||||
role: PostgresRole
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const DeleteRoleModal: FC<Props> = ({ role, visible, onClose }) => {
|
||||
const { ui, meta } = useStore()
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
const deleteRole = async () => {
|
||||
if (!role) return console.error('Failed to delete role: role is missing')
|
||||
|
||||
setIsDeleting(true)
|
||||
const res: any = await meta.roles.del(role.id)
|
||||
setIsDeleting(false)
|
||||
if (res.error) {
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to delete role: ${res.error.message}`,
|
||||
})
|
||||
} else {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully deleted role: "${role.name}"`,
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="small"
|
||||
alignFooter="right"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
onConfirm={deleteRole}
|
||||
header={<h3>Confirm to delete role "{role?.name}"</h3>}
|
||||
loading={isDeleting}
|
||||
>
|
||||
<div className="py-4">
|
||||
<Modal.Content>
|
||||
<p className="text-sm">
|
||||
This will automatically revoke any membership of this role in other roles, and this
|
||||
action cannot be undone.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteRoleModal
|
||||
@@ -1,40 +1,27 @@
|
||||
import { FC, useState, useRef } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PostgresRole } from '@supabase/postgres-meta'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Dropdown,
|
||||
IconChevronUp,
|
||||
IconCheck,
|
||||
IconX,
|
||||
Toggle,
|
||||
Collapsible,
|
||||
IconEdit3,
|
||||
IconTrash,
|
||||
IconHelpCircle,
|
||||
IconMoreVertical,
|
||||
Checkbox,
|
||||
} from 'ui'
|
||||
import { ROLE_PERMISSIONS } from './Roles.constants'
|
||||
|
||||
interface Props {
|
||||
role: PostgresRole
|
||||
onSelectDelete: (role: PostgresRole) => void
|
||||
}
|
||||
|
||||
const ROLE_PERMISSIONS: any = {
|
||||
is_superuser: 'User is a Superuser',
|
||||
can_login: 'User can login',
|
||||
can_create_role: 'User can create roles',
|
||||
can_create_db: 'User can create databases',
|
||||
is_replication_role:
|
||||
'User can initiate streaming replication and put the system in and out of backup mode',
|
||||
can_bypass_rls: 'User bypasses every row level security policy',
|
||||
}
|
||||
|
||||
const RoleRow: FC<Props> = ({ role }) => {
|
||||
const RoleRow: FC<Props> = ({ role, onSelectDelete }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
const isEditingRef = useRef<boolean>()
|
||||
|
||||
const {
|
||||
is_superuser,
|
||||
@@ -45,19 +32,6 @@ const RoleRow: FC<Props> = ({ role }) => {
|
||||
can_bypass_rls,
|
||||
} = role
|
||||
|
||||
const onSelectEdit = (event: any) => {
|
||||
event.stopPropagation()
|
||||
setIsExpanded(true)
|
||||
setIsEditing(true)
|
||||
isEditingRef.current = true
|
||||
}
|
||||
|
||||
const onCancelEdit = (event: any) => {
|
||||
event.stopPropagation()
|
||||
setIsEditing(false)
|
||||
isEditingRef.current = false
|
||||
}
|
||||
|
||||
const onSaveChanges = async (values: any) => {
|
||||
setIsSaving(true)
|
||||
console.log(values)
|
||||
@@ -89,69 +63,50 @@ const RoleRow: FC<Props> = ({ role }) => {
|
||||
'last:rounded-bl last:rounded-br',
|
||||
].join(' ')}
|
||||
>
|
||||
{({ handleReset }: any) => (
|
||||
<Collapsible
|
||||
open={isExpanded}
|
||||
onOpenChange={() => {
|
||||
if (!isEditingRef.current) setIsExpanded(!isExpanded)
|
||||
}}
|
||||
>
|
||||
<Collapsible.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="group flex w-full items-center justify-between rounded py-3 px-6 text-scale-1200"
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<IconChevronUp
|
||||
className="text-scale-800 transition data-open-parent:rotate-0 data-closed-parent:rotate-180"
|
||||
strokeWidth={2}
|
||||
width={14}
|
||||
/>
|
||||
<div className="space-x-2 flex items-center">
|
||||
<p className="text-left text-sm">{role.name}</p>
|
||||
<p className="text-left text-sm text-scale-1000">(ID: {role.id})</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<p
|
||||
className={`text-sm ${
|
||||
role.active_connections > 0 ? 'text-scale-1100' : 'text-scale-1000'
|
||||
}`}
|
||||
>
|
||||
{role.active_connections} connections
|
||||
</p>
|
||||
{isEditing ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="default"
|
||||
disabled={isSaving}
|
||||
onClick={(event) => {
|
||||
handleReset()
|
||||
onCancelEdit(event)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" disabled={isSaving} loading={isSaving} htmlType="submit">
|
||||
Save changes
|
||||
</Button>
|
||||
{({ values, initialValues, handleReset }: any) => {
|
||||
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
|
||||
|
||||
return (
|
||||
<Collapsible open={isExpanded}>
|
||||
<Collapsible.Trigger asChild>
|
||||
<button
|
||||
id="collapsible-trigger"
|
||||
type="button"
|
||||
className="group flex w-full items-center justify-between rounded py-3 px-6 text-scale-1200"
|
||||
onClick={(event: any) => {
|
||||
if (event.target.id === 'collapsible-trigger') setIsExpanded(!isExpanded)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<IconChevronUp
|
||||
className="text-scale-800 transition data-open-parent:rotate-0 data-closed-parent:rotate-180"
|
||||
strokeWidth={2}
|
||||
width={14}
|
||||
/>
|
||||
<div className="space-x-2 flex items-center">
|
||||
<p className="text-left text-sm">{role.name}</p>
|
||||
<p className="text-left text-sm text-scale-1000">(ID: {role.id})</p>
|
||||
</div>
|
||||
) : (
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<p
|
||||
className={`text-sm ${
|
||||
role.active_connections > 0 ? 'text-scale-1100' : 'text-scale-1000'
|
||||
}`}
|
||||
>
|
||||
{role.active_connections} connections
|
||||
</p>
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
className="w-[120px]"
|
||||
overlay={
|
||||
<>
|
||||
<Dropdown.Item
|
||||
icon={<IconEdit3 size="tiny" strokeWidth={2} />}
|
||||
onClick={onSelectEdit}
|
||||
>
|
||||
Edit
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Separator />
|
||||
<Dropdown.Item
|
||||
icon={<IconTrash className="text-red-800" size="tiny" strokeWidth={2} />}
|
||||
onClick={() => setIsDeleting(true)}
|
||||
onClick={(event: any) => {
|
||||
event.stopPropagation()
|
||||
onSelectDelete(role)
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Dropdown.Item>
|
||||
@@ -160,41 +115,70 @@ const RoleRow: FC<Props> = ({ role }) => {
|
||||
>
|
||||
<Button as="span" type="default" className="px-1" icon={<IconMoreVertical />} />
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<div className="group border-t border-scale-500 bg-scale-100 py-6 px-6 text-scale-1200 dark:bg-scale-300">
|
||||
{isEditing ? (
|
||||
<div className="mx-auto py-4 px-6 space-y-[9px]">
|
||||
</div>
|
||||
</button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<div className="group border-t border-scale-500 bg-scale-100 py-6 px-20 text-scale-1200 dark:bg-scale-300">
|
||||
<div className="py-4 space-y-[9px]">
|
||||
{Object.keys(ROLE_PERMISSIONS).map((permission) => (
|
||||
<Checkbox
|
||||
<Toggle
|
||||
size="small"
|
||||
key={permission}
|
||||
id={permission}
|
||||
name={permission}
|
||||
label={ROLE_PERMISSIONS[permission]}
|
||||
label={ROLE_PERMISSIONS[permission].description}
|
||||
disabled={ROLE_PERMISSIONS[permission].disabled}
|
||||
className={[
|
||||
'roles-toggle',
|
||||
ROLE_PERMISSIONS[permission].disabled ? 'opacity-50' : '',
|
||||
].join(' ')}
|
||||
afterLabel={
|
||||
ROLE_PERMISSIONS[permission].disabled && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger type="button">
|
||||
<IconHelpCircle
|
||||
size="tiny"
|
||||
strokeWidth={2}
|
||||
className="ml-2 relative top-[3px]"
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content align="center" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200 space-y-1',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs">
|
||||
This privilege cannot be updated via the dashboard
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-auto py-4 px-6 space-y-2">
|
||||
{Object.keys(ROLE_PERMISSIONS).map((permission) => (
|
||||
<div key={permission} className="flex items-center space-x-2">
|
||||
{(role as any)[permission] ? (
|
||||
<IconCheck className="text-brand-900" strokeWidth={2} />
|
||||
) : (
|
||||
<IconX className="text-scale-1000" strokeWidth={2} />
|
||||
)}
|
||||
<p className="text-sm">{ROLE_PERMISSIONS[permission]}</p>
|
||||
</div>
|
||||
))}
|
||||
<div className="py-4 flex items-center space-x-2 justify-end">
|
||||
<Button
|
||||
type="default"
|
||||
disabled={!hasChanges || isSaving}
|
||||
onClick={() => handleReset()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" disabled={!hasChanges || isSaving} loading={isSaving}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
)}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
export const ROLE_PERMISSIONS: any = {
|
||||
can_login: {
|
||||
disabled: false,
|
||||
description: 'User can login',
|
||||
},
|
||||
can_create_role: {
|
||||
disabled: false,
|
||||
description: 'User can create roles',
|
||||
},
|
||||
can_create_db: {
|
||||
disabled: false,
|
||||
description: 'User can create databases',
|
||||
},
|
||||
can_bypass_rls: {
|
||||
disabled: false,
|
||||
description: 'User bypasses every row level security policy',
|
||||
},
|
||||
is_superuser: {
|
||||
disabled: true,
|
||||
description: 'User is a Superuser',
|
||||
},
|
||||
is_replication_role: {
|
||||
disabled: true,
|
||||
description:
|
||||
'User can initiate streaming replication and put the system in and out of backup mode',
|
||||
},
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
import { FC } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PostgresRole } from '@supabase/postgres-meta'
|
||||
import { Button, IconPlus } from 'ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useParams, useStore } from 'hooks'
|
||||
import SparkBar from 'components/ui/SparkBar'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
import RoleRow from './RoleRow'
|
||||
import Link from 'next/link'
|
||||
import DeleteRoleModal from './DeleteRoleModal'
|
||||
|
||||
interface Props {
|
||||
onSelectRole: (role: any) => void
|
||||
}
|
||||
|
||||
const RolesList: FC<Props> = ({ onSelectRole = () => {} }) => {
|
||||
const RolesList: FC<Props> = ({}) => {
|
||||
const { ref } = useParams()
|
||||
const { meta } = useStore()
|
||||
const roles = meta.roles.list()
|
||||
|
||||
const [selectedRoleToDelete, setSelectedRoleToDelete] = useState<any>()
|
||||
|
||||
const connectionLimit = Math.max(...roles.map((role: PostgresRole) => role.connection_limit))
|
||||
const totalActiveConnections = roles
|
||||
.map((role: PostgresRole) => role.active_connections)
|
||||
@@ -26,61 +31,73 @@ const RolesList: FC<Props> = ({ onSelectRole = () => {} }) => {
|
||||
.sort((a, b) => b.active_connections - a.active_connections)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormHeader
|
||||
title="Database Roles"
|
||||
description="Manage access control to your database through users, groups, and permissions"
|
||||
/>
|
||||
<div className="flex items-center mb-6 space-x-6">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<div className="w-42">
|
||||
<SparkBar
|
||||
type="horizontal"
|
||||
max={connectionLimit}
|
||||
value={totalActiveConnections}
|
||||
barClass={
|
||||
totalActiveConnections > 0.9 * connectionLimit
|
||||
? 'bg-red-800'
|
||||
: totalActiveConnections > 0.75 * connectionLimit
|
||||
? 'bg-amber-900'
|
||||
: 'bg-green-800'
|
||||
}
|
||||
labelTop={`${totalActiveConnections}/${connectionLimit}`}
|
||||
labelBottom="Active connections"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content align="start" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200 space-y-1',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-xs text-scale-1100 pr-2">Connections by roles:</p>
|
||||
{rolesWithActiveConnections.map((role: PostgresRole) => (
|
||||
<div key={role.id} className="text-xs text-scale-1200">
|
||||
{role.name}: {role.active_connections}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<Button type="primary" icon={<IconPlus size="tiny" />}>
|
||||
Add role
|
||||
</Button>
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormHeader
|
||||
title="Database Roles"
|
||||
description="Manage access control to your database through users, groups, and permissions"
|
||||
/>
|
||||
<div className="flex items-center mb-6 space-x-6">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<div className="w-42">
|
||||
<SparkBar
|
||||
type="horizontal"
|
||||
max={connectionLimit}
|
||||
value={totalActiveConnections}
|
||||
barClass={
|
||||
totalActiveConnections > 0.9 * connectionLimit
|
||||
? 'bg-red-800'
|
||||
: totalActiveConnections > 0.75 * connectionLimit
|
||||
? 'bg-amber-900'
|
||||
: 'bg-green-800'
|
||||
}
|
||||
labelTop={`${totalActiveConnections}/${connectionLimit}`}
|
||||
labelBottom="Active connections"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content align="start" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200 space-y-1',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-xs text-scale-1100 pr-2">Connections by roles:</p>
|
||||
{rolesWithActiveConnections.map((role: PostgresRole) => (
|
||||
<div key={role.id} className="text-xs text-scale-1200">
|
||||
{role.name}: {role.active_connections}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<Link href={`/project/${ref}/database/roles/new`}>
|
||||
<a>
|
||||
<Button type="primary" icon={<IconPlus size="tiny" />}>
|
||||
Add role
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{roles.map((role: PostgresRole, i: number) => (
|
||||
<RoleRow key={role.id} role={role} onSelectDelete={setSelectedRoleToDelete} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{roles.map((role: PostgresRole, i: number) => (
|
||||
<RoleRow key={role.id} role={role} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DeleteRoleModal
|
||||
role={selectedRoleToDelete}
|
||||
visible={selectedRoleToDelete !== undefined}
|
||||
onClose={() => setSelectedRoleToDelete(undefined)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,21 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { DatabaseLayout } from 'components/layouts'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { checkPermissions } from 'hooks'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import CreateRole from 'components/interfaces/Database/Roles/CreateRole'
|
||||
|
||||
const CreateNewRole: NextPageWithLayout = () => {
|
||||
const canReadRoles = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'roles')
|
||||
if (!canReadRoles) {
|
||||
return <NoPermission isFullPage resourceText="view database roles" />
|
||||
}
|
||||
|
||||
return <CreateRole />
|
||||
}
|
||||
|
||||
CreateNewRole.getLayout = (page) => <DatabaseLayout title="Database">{page}</DatabaseLayout>
|
||||
|
||||
export default observer(CreateNewRole)
|
||||
@@ -333,4 +333,10 @@
|
||||
|
||||
.pitr-listbox {
|
||||
@apply md:gap-x-2;
|
||||
}
|
||||
|
||||
.roles-toggle {
|
||||
label {
|
||||
@apply mt-1;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user