diff --git a/packages/ui/src/components/Modal/Modal.tsx b/packages/ui/src/components/Modal/Modal.tsx index 4c536622e39..c475ab11ff1 100644 --- a/packages/ui/src/components/Modal/Modal.tsx +++ b/packages/ui/src/components/Modal/Modal.tsx @@ -125,7 +125,12 @@ const Modal = ({ - diff --git a/packages/ui/src/components/Toggle/Toggle.tsx b/packages/ui/src/components/Toggle/Toggle.tsx index 413c1725a1f..07f4654a7ff 100644 --- a/packages/ui/src/components/Toggle/Toggle.tsx +++ b/packages/ui/src/components/Toggle/Toggle.tsx @@ -11,7 +11,7 @@ interface Props extends Omit, 'size'> { layout?: 'horizontal' | 'vertical' | 'flex' error?: string descriptionText?: string - label?: string + label?: string | React.ReactNode afterLabel?: string beforeLabel?: string labelOptional?: string diff --git a/packages/ui/src/lib/Layout/FormLayout/FormLayout.tsx b/packages/ui/src/lib/Layout/FormLayout/FormLayout.tsx index d400ed44020..14262972430 100644 --- a/packages/ui/src/lib/Layout/FormLayout/FormLayout.tsx +++ b/packages/ui/src/lib/Layout/FormLayout/FormLayout.tsx @@ -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 && (

{descriptionText} @@ -153,19 +148,14 @@ export function FormLayout({ > {labelled && (

+
+
+ + +
+ +

Back

+
+
+ +
+

Create a new role

+ {/*
+ + + + + +
*/} +
+ +
+ {({ isSubmitting, handleReset, values, initialValues }: any) => { + const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + return ( + + +
+ } + > + Role Configuration}> + + + + + Role Privileges}> + +
+ {Object.keys(ROLE_PERMISSIONS).map((permission) => ( + + + + + + +
+ + This privilege cannot be granted via the dashboard + +
+
+ + ) + } + /> + ))} +
+
+
+ + ) + }} + + + + ) +} + +export default CreateRole diff --git a/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx b/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx new file mode 100644 index 00000000000..c4388e74ad0 --- /dev/null +++ b/studio/components/interfaces/Database/Roles/DeleteRoleModal.tsx @@ -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 = ({ 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 ( + Confirm to delete role "{role?.name}"} + loading={isDeleting} + > +
+ +

+ This will automatically revoke any membership of this role in other roles, and this + action cannot be undone. +

+
+
+
+ ) +} + +export default DeleteRoleModal diff --git a/studio/components/interfaces/Database/Roles/RoleRow.tsx b/studio/components/interfaces/Database/Roles/RoleRow.tsx index 5b949af8dd6..8f4bccd9e16 100644 --- a/studio/components/interfaces/Database/Roles/RoleRow.tsx +++ b/studio/components/interfaces/Database/Roles/RoleRow.tsx @@ -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 = ({ role }) => { +const RoleRow: FC = ({ 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() const { is_superuser, @@ -45,19 +32,6 @@ const RoleRow: FC = ({ 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 = ({ role }) => { 'last:rounded-bl last:rounded-br', ].join(' ')} > - {({ handleReset }: any) => ( - { - if (!isEditingRef.current) setIsExpanded(!isExpanded) - }} - > - - - + {({ values, initialValues, handleReset }: any) => { + const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + + return ( + + + - - -
- {isEditing ? ( -
+
+ + + +
+
{Object.keys(ROLE_PERMISSIONS).map((permission) => ( - + + + + + +
+ + This privilege cannot be updated via the dashboard + +
+
+ + ) + } /> ))}
- ) : ( -
- {Object.keys(ROLE_PERMISSIONS).map((permission) => ( -
- {(role as any)[permission] ? ( - - ) : ( - - )} -

{ROLE_PERMISSIONS[permission]}

-
- ))} +
+ +
- )} -
- - - )} +
+
+ + ) + }} ) } diff --git a/studio/components/interfaces/Database/Roles/Roles.constants.ts b/studio/components/interfaces/Database/Roles/Roles.constants.ts new file mode 100644 index 00000000000..a7d1a02ac42 --- /dev/null +++ b/studio/components/interfaces/Database/Roles/Roles.constants.ts @@ -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', + }, +} diff --git a/studio/components/interfaces/Database/Roles/RolesList.tsx b/studio/components/interfaces/Database/Roles/RolesList.tsx index e7eac075fce..519517cd70e 100644 --- a/studio/components/interfaces/Database/Roles/RolesList.tsx +++ b/studio/components/interfaces/Database/Roles/RolesList.tsx @@ -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 = ({ onSelectRole = () => {} }) => { +const RolesList: FC = ({}) => { + const { ref } = useParams() const { meta } = useStore() const roles = meta.roles.list() + const [selectedRoleToDelete, setSelectedRoleToDelete] = useState() + 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 = ({ onSelectRole = () => {} }) => { .sort((a, b) => b.active_connections - a.active_connections) return ( -
-
- -
- - -
- 0.9 * connectionLimit - ? 'bg-red-800' - : totalActiveConnections > 0.75 * connectionLimit - ? 'bg-amber-900' - : 'bg-green-800' - } - labelTop={`${totalActiveConnections}/${connectionLimit}`} - labelBottom="Active connections" - /> -
-
- - -
-

Connections by roles:

- {rolesWithActiveConnections.map((role: PostgresRole) => ( -
- {role.name}: {role.active_connections} -
- ))} -
-
-
- + <> +
+
+ +
+ + +
+ 0.9 * connectionLimit + ? 'bg-red-800' + : totalActiveConnections > 0.75 * connectionLimit + ? 'bg-amber-900' + : 'bg-green-800' + } + labelTop={`${totalActiveConnections}/${connectionLimit}`} + labelBottom="Active connections" + /> +
+
+ + +
+

Connections by roles:

+ {rolesWithActiveConnections.map((role: PostgresRole) => ( +
+ {role.name}: {role.active_connections} +
+ ))} +
+
+
+ + + + + +
+
+ +
+ {roles.map((role: PostgresRole, i: number) => ( + + ))}
-
- {roles.map((role: PostgresRole, i: number) => ( - - ))} -
-
+ setSelectedRoleToDelete(undefined)} + /> + ) } diff --git a/studio/pages/project/[ref]/database/roles.tsx b/studio/pages/project/[ref]/database/roles/index.tsx similarity index 100% rename from studio/pages/project/[ref]/database/roles.tsx rename to studio/pages/project/[ref]/database/roles/index.tsx diff --git a/studio/pages/project/[ref]/database/roles/new.tsx b/studio/pages/project/[ref]/database/roles/new.tsx new file mode 100644 index 00000000000..5b234350727 --- /dev/null +++ b/studio/pages/project/[ref]/database/roles/new.tsx @@ -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 + } + + return +} + +CreateNewRole.getLayout = (page) => {page} + +export default observer(CreateNewRole) diff --git a/studio/styles/ui.scss b/studio/styles/ui.scss index 16a1284fc6b..e927eb48782 100644 --- a/studio/styles/ui.scss +++ b/studio/styles/ui.scss @@ -333,4 +333,10 @@ .pitr-listbox { @apply md:gap-x-2; +} + +.roles-toggle { + label { + @apply mt-1; + } } \ No newline at end of file