From dd6441108cb87bb35bd6247c160736a9b2835c5a Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Wed, 13 Oct 2021 18:02:42 +0200 Subject: SONAR-15440 Remove QG permissions for a user --- server/sonar-web/src/main/js/api/quality-gates.ts | 4 + .../quality-gates/components/PermissionItem.tsx | 8 +- .../components/QualityGatePermissions.tsx | 48 +++++++++--- .../components/QualityGatePermissionsRenderer.tsx | 36 +++++++-- .../components/__tests__/PermissionItem-test.tsx | 2 +- .../__tests__/QualityGatePermissions-test.tsx | 66 ++++++++++++++-- .../QualityGatePermissionsAddModal-test.tsx | 42 ++++++++++- ...QualityGatePermissionsAddModalRenderer-test.tsx | 9 +++ .../QualityGatePermissionsRenderer-test.tsx | 11 ++- .../__snapshots__/PermissionItem-test.tsx.snap | 7 +- .../QualityGatePermissions-test.tsx.snap | 5 +- ...tyGatePermissionsAddModalRenderer-test.tsx.snap | 20 +++++ .../QualityGatePermissionsRenderer-test.tsx.snap | 88 ++++++++++++++++++++-- .../src/main/js/apps/quality-gates/styles.css | 4 + 14 files changed, 315 insertions(+), 35 deletions(-) (limited to 'server') diff --git a/server/sonar-web/src/main/js/api/quality-gates.ts b/server/sonar-web/src/main/js/api/quality-gates.ts index 60cf582e567..c4c41090810 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -134,6 +134,10 @@ export function addUser(data: AddDeleteUserPermissionsParameters) { return post('/api/qualitygates/add_user', data).catch(throwGlobalError); } +export function removeUser(data: AddDeleteUserPermissionsParameters) { + return post('/api/qualitygates/remove_user', data).catch(throwGlobalError); +} + export function searchUsers(data: SearchPermissionsParameters): Promise<{ users: T.UserBase[] }> { return getJSON('/api/qualitygates/search_users', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx index 94cddb85fa0..44a99ffe2d1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx @@ -18,9 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { DeleteButton } from '../../../components/controls/buttons'; import Avatar from '../../../components/ui/Avatar'; interface Props { + onClickDelete: (user: T.UserBase) => void; user: T.UserBase; } @@ -28,13 +30,15 @@ export default function PermissionItem(props: Props) { const { user } = props; return ( -
+
-
+
{user.name}
{user.login}
+ + props.onClickDelete(user)} />
); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx index b4bad1f358a..0e0c415eeb3 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx @@ -19,7 +19,7 @@ */ import { sortBy } from 'lodash'; import * as React from 'react'; -import { addUser, searchUsers } from '../../../api/quality-gates'; +import { addUser, removeUser, searchUsers } from '../../../api/quality-gates'; import QualityGatePermissionsRenderer from './QualityGatePermissionsRenderer'; interface Props { @@ -27,16 +27,17 @@ interface Props { } interface State { - addingUser: boolean; + submitting: boolean; loading: boolean; showAddModal: boolean; + userPermissionToDelete?: T.UserBase; users: T.UserBase[]; } export default class QualityGatePermissions extends React.Component { mounted = false; state: State = { - addingUser: false, + submitting: false, loading: true, showAddModal: false, users: [] @@ -84,38 +85,67 @@ export default class QualityGatePermissions extends React.Component { const { qualityGate } = this.props; - this.setState({ addingUser: true }); + this.setState({ submitting: true }); let error = false; try { await addUser({ qualityGate: qualityGate.id, userLogin: user.login }); - } catch (_) { + } catch { error = true; } if (this.mounted) { this.setState(({ users }) => { return { - addingUser: false, + submitting: false, showAddModal: error, - users: sortBy(users.concat(user), ['name']) + users: sortBy(users.concat(user), u => u.name) }; }); } }; + handleCloseDeletePermission = () => { + this.setState({ userPermissionToDelete: undefined }); + }; + + handleClickDeletePermission = (userPermissionToDelete?: T.UserBase) => { + this.setState({ userPermissionToDelete }); + }; + + handleConfirmDeletePermission = async (user: T.UserBase) => { + const { qualityGate } = this.props; + + let error = false; + try { + await removeUser({ qualityGate: qualityGate.id, userLogin: user.login }); + } catch { + error = true; + } + + if (this.mounted && !error) { + this.setState(({ users }) => ({ + users: users.filter(u => u.login !== user.login) + })); + } + }; + render() { const { qualityGate } = this.props; - const { addingUser, loading, showAddModal, users } = this.state; + const { submitting, loading, showAddModal, userPermissionToDelete, users } = this.state; return ( ); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx index ff2be334b4c..bf29803a3a8 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx @@ -18,28 +18,34 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import { Button } from '../../../components/controls/buttons'; +import ConfirmModal from '../../../components/controls/ConfirmModal'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; import PermissionItem from './PermissionItem'; import QualityGatePermissionsAddModal from './QualityGatePermissionsAddModal'; export interface QualityGatePermissionsRendererProps { - addingUser: boolean; loading: boolean; onClickAddPermission: () => void; onCloseAddPermission: () => void; onSubmitAddPermission: (user: T.UserBase) => void; + onCloseDeletePermission: () => void; + onConfirmDeletePermission: (user: T.UserBase) => void; + onClickDeletePermission: (user: T.UserBase) => void; qualityGate: T.QualityGate; showAddModal: boolean; + submitting: boolean; + userPermissionToDelete?: T.UserBase; users: T.UserBase[]; } export default function QualityGatePermissionsRenderer(props: QualityGatePermissionsRendererProps) { - const { addingUser, loading, qualityGate, showAddModal, users } = props; + const { loading, qualityGate, showAddModal, submitting, userPermissionToDelete, users } = props; return ( -
+

{translate('quality_gates.permissions')}

@@ -48,8 +54,8 @@ export default function QualityGatePermissionsRenderer(props: QualityGatePermiss
    {users.map(user => ( -
  • - +
  • +
  • ))}
@@ -65,9 +71,27 @@ export default function QualityGatePermissionsRenderer(props: QualityGatePermiss qualityGate={qualityGate} onClose={props.onCloseAddPermission} onSubmit={props.onSubmitAddPermission} - submitting={addingUser} + submitting={submitting} /> )} + + {userPermissionToDelete && ( + + {userPermissionToDelete.name} + }} + /> + + )}
); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/PermissionItem-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/PermissionItem-test.tsx index cf6fc7c6455..4724e340757 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/PermissionItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/PermissionItem-test.tsx @@ -27,5 +27,5 @@ it('should render correctly', () => { }); function shallowRender() { - return shallow(); + return shallow(); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissions-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissions-test.tsx index 9e750d1f860..04eccce3a1c 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissions-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissions-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { addUser, searchUsers } from '../../../../api/quality-gates'; +import { addUser, removeUser, searchUsers } from '../../../../api/quality-gates'; import { mockQualityGate } from '../../../../helpers/mocks/quality-gates'; import { mockUserBase } from '../../../../helpers/mocks/users'; import { waitAndUpdate } from '../../../../helpers/testUtils'; @@ -27,6 +27,7 @@ import QualityGatePermissions from '../QualityGatePermissions'; jest.mock('../../../../api/quality-gates', () => ({ addUser: jest.fn().mockResolvedValue(undefined), + removeUser: jest.fn().mockResolvedValue(undefined), searchUsers: jest.fn().mockResolvedValue({ users: [] }) })); @@ -76,13 +77,13 @@ it('should handleSubmitAddPermission', async () => { expect(wrapper.state().users).toHaveLength(0); wrapper.instance().handleSubmitAddPermission(mockUserBase({ login: 'user1', name: 'User One' })); - expect(wrapper.state().addingUser).toBe(true); + expect(wrapper.state().submitting).toBe(true); expect(addUser).toBeCalledWith({ qualityGate: '1', userLogin: 'user1' }); await waitAndUpdate(wrapper); - expect(wrapper.state().addingUser).toBe(false); + expect(wrapper.state().submitting).toBe(false); expect(wrapper.state().showAddModal).toBe(false); expect(wrapper.state().users).toHaveLength(1); }); @@ -96,17 +97,72 @@ it('should handleSubmitAddPermission if it returns an error', async () => { expect(wrapper.state().users).toHaveLength(0); wrapper.instance().handleSubmitAddPermission(mockUserBase({ login: 'user1', name: 'User One' })); - expect(wrapper.state().addingUser).toBe(true); + expect(wrapper.state().submitting).toBe(true); expect(addUser).toBeCalledWith({ qualityGate: '1', userLogin: 'user1' }); await waitAndUpdate(wrapper); - expect(wrapper.state().addingUser).toBe(false); + expect(wrapper.state().submitting).toBe(false); expect(wrapper.state().showAddModal).toBe(true); expect(wrapper.state().users).toHaveLength(1); }); +it('should handleCloseDeletePermission', () => { + const wrapper = shallowRender(); + wrapper.setState({ userPermissionToDelete: mockUserBase() }); + wrapper.instance().handleCloseDeletePermission(); + expect(wrapper.state().userPermissionToDelete).toBeUndefined(); +}); + +it('should handleClickDeletePermission', () => { + const user = mockUserBase(); + + const wrapper = shallowRender(); + wrapper.setState({ userPermissionToDelete: undefined }); + wrapper.instance().handleClickDeletePermission(user); + expect(wrapper.state().userPermissionToDelete).toBe(user); +}); + +it('should handleConfirmDeletePermission', async () => { + const deleteThisUser = mockUserBase(); + (searchUsers as jest.Mock).mockResolvedValueOnce({ users: [deleteThisUser] }); + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().users).toHaveLength(1); + + wrapper.instance().handleConfirmDeletePermission(deleteThisUser); + + expect(removeUser).toBeCalledWith({ qualityGate: '1', userLogin: deleteThisUser.login }); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().userPermissionToDelete).toBeUndefined(); + expect(wrapper.state().users).toHaveLength(0); +}); + +it('should handleConfirmDeletePermission if it returns an error', async () => { + const deleteThisUser = mockUserBase(); + (searchUsers as jest.Mock).mockResolvedValueOnce({ users: [deleteThisUser] }); + (removeUser as jest.Mock).mockRejectedValueOnce(undefined); + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().users).toHaveLength(1); + + wrapper.instance().handleConfirmDeletePermission(deleteThisUser); + + expect(removeUser).toBeCalledWith({ qualityGate: '1', userLogin: deleteThisUser.login }); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().userPermissionToDelete).toBeUndefined(); + expect(wrapper.state().users).toHaveLength(1); +}); + function shallowRender(overrides: Partial = {}) { return shallow( diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModal-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModal-test.tsx index 59ba7c8e4a9..766688a5fff 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModal-test.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { searchUsers } from '../../../../api/quality-gates'; import { mockQualityGate } from '../../../../helpers/mocks/quality-gates'; import { mockUserBase } from '../../../../helpers/mocks/users'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils'; import QualityGatePermissionsAddModal from '../QualityGatePermissionsAddModal'; jest.mock('../../../../api/quality-gates', () => ({ @@ -32,6 +32,7 @@ jest.mock('../../../../api/quality-gates', () => ({ beforeEach(() => { jest.clearAllMocks(); }); + it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('default'); expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting'); @@ -55,6 +56,45 @@ it('should fetch users', async () => { expect(wrapper.state().searchResults).toHaveLength(1); }); +it('should handle input change', () => { + const wrapper = shallowRender(); + + wrapper.instance().handleSearch = jest.fn(); + const { handleSearch } = wrapper.instance(); + + wrapper.instance().handleInputChange('a'); + + expect(wrapper.state().query).toBe('a'); + expect(handleSearch).not.toBeCalled(); + + const query = 'query'; + wrapper.instance().handleInputChange(query); + + expect(wrapper.state().query).toBe(query); + expect(handleSearch).toBeCalledWith(query); +}); + +it('should handleSelection', () => { + const wrapper = shallowRender(); + const selection = mockUserBase(); + wrapper.instance().handleSelection(selection); + expect(wrapper.state().selection).toBe(selection); +}); + +it('should handleSubmit', () => { + const onSubmit = jest.fn(); + const wrapper = shallowRender({ onSubmit }); + + wrapper.instance().handleSubmit(mockEvent()); + + expect(onSubmit).not.toBeCalled(); + + const selection = mockUserBase(); + wrapper.setState({ selection }); + wrapper.instance().handleSubmit(mockEvent()); + expect(onSubmit).toBeCalledWith(selection); +}); + function shallowRender(overrides: Partial = {}) { return shallow( { ); }); +it('should render options correctly', () => { + const wrapper = shallowRender(); + + const { optionRenderer = () => null } = wrapper.find(Select).props(); + + expect(optionRenderer({ avatar: 'avatar', name: 'name', login: 'login' })).toMatchSnapshot(); +}); + function shallowRender(overrides: Partial = {}) { return shallow( { expect(shallowRender()).toMatchSnapshot('with users'); expect(shallowRender({ users: [] })).toMatchSnapshot('with no users'); - expect(shallowRender({ showAddModal: true })).toMatchSnapshot('show modal'); + expect(shallowRender({ showAddModal: true })).toMatchSnapshot('show add modal'); + expect(shallowRender({ userPermissionToDelete: mockUserBase() })).toMatchSnapshot( + 'show remove modal' + ); }); function shallowRender(overrides: Partial = {}) { return shallow( diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/PermissionItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/PermissionItem-test.tsx.snap index 09a811d368b..41a7f6881c1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/PermissionItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/PermissionItem-test.tsx.snap @@ -2,7 +2,7 @@ exports[`should render correctly 1`] = `
John Doe @@ -21,5 +21,8 @@ exports[`should render correctly 1`] = ` john.doe
+
`; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissions-test.tsx.snap index 3d21e32513b..dc40f7963b4 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissions-test.tsx.snap @@ -2,10 +2,12 @@ exports[`should render correctly 1`] = ` `; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap index 34084b8235a..41c4bb96a8d 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap @@ -314,3 +314,23 @@ exports[`should render correctly: submitting 1`] = ` `; + +exports[`should render options correctly 1`] = ` + + + + name + + + login + + +`; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsRenderer-test.tsx.snap index 31179eaff5a..22f6cdc7bc0 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsRenderer-test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly: show modal 1`] = ` -
+exports[`should render correctly: show add modal 1`] = ` +
@@ -20,10 +22,10 @@ exports[`should render correctly: show modal 1`] = ` >
  • `; +exports[`should render correctly: show remove modal 1`] = ` +
    +
    +

    + quality_gates.permissions +

    +
    +

    + quality_gates.permissions.help +

    +
    + +
      +
    • + +
    • +
    +
    +
    + + + , + } + } + /> + +
    +`; + exports[`should render correctly: with no users 1`] = ` -
    +
    @@ -88,7 +162,9 @@ exports[`should render correctly: with no users 1`] = ` `; exports[`should render correctly: with users 1`] = ` -
    +
    @@ -107,10 +183,10 @@ exports[`should render correctly: with users 1`] = ` >