From a41f0c5177cf444a882752275c8627999c9bfd43 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Fri, 18 Oct 2024 20:00:09 +0800 Subject: [PATCH] [workspace]add admin validation when changing the last administrator to a lesser access (#8621) * fix conflict Signed-off-by: Qxisylolo * fix conflict Signed-off-by: Qxisylolo * add tests Signed-off-by: Qxisylolo * add tess Signed-off-by: Qxisylolo * fix typo and test Signed-off-by: tygao --------- Signed-off-by: Qxisylolo Signed-off-by: tygao Co-authored-by: tygao --- .../workspace_collaborator_table.test.tsx | 65 +++++-- .../workspace_collaborator_table.tsx | 175 +++++++++++------- 2 files changed, 163 insertions(+), 77 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.test.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.test.tsx index d823fe477474..051bb5fa9037 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.test.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.test.tsx @@ -4,8 +4,8 @@ */ import React from 'react'; -import { fireEvent, render } from '@testing-library/react'; - +import { fireEvent, render, waitFor, within } from '@testing-library/react'; +import ReactDOM from 'react-dom'; import { WorkspaceCollaboratorTable, getDisplayedType } from './workspace_collaborator_table'; import { createOpenSearchDashboardsReactContext } from '../../../../opensearch_dashboards_react/public'; import { coreMock } from '../../../../../core/public/mocks'; @@ -28,14 +28,9 @@ const displayedCollaboratorTypes = [ }, ]; -const mockOverlays = { - openModal: jest.fn(), -}; +const mockOverlays = mockCoreStart.overlays; -const { Provider } = createOpenSearchDashboardsReactContext({ - ...mockCoreStart, - overlays: mockOverlays, -}); +const { Provider } = createOpenSearchDashboardsReactContext(mockCoreStart); describe('getDisplayedTypes', () => { it('should return undefined if not match any collaborator type', () => { @@ -64,6 +59,10 @@ describe('getDisplayedTypes', () => { }); describe('WorkspaceCollaboratorTable', () => { + beforeEach(() => { + mockOverlays.openModal.mockClear(); + }); + const mockProps = { displayedCollaboratorTypes, permissionSettings: [ @@ -188,7 +187,7 @@ describe('WorkspaceCollaboratorTable', () => { expect(mockOverlays.openModal).toHaveBeenCalled(); }); - it('should openModal when clicking action tools when multi selection', () => { + it('should openModal and show warning text when changing last admin to a less permission level', async () => { const permissionSettings = [ { id: 0, @@ -204,17 +203,53 @@ describe('WorkspaceCollaboratorTable', () => { }, ]; - const { getByText, getByTestId } = render( + const handleSubmitPermissionSettingsMock = jest.fn(); + + const { getByText, getByTestId, getByRole } = render( - + +
); + + mockOverlays.openModal.mockReturnValue({ + onClose: Promise.resolve(), + close: async () => { + ReactDOM.unmountComponentAtNode(getByTestId('modal-container')); + }, + }); + fireEvent.click(getByTestId('checkboxSelectRow-0')); fireEvent.click(getByTestId('checkboxSelectRow-1')); const actions = getByTestId('workspace-detail-collaborator-table-actions'); fireEvent.click(actions); - const changeAccessLevel = getByText('Change access level'); - fireEvent.click(changeAccessLevel); - expect(mockOverlays.openModal).toHaveBeenCalled(); + fireEvent.click(getByText('Change access level')); + await waitFor(() => { + fireEvent.click(within(getByRole('dialog')).getByText('Read only')); + }); + mockOverlays.openModal.mock.calls[0][0](getByTestId('modal-container')); + await waitFor(() => { + expect(getByText('Confirm')).toBeInTheDocument(); + }); + expect( + getByText( + 'By changing the last administrator to a lesser access, only application administrators will be able to manage this workspace' + ) + ).toBeInTheDocument(); + jest.useFakeTimers(); + fireEvent.click(getByText('Confirm')); + + await waitFor(() => { + expect(handleSubmitPermissionSettingsMock).toHaveBeenCalledWith([ + { id: 0, modes: ['library_read', 'read'], type: 'user', userId: 'admin' }, + { group: 'group', id: 1, modes: ['library_read', 'read'], type: 'group' }, + ]); + }); + jest.runAllTimers(); + jest.useRealTimers(); }); }); diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx index a934ca73ac5c..4642554c4c19 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx @@ -65,9 +65,18 @@ const deletionModalWarning = i18n.translate( 'workspace.workspace.detail.collaborator.modal.delete.warning', { defaultMessage: - 'Currently you’re the only user who has access to the workspace as an owner. Share this workspace by adding collaborators.', + 'By removing the last administrator, only application administrators will be able to manage this workspace', } ); + +const changeAccessModalWarning = i18n.translate( + 'workspace.workspace.detail.collaborator.modal.changeAccessLevel.warning', + { + defaultMessage: + 'By changing the last administrator to a lesser access, only application administrators will be able to manage this workspace', + } +); + const deletionModalConfirm = i18n.translate('workspace.detail.collaborator.modal.delete.confirm', { defaultMessage: 'Delete collaborator? The collaborators will not have access to the workspace.', }); @@ -152,7 +161,7 @@ export const WorkspaceCollaboratorTable = ({ }); }, [permissionSettings, displayedCollaboratorTypes]); - const adminCollarboratorsNum = useMemo(() => { + const adminCollaboratorsNum = useMemo(() => { const admins = items.filter((item) => item.accessLevel === WORKSPACE_ACCESS_LEVEL_NAMES.admin); return admins.length; }, [items]); @@ -202,9 +211,10 @@ export const WorkspaceCollaboratorTable = ({ (item) => item.accessLevel === WORKSPACE_ACCESS_LEVEL_NAMES.admin ).length; const shouldShowWarning = - adminCollarboratorsNum === adminOfSelection && adminCollarboratorsNum !== 0; + adminCollaboratorsNum === adminOfSelection && adminCollaboratorsNum !== 0; const modal = overlays.openModal( void; + selections: PermissionSettingWithAccessLevelAndDisplayedType[]; + type: WorkspaceCollaboratorAccessLevel; + }) => { + let shouldShowWarning = false; + if (type !== 'admin') { + const adminOfSelection = selections.filter( + (item) => item.accessLevel === WORKSPACE_ACCESS_LEVEL_NAMES.admin + ).length; + shouldShowWarning = adminCollaboratorsNum - adminOfSelection < 1 && adminCollaboratorsNum > 0; + } + + const modal = overlays.openModal( + { + modal.close(); + }} + onConfirm={onConfirm} + cancelButtonText={deletionModalCancelButton} + confirmButtonText={deletionModalConfirmButton} + > + +

+ {shouldShowWarning + ? changeAccessModalWarning + : i18n.translate('workspace.detail.collaborator.changeAccessLevel.confirmation', { + defaultMessage: + 'Do you want to change access level of {numCollaborators} collaborator{pluralSuffix} to "{accessLevel}"?', + values: { + numCollaborators: selections.length, + pluralSuffix: selections.length > 1 ? 's' : '', + accessLevel: type, + }, + })} +

+
+
+ ); + + return modal; + }; + const renderToolsLeft = () => { if (selection.length === 0) { return; @@ -283,6 +344,7 @@ export const WorkspaceCollaboratorTable = ({ isTableAction={false} selection={selection} handleSubmitPermissionSettings={handleSubmitPermissionSettings} + openChangeAccessLevelModal={openChangeAccessLevelModal} /> ); }; @@ -362,6 +424,7 @@ export const WorkspaceCollaboratorTable = ({ permissionSettings={permissionSettings} handleSubmitPermissionSettings={handleSubmitPermissionSettings} openDeleteConfirmModal={openDeleteConfirmModal} + openChangeAccessLevelModal={openChangeAccessLevelModal} /> ), }, @@ -391,6 +454,7 @@ const Actions = ({ permissionSettings, handleSubmitPermissionSettings, openDeleteConfirmModal, + openChangeAccessLevelModal, }: { isTableAction: boolean; selection?: PermissionSettingWithAccessLevelAndDisplayedType[]; @@ -405,10 +469,18 @@ const Actions = ({ onConfirm: () => void; selections: PermissionSettingWithAccessLevelAndDisplayedType[]; }) => { close: () => void }; + openChangeAccessLevelModal?: ({ + onConfirm, + selections, + type, + }: { + onConfirm: () => void; + selections: PermissionSettingWithAccessLevelAndDisplayedType[]; + type: WorkspaceCollaboratorAccessLevel; + }) => { close: () => void }; }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const { - overlays, services: { notifications }, } = useOpenSearchDashboards(); @@ -416,69 +488,48 @@ const Actions = ({ WORKSPACE_ACCESS_LEVEL_NAMES ) as WorkspaceCollaboratorAccessLevel[]).map((level) => ({ name: WORKSPACE_ACCESS_LEVEL_NAMES[level], - onClick: async () => { + onClick: () => { setIsPopoverOpen(false); - if (selection) { - const modal = overlays.openModal( - modal.close()} - onConfirm={async () => { - let newSettings = permissionSettings; - selection.forEach(({ id }) => { - newSettings = newSettings.map((item) => - id === item.id - ? { - ...item, - modes: accessLevelNameToWorkspacePermissionModesMap[level], - } - : item - ); - }); - const result = await handleSubmitPermissionSettings( - newSettings as WorkspacePermissionSetting[] - ); - if (result?.success) { - notifications?.toasts?.addSuccess({ - title: i18n.translate( - 'workspace.detail.collaborator.change.access.success.title', - { - defaultMessage: 'The access level changed', + if (selection && openChangeAccessLevelModal) { + const modal = openChangeAccessLevelModal({ + onConfirm: async () => { + let newSettings = permissionSettings; + selection.forEach(({ id }) => { + newSettings = newSettings.map((item) => + id === item.id + ? { + ...item, + modes: accessLevelNameToWorkspacePermissionModesMap[level], } - ), - text: i18n.translate('workspace.detail.collaborator.change.access.success.body', { - defaultMessage: - 'The access level is changed to {level} for {num} collaborator{pluralSuffix, select, true {} other {s}}.', - values: { - level: WORKSPACE_ACCESS_LEVEL_NAMES[level], - num: selection.length, - pluralSuffix: selection.length === 1, - }, - }), - }); - } - modal.close(); - }} - cancelButtonText="Cancel" - confirmButtonText="Confirm" - > - -

- {i18n.translate('workspace.detail.collaborator.changeAccessLevel.confirmation', { + : item + ); + }); + + const result = await handleSubmitPermissionSettings( + newSettings as WorkspacePermissionSetting[] + ); + + if (result?.success) { + notifications?.toasts?.addSuccess({ + title: i18n.translate('workspace.detail.collaborator.change.access.success.title', { + defaultMessage: 'The access level changed', + }), + text: i18n.translate('workspace.detail.collaborator.change.access.success.body', { defaultMessage: - 'Do you want to change access level to {numCollaborators} collaborator{pluralSuffix, select, true {} other {s}} to "{accessLevel}"?', + 'The access level is changed to {level} for {num} collaborator{pluralSuffix, select, true {} other {s}}.', values: { - numCollaborators: selection.length, - pluralSuffix: selection.length === 1, - accessLevel: WORKSPACE_ACCESS_LEVEL_NAMES[level], + level: WORKSPACE_ACCESS_LEVEL_NAMES[level], + num: selection.length, + pluralSuffix: selection.length === 1 ? '' : 's', }, - })} -

-
-
- ); + }), + }); + } + modal.close(); + }, + selections: selection, + type: level, + }); } }, icon: '',