From 6470fb04bfe9e5c02397764f2ea3dfb9831dc008 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Thu, 14 Nov 2024 21:07:12 +0100 Subject: [PATCH] fix(misc): functional grant and revoke in Console --- console/api/database/__init__.py | 2 - console/api/database/group.py | 12 -- console/api/database/organization.py | 13 -- console/api/database/workspace.py | 9 -- console/api/models/__init__.py | 3 - console/api/models/group.py | 5 - console/api/models/organization.py | 5 - console/api/models/userpermission.py | 12 +- console/api/models/workspace.py | 9 -- console/api/routers/group.py | 29 +--- console/api/routers/organization.py | 28 ---- console/api/routers/userpermission.py | 12 +- console/api/routers/workspace.py | 29 ---- ui/src/client/console/console.ts | 29 ++++ .../console/console-rename-modal.tsx | 142 ------------------ ui/src/components/layout/layout-console.tsx | 20 +-- ui/src/pages/console/console-panel-groups.tsx | 98 ++++++++---- .../console/console-panel-organizations.tsx | 101 +++++++++---- .../console/console-panel-workspaces.tsx | 96 ++++++++---- 19 files changed, 261 insertions(+), 393 deletions(-) delete mode 100644 ui/src/components/console/console-rename-modal.tsx diff --git a/console/api/database/__init__.py b/console/api/database/__init__.py index 76a83c695..b5e93ca37 100644 --- a/console/api/database/__init__.py +++ b/console/api/database/__init__.py @@ -17,7 +17,6 @@ fetch_organization_users, fetch_organization_workspaces, fetch_organization_groups, - update_organization, fetch_organization_count, ) from .snapshot import fetch_snapshot, fetch_snapshots @@ -31,7 +30,6 @@ from .workspace import ( fetch_workspace, fetch_workspaces, - update_workspace, fetch_workspace_count, ) from .overview import fetch_version diff --git a/console/api/database/group.py b/console/api/database/group.py index b832fa568..128ac401f 100644 --- a/console/api/database/group.py +++ b/console/api/database/group.py @@ -104,18 +104,6 @@ def fetch_groups(page=1, size=10) -> Tuple[Iterable[Dict], int]: raise error -# --- UPDATE --- # -def update_group(data: dict) -> None: - try: - with conn.cursor() as curs: - if not exists(curs=curs, _id=data["id"], tablename="group"): - raise NotFoundException(f"Group with id={data['id']} does not exist!") - - curs.execute(parse_sql_update_query("group", data)) - except DatabaseError as error: - raise error - - # --- CREATE --- # # --- DELETE --- # diff --git a/console/api/database/organization.py b/console/api/database/organization.py index 1ab553dca..fedeb908d 100644 --- a/console/api/database/organization.py +++ b/console/api/database/organization.py @@ -173,19 +173,6 @@ def fetch_organization_groups( raise error -# --- UPDATE --- # -def update_organization(data: Dict) -> None: - try: - with conn.cursor() as curs: - if not exists(curs=curs, _id=data["id"], tablename="organization"): - raise NotFoundException( - f"Organization with id={data['id']} does not exist!" - ) - curs.execute(parse_sql_update_query("organization", data)) - except DatabaseError as error: - raise error - - # --- CREATE --- # # --- DELETE --- # diff --git a/console/api/database/workspace.py b/console/api/database/workspace.py index 3a1d19a7d..70ce593be 100644 --- a/console/api/database/workspace.py +++ b/console/api/database/workspace.py @@ -109,15 +109,6 @@ def fetch_workspaces(page=1, size=10) -> Tuple[Iterable[Dict], int]: raise error -# --- UPDATE --- # -def update_workspace(data: dict) -> None: - try: - with conn.cursor() as curs: - curs.execute(parse_sql_update_query("workspace", data)) - except DatabaseError as error: - raise error - - # --- CREATE --- # # --- DELETE --- # diff --git a/console/api/models/__init__.py b/console/api/models/__init__.py index bdf9721a1..e06aa3efd 100644 --- a/console/api/models/__init__.py +++ b/console/api/models/__init__.py @@ -30,7 +30,6 @@ GroupListRequest, GroupResponse, GroupListResponse, - UpdateGroupRequest, GroupSearchRequest, ) from .grouppermission import ( @@ -51,7 +50,6 @@ OrganizationListRequest, OrganizationResponse, OrganizationListResponse, - UpdateOrganizationRequest, OrganizationWorkspaceResponse, OrganizationUserListResponse, OrganizationUserResponse, @@ -106,7 +104,6 @@ WorkspaceResponse, WorkspaceListResponse, WorkspaceOrganizationListRequest, - UpdateWorkspaceRequest, WorkspaceSearchRequest, ) from .token import TokenResponse, TokenPayload diff --git a/console/api/models/group.py b/console/api/models/group.py index c2f8ed0b5..66773ea0d 100644 --- a/console/api/models/group.py +++ b/console/api/models/group.py @@ -36,11 +36,6 @@ class GroupSearchRequest(GenericSearchRequest): pass -class UpdateGroupRequest(GenericRequest): - name: str | None = Field(None) - updateTime: datetime.datetime | None = Field(default_factory=datetime.datetime.now) - - # --- RESPONSE MODELS --- # class GroupResponse(GenericResponse): name: str diff --git a/console/api/models/organization.py b/console/api/models/organization.py index a0ab4087e..acf334921 100644 --- a/console/api/models/organization.py +++ b/console/api/models/organization.py @@ -47,11 +47,6 @@ class OrganizationGroupListRequest(GenericRequest, GenericPaginationRequest): pass -class UpdateOrganizationRequest(GenericRequest): - name: str = Field(None) - updateTime: datetime.datetime = Field(default_factory=datetime.datetime.now) - - # --- RESPONSE MODELS --- # class OrganizationResponse(GenericResponse): name: str diff --git a/console/api/models/userpermission.py b/console/api/models/userpermission.py index bbdcbcdcb..833920831 100644 --- a/console/api/models/userpermission.py +++ b/console/api/models/userpermission.py @@ -31,16 +31,16 @@ class UserPermissionListRequest(GenericPaginationRequest): class UserPermissionGrantRequest(BaseModel): - user_id: str - resource_id: str - resource_type: Literal["file", "group", "organization", "workspace"] + userId: str + resourceId: str + resourceType: Literal["file", "group", "organization", "workspace"] permission: str class UserPermissionRevokeRequest(BaseModel): - user_id: str - resource_id: str - resource_type: Literal["file", "group", "organization", "workspace"] + userId: str + resourceId: str + resourceType: Literal["file", "group", "organization", "workspace"] # --- RESPONSE MODELS --- # diff --git a/console/api/models/workspace.py b/console/api/models/workspace.py index ffe0f6ee3..108f097f7 100644 --- a/console/api/models/workspace.py +++ b/console/api/models/workspace.py @@ -28,15 +28,6 @@ class WorkspaceRequest(GenericRequest): pass -class UpdateWorkspaceRequest(GenericRequest): - name: str | None = Field(None) - organizationId: str | None = Field(None) - storageCapacity: float | None = Field(None) - rootId: str | None = Field(None) - bucket: str | None = Field(None) - updateTime: datetime.datetime | None = Field(default_factory=datetime.datetime.now) - - class WorkspaceListRequest(GenericPaginationRequest): pass diff --git a/console/api/routers/group.py b/console/api/routers/group.py index efc524568..1197b3626 100644 --- a/console/api/routers/group.py +++ b/console/api/routers/group.py @@ -13,7 +13,7 @@ from fastapi import APIRouter, Depends, status, Response from ..database import fetch_groups, fetch_group -from ..database.group import update_group, fetch_group_count +from ..database.group import fetch_group_count from ..dependencies import JWTBearer, meilisearch_client from ..log import base_logger from ..errors import ( @@ -28,7 +28,6 @@ GroupListRequest, GroupListResponse, GroupRequest, - UpdateGroupRequest, CountResponse, GroupSearchRequest, ) @@ -123,32 +122,6 @@ async def get_search_groups(data: Annotated[GroupSearchRequest, Depends()]): return UnknownApiError() -# --- PATCH --- # -@group_api_router.patch(path="", status_code=status.HTTP_202_ACCEPTED) -async def patch_group(data: UpdateGroupRequest, response: Response): - try: - await redis_conn.delete(f"group:{data.id}") - update_group(data=data.model_dump(exclude_none=True)) - meilisearch_client.index("group").update_documents( - [ - { - "id": data.id, - "name": data.name, - "updateTime": data.updateTime.strftime("%Y-%m-%dT%H:%M:%SZ"), - } - ] - ) - except NotFoundException as e: - logger.error(e) - return NotFoundError(message=str(e)) - except Exception as e: - logger.exception(e) - return UnknownApiError() - - response.status_code = status.HTTP_202_ACCEPTED - return None - - # --- POST --- # # --- PUT --- # diff --git a/console/api/routers/organization.py b/console/api/routers/organization.py index 7a0a0f71b..d0f1af9b8 100644 --- a/console/api/routers/organization.py +++ b/console/api/routers/organization.py @@ -17,7 +17,6 @@ fetch_organizations, fetch_organization_users, fetch_organization_workspaces, - update_organization, fetch_organization_groups, fetch_organization_count, ) @@ -36,7 +35,6 @@ OrganizationListResponse, OrganizationListRequest, OrganizationWorkspaceListRequest, - UpdateOrganizationRequest, OrganizationWorkspaceListResponse, OrganizationGroupListResponse, OrganizationGroupListRequest, @@ -229,32 +227,6 @@ async def get_organization_groups( return UnknownApiError() -# --- PATCH --- # -@organization_api_router.patch(path="", status_code=status.HTTP_202_ACCEPTED) -async def patch_organization(data: UpdateOrganizationRequest, response: Response): - try: - await redis_conn.delete(f"organization:{data.id}") - update_organization(data=data.model_dump(exclude_none=True)) - meilisearch_client.index("organization").update_documents( - [ - { - "id": data.id, - "name": data.name, - "updateTime": data.updateTime.strftime("%Y-%m-%dT%H:%M:%SZ"), - } - ] - ) - except NotFoundException as e: - logger.error(e) - return NotFoundError(message=str(e)) - except Exception as e: - logger.exception(e) - return UnknownApiError() - - response.status_code = status.HTTP_202_ACCEPTED - return None - - # --- POST --- # # --- PUT --- # diff --git a/console/api/routers/userpermission.py b/console/api/routers/userpermission.py index dbfb7d522..1eef3d9d9 100644 --- a/console/api/routers/userpermission.py +++ b/console/api/routers/userpermission.py @@ -26,10 +26,10 @@ @user_permission_api_router.post(path="/grant", status_code=status.HTTP_200_OK) async def post_grant_user_permission(data: UserPermissionGrantRequest): - await redis_conn.delete(f"{data.resource_type}:{data.resource_id}") + await redis_conn.delete(f"{data.resourceType}:{data.resourceId}") grant_user_permission( - user_id=data.user_id, - resource_id=data.resource_id, + user_id=data.userId, + resource_id=data.resourceId, permission=data.permission, ) return Response(status_code=status.HTTP_200_OK) @@ -37,9 +37,9 @@ async def post_grant_user_permission(data: UserPermissionGrantRequest): @user_permission_api_router.post(path="/revoke", status_code=status.HTTP_200_OK) async def post_revoke_user_permission(data: UserPermissionRevokeRequest): - await redis_conn.delete(f"{data.resource_type}:{data.resource_id}") + await redis_conn.delete(f"{data.resourceType}:{data.resourceId}") revoke_user_permission( - user_id=data.user_id, - resource_id=data.resource_id, + user_id=data.userId, + resource_id=data.resourceId, ) return Response(status_code=status.HTTP_200_OK) diff --git a/console/api/routers/workspace.py b/console/api/routers/workspace.py index e63bc7b16..fc086fa6a 100644 --- a/console/api/routers/workspace.py +++ b/console/api/routers/workspace.py @@ -15,7 +15,6 @@ from ..database import ( fetch_workspace, fetch_workspaces, - update_workspace, fetch_workspace_count, ) from ..dependencies import JWTBearer, meilisearch_client, redis_conn @@ -32,7 +31,6 @@ WorkspaceRequest, WorkspaceListResponse, WorkspaceListRequest, - UpdateWorkspaceRequest, GenericUnexpectedErrorResponse, GenericAcceptedResponse, CountResponse, @@ -137,33 +135,6 @@ async def get_search_workspaces(data: Annotated[WorkspaceSearchRequest, Depends( return UnknownApiError() -# --- PATCH --- # -@workspace_api_router.patch(path="", status_code=status.HTTP_202_ACCEPTED) -async def patch_workspace(data: UpdateWorkspaceRequest, response: Response): - try: - await redis_conn.delete(f"workspace:{data.id}") - update_workspace(data=data.model_dump(exclude_none=True)) - meilisearch_client.index("workspace").update_documents( - [ - { - "id": data.id, - "name": data.name, - "storageCapacity": data.storageCapacity, - "updateTime": data.updateTime.strftime("%Y-%m-%dT%H:%M:%SZ"), - } - ] - ) - except NotFoundException as e: - logger.error(e) - return NotFoundError(message=str(e)) - except Exception as e: - logger.exception(e) - return UnknownApiError() - - response.status_code = status.HTTP_202_ACCEPTED - return None - - # --- POST --- # # --- PUT --- # diff --git a/ui/src/client/console/console.ts b/ui/src/client/console/console.ts index 88e12e3fc..b61ee4dd0 100644 --- a/ui/src/client/console/console.ts +++ b/ui/src/client/console/console.ts @@ -112,6 +112,19 @@ export interface InvitationStatusRequest extends BaseIDRequest { accept: boolean } +export type GrantUserPermissionOptions = { + userId: string + resourceId: string + resourceType: 'file' | 'group' | 'organization' | 'workspace' + permission: string +} + +export type RevokeUserPermissionOptions = { + userId: string + resourceId: string + resourceType: 'file' | 'group' | 'organization' | 'workspace' +} + export interface CountResponse { count: number } @@ -382,6 +395,22 @@ export default class ConsoleAPI { } } + static grantUserPermission(options: GrantUserPermissionOptions) { + return consoleFetcher({ + url: '/user_permission/grant', + method: 'POST', + body: JSON.stringify(options), + }) as Promise + } + + static revokeUserPermission(options: RevokeUserPermissionOptions) { + return consoleFetcher({ + url: '/user_permission/revoke', + method: 'POST', + body: JSON.stringify(options), + }) as Promise + } + static paramsFromListOptions = (options: ListOptions): URLSearchParams => { const params: ListQueryParams = {} if (options.id) { diff --git a/ui/src/components/console/console-rename-modal.tsx b/ui/src/components/console/console-rename-modal.tsx deleted file mode 100644 index aeb990b6a..000000000 --- a/ui/src/components/console/console-rename-modal.tsx +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2024 Mateusz Kaźmierczak. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the GNU Affero General Public License v3.0 only, included in the file -// licenses/AGPL.txt. -import { useCallback, useRef } from 'react' -import { - Button, - FormControl, - FormErrorMessage, - Input, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, -} from '@chakra-ui/react' -import { - Field, - FieldAttributes, - FieldProps, - Form, - Formik, - FormikHelpers, -} from 'formik' -import * as Yup from 'yup' -import cx from 'classnames' -import useFocusAndSelectAll from '@/hooks/use-focus-and-select-all' - -interface ConsoleRenameModalProps { - currentName: string - isOpen: boolean - onClose: () => void - onRequest: ConsoleRenameModalRequest -} - -export type ConsoleRenameModalRequest = (name: string) => Promise - -type FormValues = { - name: string -} - -const ConsoleRenameModal = ({ - currentName, - isOpen, - onClose, - onRequest, -}: ConsoleRenameModalProps) => { - const inputRef = useRef(null) - useFocusAndSelectAll(inputRef, isOpen) - const formSchema = Yup.object().shape({ - name: Yup.string().required('Name is required').max(255), - }) - - const handleRequest = useCallback( - async ( - { name }: FormValues, - { setSubmitting }: FormikHelpers, - ) => { - setSubmitting(true) - try { - await onRequest(name) - onClose() - } finally { - setSubmitting(false) - } - }, - [onRequest, onClose], - ) - - return ( - - - - Edit Name - - - {({ errors, touched, isSubmitting }) => ( -
- - - {({ field }: FieldAttributes) => ( - - - {errors.name} - - )} - - - -
- - -
-
-
- )} -
-
-
- ) -} - -export default ConsoleRenameModal diff --git a/ui/src/components/layout/layout-console.tsx b/ui/src/components/layout/layout-console.tsx index 5f7ee10b9..a63d6d761 100644 --- a/ui/src/components/layout/layout-console.tsx +++ b/ui/src/components/layout/layout-console.tsx @@ -70,10 +70,10 @@ const LayoutConsole = () => { secondaryText: 'Basic information about your instance', }, { - href: '/console/users', - icon: , - primaryText: 'Users', - secondaryText: 'Manage users', + href: '/console/workspaces', + icon: , + primaryText: 'Workspaces', + secondaryText: 'Manage workspaces', }, { href: '/console/groups', @@ -81,12 +81,6 @@ const LayoutConsole = () => { primaryText: 'Groups', secondaryText: 'Manage groups', }, - { - href: '/console/workspaces', - icon: , - primaryText: 'Workspaces', - secondaryText: 'Manage workspaces', - }, { href: '/console/organizations', icon: , @@ -99,6 +93,12 @@ const LayoutConsole = () => { primaryText: 'Invitations', secondaryText: 'Manage invitations', }, + { + href: '/console/users', + icon: , + primaryText: 'Users', + secondaryText: 'Manage users', + }, ]} navigateFn={navigate} pathnameFn={() => location.pathname} diff --git a/ui/src/pages/console/console-panel-groups.tsx b/ui/src/pages/console/console-panel-groups.tsx index 65aa9df76..090f5eb65 100644 --- a/ui/src/pages/console/console-panel-groups.tsx +++ b/ui/src/pages/console/console-panel-groups.tsx @@ -7,7 +7,7 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. -import { useCallback, useState } from 'react' +import { ReactElement, useState } from 'react' import { Link, useLocation, @@ -18,7 +18,8 @@ import { Avatar, Link as ChakraLink } from '@chakra-ui/react' import { Heading } from '@chakra-ui/react' import { DataTable, - IconEdit, + IconRemoveModerator, + IconShield, PagePagination, RelativeDate, SectionError, @@ -32,8 +33,11 @@ import cx from 'classnames' import { Helmet } from 'react-helmet-async' import ConsoleAPI, { ConsoleGroup } from '@/client/console/console' import { swrConfig } from '@/client/options' -import ConsoleRenameModal from '@/components/console/console-rename-modal' +import ConsoleConfirmationModal, { + ConsoleConfirmationModalRequest, +} from '@/components/console/console-confirmation-modal' import { consoleGroupsPaginationStorage } from '@/infra/pagination' +import { getUserId } from '@/infra/token' import { decodeQuery } from '@/lib/helpers/query' const ConsolePanelGroups = () => { @@ -46,9 +50,13 @@ const ConsolePanelGroups = () => { searchFn: () => location.search, storage: consoleGroupsPaginationStorage(), }) - const [isConfirmRenameOpen, setIsConfirmRenameOpen] = useState(false) - const [currentName, setCurrentName] = useState('') - const [groupId, setGroupId] = useState() + const [isConfirmationOpen, setIsConfirmationOpen] = useState(false) + const [isConfirmationDestructive, setIsConfirmationDestructive] = + useState(false) + const [confirmationHeader, setConfirmationHeader] = useState() + const [confirmationBody, setConfirmationBody] = useState() + const [confirmationRequest, setConfirmationRequest] = + useState() const { data: list, error: listError, @@ -68,16 +76,6 @@ const ConsolePanelGroups = () => { const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 - const renameRequest = useCallback( - async (name: string) => { - if (groupId) { - await ConsoleAPI.renameObject({ id: groupId, name }, 'group') - await mutate() - } - }, - [groupId], - ) - return ( <> @@ -141,12 +139,52 @@ const ConsolePanelGroups = () => { ]} actions={[ { - label: 'Edit Name', - icon: , - onClick: async (group) => { - setCurrentName(group.name) - setGroupId(group.id) - setIsConfirmRenameOpen(true) + label: 'Grant Owner Permission', + icon: , + onClick: async (workspace) => { + setConfirmationHeader(<>Grant Owner Permission) + setConfirmationBody( + <> + Are you sure you want to grant yourself owner permission + on{' '} + {workspace.name}? + , + ) + setConfirmationRequest(() => async () => { + await ConsoleAPI.grantUserPermission({ + userId: getUserId(), + resourceId: workspace.id, + resourceType: 'group', + permission: 'owner', + }) + await mutate() + }) + setIsConfirmationDestructive(false) + setIsConfirmationOpen(true) + }, + }, + { + label: 'Revoke Permission', + icon: , + isDestructive: true, + onClick: async (workspace) => { + setConfirmationHeader(<>Revoke Permission) + setConfirmationBody( + <> + Are you sure you want to revoke your permission on{' '} + {workspace.name}? + , + ) + setConfirmationRequest(() => async () => { + await ConsoleAPI.revokeUserPermission({ + userId: getUserId(), + resourceId: workspace.id, + resourceType: 'group', + }) + await mutate() + }) + setIsConfirmationDestructive(true) + setIsConfirmationOpen(true) }, }, ]} @@ -166,12 +204,16 @@ const ConsolePanelGroups = () => { /> ) : null} - setIsConfirmRenameOpen(false)} - onRequest={renameRequest} - /> + {confirmationHeader && confirmationBody && confirmationRequest ? ( + setIsConfirmationOpen(false)} + onRequest={confirmationRequest} + /> + ) : null} ) } diff --git a/ui/src/pages/console/console-panel-organizations.tsx b/ui/src/pages/console/console-panel-organizations.tsx index 5b1d344c0..8472d90a1 100644 --- a/ui/src/pages/console/console-panel-organizations.tsx +++ b/ui/src/pages/console/console-panel-organizations.tsx @@ -7,7 +7,7 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. -import { useCallback, useState } from 'react' +import { ReactElement, useState } from 'react' import { useLocation, useNavigate, @@ -17,7 +17,8 @@ import { import { Avatar, Heading, Link as ChakraLink } from '@chakra-ui/react' import { DataTable, - IconEdit, + IconRemoveModerator, + IconShield, PagePagination, RelativeDate, SectionError, @@ -31,8 +32,11 @@ import cx from 'classnames' import { Helmet } from 'react-helmet-async' import ConsoleAPI, { ConsoleOrganization } from '@/client/console/console' import { swrConfig } from '@/client/options' -import ConsoleRenameModal from '@/components/console/console-rename-modal' +import ConsoleConfirmationModal, { + ConsoleConfirmationModalRequest, +} from '@/components/console/console-confirmation-modal' import { consoleOrganizationsPaginationStorage } from '@/infra/pagination' +import { getUserId } from '@/infra/token' import { decodeQuery } from '@/lib/helpers/query' const ConsolePanelOrganizations = () => { @@ -45,9 +49,13 @@ const ConsolePanelOrganizations = () => { searchFn: () => location.search, storage: consoleOrganizationsPaginationStorage(), }) - const [isConfirmRenameOpen, setIsConfirmRenameOpen] = useState(false) - const [currentName, setCurrentName] = useState('') - const [organizationId, setOrganizationId] = useState() + const [isConfirmationOpen, setIsConfirmationOpen] = useState(false) + const [isConfirmationDestructive, setIsConfirmationDestructive] = + useState(false) + const [confirmationHeader, setConfirmationHeader] = useState() + const [confirmationBody, setConfirmationBody] = useState() + const [confirmationRequest, setConfirmationRequest] = + useState() const { data: list, error: listError, @@ -67,19 +75,6 @@ const ConsolePanelOrganizations = () => { const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 - const renameRequest = useCallback( - async (name: string) => { - if (organizationId) { - await ConsoleAPI.renameObject( - { id: organizationId, name }, - 'organization', - ) - await mutate() - } - }, - [organizationId], - ) - return ( <> @@ -140,12 +135,52 @@ const ConsolePanelOrganizations = () => { ]} actions={[ { - label: 'Edit Name', - icon: , - onClick: async (organization) => { - setCurrentName(organization.name) - setOrganizationId(organization.id) - setIsConfirmRenameOpen(true) + label: 'Grant Owner Permission', + icon: , + onClick: async (workspace) => { + setConfirmationHeader(<>Grant Owner Permission) + setConfirmationBody( + <> + Are you sure you want to grant yourself owner permission + on{' '} + {workspace.name}? + , + ) + setConfirmationRequest(() => async () => { + await ConsoleAPI.grantUserPermission({ + userId: getUserId(), + resourceId: workspace.id, + resourceType: 'organization', + permission: 'owner', + }) + await mutate() + }) + setIsConfirmationDestructive(false) + setIsConfirmationOpen(true) + }, + }, + { + label: 'Revoke Permission', + icon: , + isDestructive: true, + onClick: async (workspace) => { + setConfirmationHeader(<>Revoke Permission) + setConfirmationBody( + <> + Are you sure you want to revoke your permission on{' '} + {workspace.name}? + , + ) + setConfirmationRequest(() => async () => { + await ConsoleAPI.revokeUserPermission({ + userId: getUserId(), + resourceId: workspace.id, + resourceType: 'organization', + }) + await mutate() + }) + setIsConfirmationDestructive(true) + setIsConfirmationOpen(true) }, }, ]} @@ -165,12 +200,16 @@ const ConsolePanelOrganizations = () => { /> ) : null} - setIsConfirmRenameOpen(false)} - onRequest={renameRequest} - /> + {confirmationHeader && confirmationBody && confirmationRequest ? ( + setIsConfirmationOpen(false)} + onRequest={confirmationRequest} + /> + ) : null} ) } diff --git a/ui/src/pages/console/console-panel-workspaces.tsx b/ui/src/pages/console/console-panel-workspaces.tsx index 5efbe0721..e5ef12759 100644 --- a/ui/src/pages/console/console-panel-workspaces.tsx +++ b/ui/src/pages/console/console-panel-workspaces.tsx @@ -7,7 +7,7 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. -import { useCallback, useState } from 'react' +import { ReactElement, useState } from 'react' import { Link, useLocation, @@ -18,7 +18,8 @@ import { Avatar, Link as ChakraLink } from '@chakra-ui/react' import { Heading } from '@chakra-ui/react' import { DataTable, - IconEdit, + IconRemoveModerator, + IconShield, PagePagination, RelativeDate, SectionError, @@ -32,8 +33,11 @@ import cx from 'classnames' import { Helmet } from 'react-helmet-async' import ConsoleAPI, { ConsoleWorkspace } from '@/client/console/console' import { swrConfig } from '@/client/options' -import ConsoleRenameModal from '@/components/console/console-rename-modal' +import ConsoleConfirmationModal, { + ConsoleConfirmationModalRequest, +} from '@/components/console/console-confirmation-modal' import { consoleWorkspacesPaginationStorage } from '@/infra/pagination' +import { getUserId } from '@/infra/token' import prettyBytes from '@/lib/helpers/pretty-bytes' import { decodeQuery } from '@/lib/helpers/query' @@ -47,9 +51,13 @@ const ConsolePanelWorkspaces = () => { searchFn: () => location.search, storage: consoleWorkspacesPaginationStorage(), }) - const [isConfirmRenameOpen, setIsConfirmRenameOpen] = useState(false) - const [currentName, setCurrentName] = useState('') - const [workspaceId, setWorkspaceId] = useState() + const [isConfirmationOpen, setIsConfirmationOpen] = useState(false) + const [isConfirmationDestructive, setIsConfirmationDestructive] = + useState(false) + const [confirmationHeader, setConfirmationHeader] = useState() + const [confirmationBody, setConfirmationBody] = useState() + const [confirmationRequest, setConfirmationRequest] = + useState() const { data: list, error: listError, @@ -69,16 +77,6 @@ const ConsolePanelWorkspaces = () => { const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 - const renameRequest = useCallback( - async (name: string) => { - if (workspaceId) { - await ConsoleAPI.renameObject({ id: workspaceId, name }, 'workspace') - await mutate() - } - }, - [workspaceId], - ) - return ( <> @@ -151,12 +149,52 @@ const ConsolePanelWorkspaces = () => { ]} actions={[ { - label: 'Edit Name', - icon: , + label: 'Grant Owner Permission', + icon: , + onClick: async (workspace) => { + setConfirmationHeader(<>Grant Owner Permission) + setConfirmationBody( + <> + Are you sure you want to grant yourself owner permission + on{' '} + {workspace.name}? + , + ) + setConfirmationRequest(() => async () => { + await ConsoleAPI.grantUserPermission({ + userId: getUserId(), + resourceId: workspace.id, + resourceType: 'workspace', + permission: 'owner', + }) + await mutate() + }) + setIsConfirmationDestructive(false) + setIsConfirmationOpen(true) + }, + }, + { + label: 'Revoke Permission', + icon: , + isDestructive: true, onClick: async (workspace) => { - setCurrentName(workspace.name) - setWorkspaceId(workspace.id) - setIsConfirmRenameOpen(true) + setConfirmationHeader(<>Revoke Permission) + setConfirmationBody( + <> + Are you sure you want to revoke your permission on{' '} + {workspace.name}? + , + ) + setConfirmationRequest(() => async () => { + await ConsoleAPI.revokeUserPermission({ + userId: getUserId(), + resourceId: workspace.id, + resourceType: 'workspace', + }) + await mutate() + }) + setIsConfirmationDestructive(true) + setIsConfirmationOpen(true) }, }, ]} @@ -176,12 +214,16 @@ const ConsolePanelWorkspaces = () => { /> ) : null} - setIsConfirmRenameOpen(false)} - onRequest={renameRequest} - /> + {confirmationHeader && confirmationBody && confirmationRequest ? ( + setIsConfirmationOpen(false)} + onRequest={confirmationRequest} + /> + ) : null} ) }