Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] [workspace]add admin validation when changing the last administrator to a lesser access #8652

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
'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 @@
});
}, [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 @@
(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 @@
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();

Check warning on line 258 in src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx#L258

Added line #L258 was not covered by tests
}}
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 @@
isTableAction={false}
selection={selection}
handleSubmitPermissionSettings={handleSubmitPermissionSettings}
openChangeAccessLevelModal={openChangeAccessLevelModal}
/>
);
};
Expand Down Expand Up @@ -362,6 +424,7 @@
permissionSettings={permissionSettings}
handleSubmitPermissionSettings={handleSubmitPermissionSettings}
openDeleteConfirmModal={openDeleteConfirmModal}
openChangeAccessLevelModal={openChangeAccessLevelModal}
/>
),
},
Expand Down Expand Up @@ -391,6 +454,7 @@
permissionSettings,
handleSubmitPermissionSettings,
openDeleteConfirmModal,
openChangeAccessLevelModal,
}: {
isTableAction: boolean;
selection?: PermissionSettingWithAccessLevelAndDisplayedType[];
Expand All @@ -405,80 +469,67 @@
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({

Check warning on line 513 in src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx#L513

Added line #L513 was not covered by tests
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
Loading