diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2021-10-13 15:02:59 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-10-22 20:03:27 +0000 |
commit | 9076ba8bee6b8d28ea69ddb811e8951f21420bd4 (patch) | |
tree | c5d0c6c0a4680488d291ab2cbd1e253dbe358eb7 | |
parent | f1dee48a75bd18996797b269aa08ea3758df083b (diff) | |
download | sonarqube-9076ba8bee6b8d28ea69ddb811e8951f21420bd4.tar.gz sonarqube-9076ba8bee6b8d28ea69ddb811e8951f21420bd4.zip |
SONAR-15440 Add QG permissions to a user
16 files changed, 990 insertions, 42 deletions
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 ae8a2f61c8b..60cf582e567 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -21,6 +21,7 @@ import throwGlobalError from '../app/utils/throwGlobalError'; import { getJSON, post, postJSON } from '../helpers/request'; import { BranchParameters } from '../types/branch-like'; import { + AddDeleteUserPermissionsParameters, QualityGateApplicationStatus, QualityGateProjectStatus, SearchPermissionsParameters @@ -129,6 +130,10 @@ export function getQualityGateProjectStatus( .catch(throwGlobalError); } +export function addUser(data: AddDeleteUserPermissionsParameters) { + return post('/api/qualitygates/add_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/QualityGatePermissions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx index c9541a6a364..b4bad1f358a 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 @@ -17,8 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { sortBy } from 'lodash'; import * as React from 'react'; -import { searchUsers } from '../../../api/quality-gates'; +import { addUser, searchUsers } from '../../../api/quality-gates'; import QualityGatePermissionsRenderer from './QualityGatePermissionsRenderer'; interface Props { @@ -26,14 +27,18 @@ interface Props { } interface State { + addingUser: boolean; loading: boolean; + showAddModal: boolean; users: T.UserBase[]; } export default class QualityGatePermissions extends React.Component<Props, State> { mounted = false; state: State = { + addingUser: false, loading: true, + showAddModal: false, users: [] }; @@ -69,8 +74,50 @@ export default class QualityGatePermissions extends React.Component<Props, State } }; + handleCloseAddPermission = () => { + this.setState({ showAddModal: false }); + }; + + handleClickAddPermission = () => { + this.setState({ showAddModal: true }); + }; + + handleSubmitAddPermission = async (user: T.UserBase) => { + const { qualityGate } = this.props; + this.setState({ addingUser: true }); + + let error = false; + try { + await addUser({ qualityGate: qualityGate.id, userLogin: user.login }); + } catch (_) { + error = true; + } + + if (this.mounted) { + this.setState(({ users }) => { + return { + addingUser: false, + showAddModal: error, + users: sortBy(users.concat(user), ['name']) + }; + }); + } + }; + render() { - const { loading, users } = this.state; - return <QualityGatePermissionsRenderer loading={loading} users={users} />; + const { qualityGate } = this.props; + const { addingUser, loading, showAddModal, users } = this.state; + return ( + <QualityGatePermissionsRenderer + addingUser={addingUser} + loading={loading} + onClickAddPermission={this.handleClickAddPermission} + onCloseAddPermission={this.handleCloseAddPermission} + onSubmitAddPermission={this.handleSubmitAddPermission} + qualityGate={qualityGate} + showAddModal={showAddModal} + users={users} + /> + ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx new file mode 100644 index 00000000000..ce7c92f7b63 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx @@ -0,0 +1,115 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { debounce } from 'lodash'; +import * as React from 'react'; +import { searchUsers } from '../../../api/quality-gates'; +import QualityGatePermissionsAddModalRenderer from './QualityGatePermissionsAddModalRenderer'; + +interface Props { + onClose: () => void; + onSubmit: (selectedUser: T.UserBase) => void; + qualityGate: T.QualityGate; + submitting: boolean; +} + +interface State { + loading: boolean; + query?: string; + searchResults: T.UserBase[]; + selection?: T.UserBase; +} + +const DEBOUNCE_DELAY = 250; + +export default class QualityGatePermissionsAddModal extends React.Component<Props, State> { + mounted = false; + state: State = { + loading: false, + searchResults: [] + }; + + constructor(props: Props) { + super(props); + this.handleSearch = debounce(this.handleSearch, DEBOUNCE_DELAY); + } + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleSearch = (query: string) => { + const { qualityGate } = this.props; + this.setState({ loading: true }); + searchUsers({ qualityGate: qualityGate.id, q: query, selected: 'deselected' }).then( + result => { + if (this.mounted) { + this.setState({ loading: false, searchResults: result.users }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + handleInputChange = (query: string) => { + this.setState({ query }); + if (query.length > 1) { + this.handleSearch(query); + } + }; + + handleSelection = (selection: T.UserBase) => { + this.setState({ selection }); + }; + + handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { + event.preventDefault(); + const { selection } = this.state; + if (selection) { + this.props.onSubmit(selection); + } + }; + + render() { + const { submitting } = this.props; + const { loading, query = '', searchResults, selection } = this.state; + + return ( + <QualityGatePermissionsAddModalRenderer + loading={loading} + onClose={this.props.onClose} + onInputChange={this.handleInputChange} + onSelection={this.handleSelection} + onSubmit={this.handleSubmit} + query={query} + searchResults={searchResults} + selection={selection} + submitting={submitting} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx new file mode 100644 index 00000000000..dd4f7355f5d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx @@ -0,0 +1,97 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; +import Modal from '../../../components/controls/Modal'; +import Select from '../../../components/controls/Select'; +import Avatar from '../../../components/ui/Avatar'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +export interface QualityGatePermissionsAddModalRendererProps { + onClose: () => void; + onInputChange: (query: string) => void; + onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void; + onSelection: (selection: T.UserBase) => void; + submitting: boolean; + loading: boolean; + query: string; + searchResults: T.UserBase[]; + selection?: T.UserBase; +} + +type Option = T.UserBase & { value: string }; + +export default function QualityGatePermissionsAddModalRenderer( + props: QualityGatePermissionsAddModalRendererProps +) { + const { loading, query = '', searchResults, selection, submitting } = props; + + const header = translate('quality_gates.permissions.grant'); + + const noResultsText = + query.length === 1 ? translateWithParameters('select2.tooShort', 2) : translate('no_results'); + + return ( + <Modal contentLabel={header} onRequestClose={props.onClose}> + <header className="modal-head"> + <h2>{header}</h2> + </header> + <form onSubmit={props.onSubmit}> + <div className="modal-body"> + <div className="modal-field"> + <label>{translate('quality_gates.permissions.search')}</label> + <Select + autoFocus={true} + className="Select-big" + clearable={false} + // disable default react-select filtering + filterOptions={i => i} + isLoading={loading} + noResultsText={noResultsText} + onChange={props.onSelection} + onInputChange={props.onInputChange} + optionRenderer={optionRenderer} + options={searchResults.map(r => ({ ...r, value: r.login }))} + placeholder="" + searchable={true} + value={selection} + valueRenderer={optionRenderer} + /> + </div> + </div> + <footer className="modal-foot"> + {submitting && <i className="spinner spacer-right" />} + <SubmitButton disabled={!selection || submitting}>{translate('add_verb')}</SubmitButton> + <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink> + </footer> + </form> + </Modal> + ); +} + +function optionRenderer(option: Option) { + return ( + <> + <Avatar hash={option.avatar} name={option.name} size={16} /> + <strong className="spacer-left">{option.name}</strong> + <span className="note little-spacer-left">{option.login}</span> + </> + ); +} 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 0e60ff10db8..ff2be334b4c 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,17 +18,25 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { Button } from '../../../components/controls/buttons'; 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; + qualityGate: T.QualityGate; + showAddModal: boolean; users: T.UserBase[]; } export default function QualityGatePermissionsRenderer(props: QualityGatePermissionsRendererProps) { - const { loading, users } = props; + const { addingUser, loading, qualityGate, showAddModal, users } = props; return ( <div> @@ -36,15 +44,30 @@ export default function QualityGatePermissionsRenderer(props: QualityGatePermiss <h3>{translate('quality_gates.permissions')}</h3> </header> <p className="spacer-bottom">{translate('quality_gates.permissions.help')}</p> - <DeferredSpinner loading={loading}> - <ul> - {users.map(user => ( - <li key={user.login} className="spacer-top"> - <PermissionItem user={user} /> - </li> - ))} - </ul> - </DeferredSpinner> + <div> + <DeferredSpinner loading={loading}> + <ul> + {users.map(user => ( + <li key={user.login} className="spacer-top"> + <PermissionItem user={user} /> + </li> + ))} + </ul> + </DeferredSpinner> + </div> + + <Button className="big-spacer-top" onClick={props.onClickAddPermission}> + {translate('quality_gates.permissions.grant')} + </Button> + + {showAddModal && ( + <QualityGatePermissionsAddModal + qualityGate={qualityGate} + onClose={props.onCloseAddPermission} + onSubmit={props.onSubmitAddPermission} + submitting={addingUser} + /> + )} </div> ); } 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 b89c4a368be..9e750d1f860 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,27 +19,96 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { searchUsers } from '../../../../api/quality-gates'; +import { addUser, searchUsers } from '../../../../api/quality-gates'; import { mockQualityGate } from '../../../../helpers/mocks/quality-gates'; +import { mockUserBase } from '../../../../helpers/mocks/users'; import { waitAndUpdate } from '../../../../helpers/testUtils'; import QualityGatePermissions from '../QualityGatePermissions'; jest.mock('../../../../api/quality-gates', () => ({ + addUser: jest.fn().mockResolvedValue(undefined), searchUsers: jest.fn().mockResolvedValue({ users: [] }) })); +beforeEach(() => { + jest.clearAllMocks(); +}); + it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); }); it('should fetch users', async () => { const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(searchUsers).toBeCalledWith({ qualityGate: '1', selected: 'selected' }); +}); +it('should fetch users on update', async () => { + const wrapper = shallowRender(); await waitAndUpdate(wrapper); - expect(searchUsers).toBeCalledWith({ qualityGate: '1', selected: 'selected' }); + (searchUsers as jest.Mock).mockClear(); + + wrapper.setProps({ qualityGate: mockQualityGate({ id: '2' }) }); + expect(searchUsers).toBeCalledWith({ qualityGate: '2', selected: 'selected' }); +}); + +it('should handleCloseAddPermission', () => { + const wrapper = shallowRender(); + wrapper.setState({ showAddModal: true }); + wrapper.instance().handleCloseAddPermission(); + expect(wrapper.state().showAddModal).toBe(false); +}); + +it('should handleClickAddPermission', () => { + const wrapper = shallowRender(); + wrapper.setState({ showAddModal: false }); + wrapper.instance().handleClickAddPermission(); + expect(wrapper.state().showAddModal).toBe(true); +}); + +it('should handleSubmitAddPermission', async () => { + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().users).toHaveLength(0); + + wrapper.instance().handleSubmitAddPermission(mockUserBase({ login: 'user1', name: 'User One' })); + expect(wrapper.state().addingUser).toBe(true); + + expect(addUser).toBeCalledWith({ qualityGate: '1', userLogin: 'user1' }); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().addingUser).toBe(false); + expect(wrapper.state().showAddModal).toBe(false); + expect(wrapper.state().users).toHaveLength(1); +}); + +it('should handleSubmitAddPermission if it returns an error', async () => { + (addUser as jest.Mock).mockRejectedValueOnce(undefined); + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().users).toHaveLength(0); + + wrapper.instance().handleSubmitAddPermission(mockUserBase({ login: 'user1', name: 'User One' })); + expect(wrapper.state().addingUser).toBe(true); + + expect(addUser).toBeCalledWith({ qualityGate: '1', userLogin: 'user1' }); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().addingUser).toBe(false); + expect(wrapper.state().showAddModal).toBe(true); + expect(wrapper.state().users).toHaveLength(1); }); function shallowRender(overrides: Partial<QualityGatePermissions['props']> = {}) { - return shallow(<QualityGatePermissions qualityGate={mockQualityGate()} {...overrides} />); + return shallow<QualityGatePermissions>( + <QualityGatePermissions qualityGate={mockQualityGate()} {...overrides} /> + ); } 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 new file mode 100644 index 00000000000..59ba7c8e4a9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModal-test.tsx @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +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 QualityGatePermissionsAddModal from '../QualityGatePermissionsAddModal'; + +jest.mock('../../../../api/quality-gates', () => ({ + searchUsers: jest.fn().mockResolvedValue({ users: [] }) +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting'); +}); + +it('should fetch users', async () => { + (searchUsers as jest.Mock).mockResolvedValueOnce({ users: [mockUserBase()] }); + + const wrapper = shallowRender(); + + const query = 'query'; + + wrapper.instance().handleSearch(query); + + expect(wrapper.state().loading).toBe(true); + expect(searchUsers).toBeCalledWith({ qualityGate: '1', q: query, selected: 'deselected' }); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().loading).toBe(false); + expect(wrapper.state().searchResults).toHaveLength(1); +}); + +function shallowRender(overrides: Partial<QualityGatePermissionsAddModal['props']> = {}) { + return shallow<QualityGatePermissionsAddModal>( + <QualityGatePermissionsAddModal + onClose={jest.fn()} + onSubmit={jest.fn()} + submitting={false} + qualityGate={mockQualityGate()} + {...overrides} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx new file mode 100644 index 00000000000..5801c783900 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { mockUserBase } from '../../../../helpers/mocks/users'; +import QualityGatePermissionsAddModalRenderer, { + QualityGatePermissionsAddModalRendererProps +} from '../QualityGatePermissionsAddModalRenderer'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ query: 'a' })).toMatchSnapshot('short query'); + expect(shallowRender({ selection: mockUserBase() })).toMatchSnapshot('selection'); + expect(shallowRender({ selection: mockUserBase(), submitting: true })).toMatchSnapshot( + 'submitting' + ); + expect(shallowRender({ query: 'ab', searchResults: [mockUserBase()] })).toMatchSnapshot( + 'query and results' + ); +}); + +function shallowRender(overrides: Partial<QualityGatePermissionsAddModalRendererProps> = {}) { + return shallow( + <QualityGatePermissionsAddModalRenderer + loading={false} + onClose={jest.fn()} + onInputChange={jest.fn()} + onSelection={jest.fn()} + onSubmit={jest.fn()} + query="" + searchResults={[]} + submitting={false} + {...overrides} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsRenderer-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsRenderer-test.tsx index 6538f6641cb..f18b3c7a067 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsRenderer-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockQualityGate } from '../../../../helpers/mocks/quality-gates'; import { mockUser } from '../../../../helpers/testMocks'; import QualityGatePermissionsRenderer, { QualityGatePermissionsRendererProps @@ -27,10 +28,21 @@ import QualityGatePermissionsRenderer, { it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('with users'); expect(shallowRender({ users: [] })).toMatchSnapshot('with no users'); + expect(shallowRender({ showAddModal: true })).toMatchSnapshot('show modal'); }); function shallowRender(overrides: Partial<QualityGatePermissionsRendererProps> = {}) { return shallow( - <QualityGatePermissionsRenderer loading={false} users={[mockUser()]} {...overrides} /> + <QualityGatePermissionsRenderer + addingUser={false} + loading={false} + onClickAddPermission={jest.fn()} + onCloseAddPermission={jest.fn()} + onSubmitAddPermission={jest.fn()} + qualityGate={mockQualityGate()} + showAddModal={false} + users={[mockUser()]} + {...overrides} + /> ); } 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 ce01fd4ae76..3d21e32513b 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,7 +2,18 @@ exports[`should render correctly 1`] = ` <QualityGatePermissionsRenderer + addingUser={false} loading={true} + onClickAddPermission={[Function]} + onCloseAddPermission={[Function]} + onSubmitAddPermission={[Function]} + qualityGate={ + Object { + "id": "1", + "name": "qualitygate", + } + } + showAddModal={false} users={Array []} /> `; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModal-test.tsx.snap new file mode 100644 index 00000000000..6711d9c224d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModal-test.tsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +<QualityGatePermissionsAddModalRenderer + loading={false} + onClose={[MockFunction]} + onInputChange={[Function]} + onSelection={[Function]} + onSubmit={[Function]} + query="" + searchResults={Array []} + submitting={false} +/> +`; + +exports[`should render correctly: submitting 1`] = ` +<QualityGatePermissionsAddModalRenderer + loading={false} + onClose={[MockFunction]} + onInputChange={[Function]} + onSelection={[Function]} + onSubmit={[Function]} + query="" + searchResults={Array []} + submitting={true} +/> +`; 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 new file mode 100644 index 00000000000..34084b8235a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap @@ -0,0 +1,316 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +<Modal + contentLabel="quality_gates.permissions.grant" + onRequestClose={[MockFunction]} +> + <header + className="modal-head" + > + <h2> + quality_gates.permissions.grant + </h2> + </header> + <form + onSubmit={[MockFunction]} + > + <div + className="modal-body" + > + <div + className="modal-field" + > + <label> + quality_gates.permissions.search + </label> + <Select + autoFocus={true} + className="Select-big" + clearable={false} + filterOptions={[Function]} + isLoading={false} + noResultsText="no_results" + onChange={[MockFunction]} + onInputChange={[MockFunction]} + optionRenderer={[Function]} + options={Array []} + placeholder="" + searchable={true} + valueRenderer={[Function]} + /> + </div> + </div> + <footer + className="modal-foot" + > + <SubmitButton + disabled={true} + > + add_verb + </SubmitButton> + <ResetButtonLink + onClick={[MockFunction]} + > + cancel + </ResetButtonLink> + </footer> + </form> +</Modal> +`; + +exports[`should render correctly: query and results 1`] = ` +<Modal + contentLabel="quality_gates.permissions.grant" + onRequestClose={[MockFunction]} +> + <header + className="modal-head" + > + <h2> + quality_gates.permissions.grant + </h2> + </header> + <form + onSubmit={[MockFunction]} + > + <div + className="modal-body" + > + <div + className="modal-field" + > + <label> + quality_gates.permissions.search + </label> + <Select + autoFocus={true} + className="Select-big" + clearable={false} + filterOptions={[Function]} + isLoading={false} + noResultsText="no_results" + onChange={[MockFunction]} + onInputChange={[MockFunction]} + optionRenderer={[Function]} + options={ + Array [ + Object { + "login": "userlogin", + "value": "userlogin", + }, + ] + } + placeholder="" + searchable={true} + valueRenderer={[Function]} + /> + </div> + </div> + <footer + className="modal-foot" + > + <SubmitButton + disabled={true} + > + add_verb + </SubmitButton> + <ResetButtonLink + onClick={[MockFunction]} + > + cancel + </ResetButtonLink> + </footer> + </form> +</Modal> +`; + +exports[`should render correctly: selection 1`] = ` +<Modal + contentLabel="quality_gates.permissions.grant" + onRequestClose={[MockFunction]} +> + <header + className="modal-head" + > + <h2> + quality_gates.permissions.grant + </h2> + </header> + <form + onSubmit={[MockFunction]} + > + <div + className="modal-body" + > + <div + className="modal-field" + > + <label> + quality_gates.permissions.search + </label> + <Select + autoFocus={true} + className="Select-big" + clearable={false} + filterOptions={[Function]} + isLoading={false} + noResultsText="no_results" + onChange={[MockFunction]} + onInputChange={[MockFunction]} + optionRenderer={[Function]} + options={Array []} + placeholder="" + searchable={true} + value={ + Object { + "login": "userlogin", + } + } + valueRenderer={[Function]} + /> + </div> + </div> + <footer + className="modal-foot" + > + <SubmitButton + disabled={false} + > + add_verb + </SubmitButton> + <ResetButtonLink + onClick={[MockFunction]} + > + cancel + </ResetButtonLink> + </footer> + </form> +</Modal> +`; + +exports[`should render correctly: short query 1`] = ` +<Modal + contentLabel="quality_gates.permissions.grant" + onRequestClose={[MockFunction]} +> + <header + className="modal-head" + > + <h2> + quality_gates.permissions.grant + </h2> + </header> + <form + onSubmit={[MockFunction]} + > + <div + className="modal-body" + > + <div + className="modal-field" + > + <label> + quality_gates.permissions.search + </label> + <Select + autoFocus={true} + className="Select-big" + clearable={false} + filterOptions={[Function]} + isLoading={false} + noResultsText="select2.tooShort.2" + onChange={[MockFunction]} + onInputChange={[MockFunction]} + optionRenderer={[Function]} + options={Array []} + placeholder="" + searchable={true} + valueRenderer={[Function]} + /> + </div> + </div> + <footer + className="modal-foot" + > + <SubmitButton + disabled={true} + > + add_verb + </SubmitButton> + <ResetButtonLink + onClick={[MockFunction]} + > + cancel + </ResetButtonLink> + </footer> + </form> +</Modal> +`; + +exports[`should render correctly: submitting 1`] = ` +<Modal + contentLabel="quality_gates.permissions.grant" + onRequestClose={[MockFunction]} +> + <header + className="modal-head" + > + <h2> + quality_gates.permissions.grant + </h2> + </header> + <form + onSubmit={[MockFunction]} + > + <div + className="modal-body" + > + <div + className="modal-field" + > + <label> + quality_gates.permissions.search + </label> + <Select + autoFocus={true} + className="Select-big" + clearable={false} + filterOptions={[Function]} + isLoading={false} + noResultsText="no_results" + onChange={[MockFunction]} + onInputChange={[MockFunction]} + optionRenderer={[Function]} + options={Array []} + placeholder="" + searchable={true} + value={ + Object { + "login": "userlogin", + } + } + valueRenderer={[Function]} + /> + </div> + </div> + <footer + className="modal-foot" + > + <i + className="spinner spacer-right" + /> + <SubmitButton + disabled={true} + > + add_verb + </SubmitButton> + <ResetButtonLink + onClick={[MockFunction]} + > + cancel + </ResetButtonLink> + </footer> + </form> +</Modal> +`; 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 1e2f83f6744..31179eaff5a 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,5 +1,62 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should render correctly: show modal 1`] = ` +<div> + <header + className="display-flex-center spacer-bottom" + > + <h3> + quality_gates.permissions + </h3> + </header> + <p + className="spacer-bottom" + > + quality_gates.permissions.help + </p> + <div> + <DeferredSpinner + loading={false} + > + <ul> + <li + className="spacer-top" + key="john.doe" + > + <PermissionItem + user={ + Object { + "active": true, + "local": true, + "login": "john.doe", + "name": "John Doe", + } + } + /> + </li> + </ul> + </DeferredSpinner> + </div> + <Button + className="big-spacer-top" + onClick={[MockFunction]} + > + quality_gates.permissions.grant + </Button> + <QualityGatePermissionsAddModal + onClose={[MockFunction]} + onSubmit={[MockFunction]} + qualityGate={ + Object { + "id": "1", + "name": "qualitygate", + } + } + submitting={false} + /> +</div> +`; + exports[`should render correctly: with no users 1`] = ` <div> <header @@ -14,11 +71,19 @@ exports[`should render correctly: with no users 1`] = ` > quality_gates.permissions.help </p> - <DeferredSpinner - loading={false} + <div> + <DeferredSpinner + loading={false} + > + <ul /> + </DeferredSpinner> + </div> + <Button + className="big-spacer-top" + onClick={[MockFunction]} > - <ul /> - </DeferredSpinner> + quality_gates.permissions.grant + </Button> </div> `; @@ -36,26 +101,34 @@ exports[`should render correctly: with users 1`] = ` > quality_gates.permissions.help </p> - <DeferredSpinner - loading={false} - > - <ul> - <li - className="spacer-top" - key="john.doe" - > - <PermissionItem - user={ - Object { - "active": true, - "local": true, - "login": "john.doe", - "name": "John Doe", + <div> + <DeferredSpinner + loading={false} + > + <ul> + <li + className="spacer-top" + key="john.doe" + > + <PermissionItem + user={ + Object { + "active": true, + "local": true, + "login": "john.doe", + "name": "John Doe", + } } - } - /> - </li> - </ul> - </DeferredSpinner> + /> + </li> + </ul> + </DeferredSpinner> + </div> + <Button + className="big-spacer-top" + onClick={[MockFunction]} + > + quality_gates.permissions.grant + </Button> </div> `; diff --git a/server/sonar-web/src/main/js/helpers/mocks/users.ts b/server/sonar-web/src/main/js/helpers/mocks/users.ts new file mode 100644 index 00000000000..b1bd18b7566 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/mocks/users.ts @@ -0,0 +1,26 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +export function mockUserBase(overrides: Partial<T.UserBase> = {}): T.UserBase { + return { + login: 'userlogin', + ...overrides + }; +} diff --git a/server/sonar-web/src/main/js/types/quality-gates.ts b/server/sonar-web/src/main/js/types/quality-gates.ts index ae17deb3c0a..425b401d5de 100644 --- a/server/sonar-web/src/main/js/types/quality-gates.ts +++ b/server/sonar-web/src/main/js/types/quality-gates.ts @@ -86,3 +86,8 @@ export interface SearchPermissionsParameters { q?: string; selected?: 'all' | 'selected' | 'deselected'; } + +export interface AddDeleteUserPermissionsParameters { + qualityGate: string; + userLogin: string; +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 2a56f06c555..2eccc6a6bc3 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1736,7 +1736,8 @@ quality_gates.status=Quality Gate status quality_gates.help=A Quality Gate is a set of measure-based, Boolean conditions. It helps you know immediately whether your projects are production-ready. Ideally, all projects will use the same quality gate. Each project's Quality Gate status is displayed prominently on its homepage. quality_gates.permissions=Permissions quality_gates.permissions.help=Users with the global "Manage Quality Gates" permission can manage this Quality Gate. - +quality_gates.permissions.grant=Grant permissions to a user +quality_gates.permissions.search=Search users by login or name: #------------------------------------------------------------------------------ # |