Skip to content

Commit

Permalink
[workspace]add admin validation when changing the last administrator …
Browse files Browse the repository at this point in the history
…to a lesser access (#8621)

* fix conflict

Signed-off-by: Qxisylolo <[email protected]>

* fix conflict

Signed-off-by: Qxisylolo <[email protected]>

* add tests

Signed-off-by: Qxisylolo <[email protected]>

* add tess

Signed-off-by: Qxisylolo <[email protected]>

* fix typo and test

Signed-off-by: tygao <[email protected]>

---------

Signed-off-by: Qxisylolo <[email protected]>
Signed-off-by: tygao <[email protected]>
Co-authored-by: tygao <[email protected]>
  • Loading branch information
Qxisylolo and raintygao authored Oct 18, 2024
1 parent 8d30da4 commit a41f0c5
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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', () => {
Expand Down Expand Up @@ -64,6 +59,10 @@ describe('getDisplayedTypes', () => {
});

describe('WorkspaceCollaboratorTable', () => {
beforeEach(() => {
mockOverlays.openModal.mockClear();
});

const mockProps = {
displayedCollaboratorTypes,
permissionSettings: [
Expand Down Expand Up @@ -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,
Expand All @@ -204,17 +203,53 @@ describe('WorkspaceCollaboratorTable', () => {
},
];

const { getByText, getByTestId } = render(
const handleSubmitPermissionSettingsMock = jest.fn();

const { getByText, getByTestId, getByRole } = render(
<Provider>
<WorkspaceCollaboratorTable {...mockProps} permissionSettings={permissionSettings} />
<WorkspaceCollaboratorTable
{...mockProps}
permissionSettings={permissionSettings}
handleSubmitPermissionSettings={handleSubmitPermissionSettingsMock}
/>
<div data-test-subj="modal-container" />
</Provider>
);

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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
});
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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(
<EuiConfirmModal
data-test-subj="delete-confirm-modal"
title={i18n.translate('workspace.detail.collaborator.actions.delete', {
defaultMessage: 'Delete collaborator',
})}
Expand All @@ -221,6 +231,57 @@ export const WorkspaceCollaboratorTable = ({
return modal;
};

const openChangeAccessLevelModal = ({
onConfirm,
selections,
type,
}: {
onConfirm: () => 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(
<EuiConfirmModal
data-test-subj="change-access-confirm-modal"
title={i18n.translate('workspace.detail.collaborator.table.change.access.level', {
defaultMessage: 'Change access level',
})}
onCancel={() => {
modal.close();
}}
onConfirm={onConfirm}
cancelButtonText={deletionModalCancelButton}
confirmButtonText={deletionModalConfirmButton}
>
<EuiText color={shouldShowWarning ? 'danger' : 'default'}>
<p>
{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,
},
})}
</p>
</EuiText>
</EuiConfirmModal>
);

return modal;
};

const renderToolsLeft = () => {
if (selection.length === 0) {
return;
Expand Down Expand Up @@ -283,6 +344,7 @@ export const WorkspaceCollaboratorTable = ({
isTableAction={false}
selection={selection}
handleSubmitPermissionSettings={handleSubmitPermissionSettings}
openChangeAccessLevelModal={openChangeAccessLevelModal}
/>
);
};
Expand Down Expand Up @@ -362,6 +424,7 @@ export const WorkspaceCollaboratorTable = ({
permissionSettings={permissionSettings}
handleSubmitPermissionSettings={handleSubmitPermissionSettings}
openDeleteConfirmModal={openDeleteConfirmModal}
openChangeAccessLevelModal={openChangeAccessLevelModal}
/>
),
},
Expand Down Expand Up @@ -391,6 +454,7 @@ const Actions = ({
permissionSettings,
handleSubmitPermissionSettings,
openDeleteConfirmModal,
openChangeAccessLevelModal,
}: {
isTableAction: boolean;
selection?: PermissionSettingWithAccessLevelAndDisplayedType[];
Expand All @@ -405,80 +469,67 @@ 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();

const accessLevelOptions = (Object.keys(
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(
<EuiConfirmModal
title={i18n.translate('workspace.detail.collaborator.table.change.access.level', {
defaultMessage: 'Change access level',
})}
onCancel={() => 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"
>
<EuiText>
<p>
{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',
},
})}
</p>
</EuiText>
</EuiConfirmModal>
);
}),
});
}
modal.close();
},
selections: selection,
type: level,
});
}
},
icon: '',
Expand Down

0 comments on commit a41f0c5

Please sign in to comment.