aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2021-10-13 15:02:59 +0200
committersonartech <sonartech@sonarsource.com>2021-10-22 20:03:27 +0000
commit9076ba8bee6b8d28ea69ddb811e8951f21420bd4 (patch)
treec5d0c6c0a4680488d291ab2cbd1e253dbe358eb7
parentf1dee48a75bd18996797b269aa08ea3758df083b (diff)
downloadsonarqube-9076ba8bee6b8d28ea69ddb811e8951f21420bd4.tar.gz
sonarqube-9076ba8bee6b8d28ea69ddb811e8951f21420bd4.zip
SONAR-15440 Add QG permissions to a user
-rw-r--r--server/sonar-web/src/main/js/api/quality-gates.ts5
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx115
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx97
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsRenderer.tsx43
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissions-test.tsx75
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModal-test.tsx68
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsRenderer-test.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissions-test.tsx.snap11
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModal-test.tsx.snap27
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap316
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsRenderer-test.tsx.snap121
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/users.ts26
-rw-r--r--server/sonar-web/src/main/js/types/quality-gates.ts5
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties3
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:
#------------------------------------------------------------------------------
#