aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2022-04-05 18:01:14 +0200
committersonartech <sonartech@sonarsource.com>2022-04-06 20:03:00 +0000
commit86e4825c2a905c452492d5796bd6d6e112ad3748 (patch)
tree201a75390c3bd1139c38eca2dd7a23ddf296ab5d
parent701d3e7366f62ec4ade502e09219ec4859bfd941 (diff)
downloadsonarqube-86e4825c2a905c452492d5796bd6d6e112ad3748.tar.gz
sonarqube-86e4825c2a905c452492d5796bd6d6e112ad3748.zip
SONAR-16238 Fix QP permission Select behavior
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx93
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsFormSelect-test.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap32
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsFormSelect-test.tsx.snap38
7 files changed, 83 insertions, 189 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
index a8de3c4f436..7cd6f079125 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
+++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
@@ -37,7 +37,7 @@ beforeAll(() => {
afterEach(() => handler.reset());
-jest.setTimeout(10_000);
+jest.setTimeout(20_000);
it('should list all rules', async () => {
renderCodingRulesApp();
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
index 96035b0c7c4..420a235e587 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
@@ -18,13 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import {
- addGroup,
- addUser,
- searchGroups,
- searchUsers,
- SearchUsersGroupsParameters
-} from '../../../api/quality-profiles';
+import { addGroup, addUser } from '../../../api/quality-profiles';
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import Modal from '../../../components/controls/Modal';
import { translate } from '../../../helpers/l10n';
@@ -97,28 +91,12 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
}
};
- handleSearch = (q: string) => {
- const { profile } = this.props;
- const parameters: SearchUsersGroupsParameters = {
- language: profile.language,
- q,
- qualityProfile: profile.name,
- selected: 'deselected'
- };
- return Promise.all([
- searchUsers(parameters),
- searchGroups(parameters)
- ]).then(([usersResponse, groupsResponse]) => [
- ...usersResponse.users,
- ...groupsResponse.groups
- ]);
- };
-
handleValueChange = (selected: UserSelected | Group) => {
this.setState({ selected });
};
render() {
+ const { profile } = this.props;
const header = translate('quality_profiles.grant_permissions_to_user_or_group');
const submitDisabled = !this.state.selected || this.state.submitting;
return (
@@ -132,11 +110,7 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
<label htmlFor="change-profile-permission-input">
{translate('quality_profiles.search_description')}
</label>
- <ProfilePermissionsFormSelect
- onChange={this.handleValueChange}
- onSearch={this.handleSearch}
- selected={this.state.selected}
- />
+ <ProfilePermissionsFormSelect onChange={this.handleValueChange} profile={profile} />
</div>
</div>
<footer className="modal-foot">
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx
index 4ffb8d67b0b..cb4d7abef7b 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx
@@ -17,10 +17,15 @@
* 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, identity, omit } from 'lodash';
+import { debounce, omit } from 'lodash';
import * as React from 'react';
import { components, ControlProps, OptionProps, SingleValueProps } from 'react-select';
-import Select from '../../../components/controls/Select';
+import {
+ searchGroups,
+ searchUsers,
+ SearchUsersGroupsParameters
+} from '../../../api/quality-profiles';
+import { SearchSelect } from '../../../components/controls/Select';
import GroupIcon from '../../../components/icons/GroupIcon';
import Avatar from '../../../components/ui/Avatar';
import { translate } from '../../../helpers/l10n';
@@ -32,58 +37,17 @@ type OptionWithValue = Option & { value: string };
interface Props {
onChange: (option: OptionWithValue) => void;
- onSearch: (query: string) => Promise<Option[]>;
- selected?: Option;
+ profile: { language: string; name: string };
}
-interface State {
- loading: boolean;
- query: string;
- searchResults: Option[];
-}
-
-export default class ProfilePermissionsFormSelect extends React.PureComponent<Props, State> {
+export default class ProfilePermissionsFormSelect extends React.PureComponent<Props> {
mounted = false;
constructor(props: Props) {
super(props);
this.handleSearch = debounce(this.handleSearch, 250);
- this.state = { loading: false, query: '', searchResults: [] };
- }
-
- componentDidMount() {
- this.mounted = true;
- this.handleSearch(this.state.query);
}
- componentWillUnmount() {
- this.mounted = false;
- }
-
- handleSearch = (query: string) => {
- this.setState({ loading: true });
- this.props.onSearch(query).then(
- searchResults => {
- if (this.mounted) {
- this.setState({ loading: false, searchResults });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- }
- );
- };
-
- handleInputChange = (newQuery: string) => {
- const { query } = this.state;
- if (query !== newQuery) {
- this.setState({ query: newQuery });
- this.handleSearch(newQuery);
- }
- };
-
optionRenderer(props: OptionProps<OptionWithValue, false>) {
const { data } = props;
return (
@@ -105,44 +69,41 @@ export default class ProfilePermissionsFormSelect extends React.PureComponent<Pr
</components.Control>
);
+ handleSearch = (q: string, resolve: (options: OptionWithValue[]) => void) => {
+ const { profile } = this.props;
+ const parameters: SearchUsersGroupsParameters = {
+ language: profile.language,
+ q,
+ qualityProfile: profile.name,
+ selected: 'deselected'
+ };
+ Promise.all([searchUsers(parameters), searchGroups(parameters)])
+ .then(([usersResponse, groupsResponse]) => [...usersResponse.users, ...groupsResponse.groups])
+ .then((options: Option[]) => options.map(opt => ({ ...opt, value: getStringValue(opt) })))
+ .then(resolve)
+ .catch(() => resolve([]));
+ };
+
render() {
const noResultsText = translate('no_results');
- const { selected } = this.props;
- // create a uniq string both for users and groups
- const options = this.state.searchResults.map(r => ({ ...r, value: getStringValue(r) }));
-
- // when user input is empty the options shows only top 30 names
- // the below code add the selected user so that it appears too
- if (
- selected !== undefined &&
- options.find(o => o.value === getStringValue(selected)) === undefined
- ) {
- options.unshift({ ...selected, value: getStringValue(selected) });
- }
return (
- <Select
+ <SearchSelect
className="Select-big width-100"
autoFocus={true}
isClearable={false}
id="change-profile-permission"
inputId="change-profile-permission-input"
onChange={this.props.onChange}
- onInputChange={this.handleInputChange}
+ defaultOptions={true}
+ loadOptions={this.handleSearch}
placeholder=""
noOptionsMessage={() => noResultsText}
- isLoading={this.state.loading}
- options={options}
- isSearchable={true}
- filterOptions={identity}
components={{
Option: this.optionRenderer,
SingleValue: this.singleValueRenderer,
Control: this.controlRenderer
}}
- value={options.filter(
- o => o.value === (this.props.selected && getStringValue(this.props.selected))
- )}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx
index 0ceac094fbf..0a1943966cb 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx
@@ -19,12 +19,11 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { addGroup, addUser, searchGroups, searchUsers } from '../../../../api/quality-profiles';
+import { addGroup, addUser } from '../../../../api/quality-profiles';
import { mockGroup, mockUser } from '../../../../helpers/testMocks';
import { submit, waitAndUpdate } from '../../../../helpers/testUtils';
import { UserSelected } from '../../../../types/types';
import ProfilePermissionsForm from '../ProfilePermissionsForm';
-import ProfilePermissionsFormSelect from '../ProfilePermissionsFormSelect';
jest.mock('../../../../api/quality-profiles', () => ({
addUser: jest.fn().mockResolvedValue(null),
@@ -46,7 +45,7 @@ it('correctly adds users', async () => {
const user: UserSelected = { ...mockUser(), name: 'John doe', active: true, selected: true };
wrapper.instance().handleValueChange(user);
- expect(wrapper.find(ProfilePermissionsFormSelect).prop('selected')).toBe(user);
+ expect(wrapper.state().selected).toBe(user);
submit(wrapper.find('form'));
expect(wrapper).toMatchSnapshot();
@@ -68,7 +67,7 @@ it('correctly adds groups', async () => {
const group = mockGroup();
wrapper.instance().handleValueChange(group);
- expect(wrapper.find(ProfilePermissionsFormSelect).prop('selected')).toBe(group);
+ expect(wrapper.state().selected).toBe(group);
submit(wrapper.find('form'));
expect(wrapper).toMatchSnapshot();
@@ -84,21 +83,6 @@ it('correctly adds groups', async () => {
expect(onGroupAdd).toBeCalledWith(group);
});
-it('correctly handles search', () => {
- const wrapper = shallowRender();
- wrapper.instance().handleSearch('foo');
-
- const parameters = {
- language: PROFILE.language,
- q: 'foo',
- qualityProfile: PROFILE.name,
- selected: 'deselected'
- };
-
- expect(searchUsers).toBeCalledWith(parameters);
- expect(searchGroups).toBeCalledWith(parameters);
-});
-
function shallowRender(props: Partial<ProfilePermissionsForm['props']> = {}) {
return shallow<ProfilePermissionsForm>(
<ProfilePermissionsForm
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsFormSelect-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsFormSelect-test.tsx
index 1060121d1a2..53b417a2680 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsFormSelect-test.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsFormSelect-test.tsx
@@ -19,10 +19,12 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { searchGroups, searchUsers } from '../../../../api/quality-profiles';
import {
mockReactSelectControlProps,
mockReactSelectOptionProps
} from '../../../../helpers/mocks/react-select';
+import { mockUser } from '../../../../helpers/testMocks';
import ProfilePermissionsFormSelect from '../ProfilePermissionsFormSelect';
jest.mock('lodash', () => {
@@ -31,40 +33,28 @@ jest.mock('lodash', () => {
return lodash;
});
-it('renders', () => {
- expect(
- shallow(
- <ProfilePermissionsFormSelect
- onChange={jest.fn()}
- onSearch={jest.fn(() => Promise.resolve([]))}
- selected={{ name: 'lambda' }}
- />
- )
- ).toMatchSnapshot();
-});
+jest.mock('../../../../api/quality-profiles', () => ({
+ searchGroups: jest.fn().mockResolvedValue([]),
+ searchUsers: jest.fn().mockResolvedValue([])
+}));
-it('searches', () => {
- const onSearch = jest.fn(() => Promise.resolve([]));
- const wrapper = shallow(
- <ProfilePermissionsFormSelect
- onChange={jest.fn()}
- onSearch={onSearch}
- selected={{ name: 'lambda' }}
- />
- );
- expect(onSearch).toBeCalledWith('');
- onSearch.mockClear();
-
- wrapper.prop<Function>('onInputChange')('f');
- expect(onSearch).toBeCalled();
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
- wrapper.prop<Function>('onInputChange')('foo');
- expect(onSearch).toBeCalledWith('foo');
+it('should handle search', async () => {
+ (searchUsers as jest.Mock).mockResolvedValueOnce({ users: [mockUser()] });
+ (searchGroups as jest.Mock).mockResolvedValueOnce({ groups: [{ name: 'group1' }] });
- onSearch.mockClear();
+ const wrapper = shallowRender();
+ const query = 'Waldo';
+ const results = await new Promise(resolve => {
+ wrapper.instance().handleSearch(query, resolve);
+ });
+ expect(searchUsers).toBeCalledWith(expect.objectContaining({ q: query }));
+ expect(searchGroups).toBeCalledWith(expect.objectContaining({ q: query }));
- wrapper.prop<Function>('onInputChange')('foo');
- expect(onSearch).not.toBeCalled();
+ expect(results).toHaveLength(2);
});
it('should render option correctly', () => {
@@ -95,8 +85,7 @@ function shallowRender(overrides: Partial<ProfilePermissionsFormSelect['props']>
return shallow<ProfilePermissionsFormSelect>(
<ProfilePermissionsFormSelect
onChange={jest.fn()}
- onSearch={jest.fn(() => Promise.resolve([]))}
- selected={{ name: 'lambda' }}
+ profile={{ language: 'Java', name: 'Sonar Way' }}
{...overrides}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap
index f76052be05d..75208b639c3 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap
@@ -28,12 +28,10 @@ exports[`correctly adds groups 1`] = `
</label>
<ProfilePermissionsFormSelect
onChange={[Function]}
- onSearch={[Function]}
- selected={
+ profile={
Object {
- "id": 1,
- "membersCount": 1,
- "name": "Foo",
+ "language": "js",
+ "name": "Sonar way",
}
}
/>
@@ -88,14 +86,10 @@ exports[`correctly adds users 1`] = `
</label>
<ProfilePermissionsFormSelect
onChange={[Function]}
- onSearch={[Function]}
- selected={
+ profile={
Object {
- "active": true,
- "local": true,
- "login": "john.doe",
- "name": "John doe",
- "selected": true,
+ "language": "js",
+ "name": "Sonar way",
}
}
/>
@@ -150,7 +144,12 @@ exports[`should render correctly: default 1`] = `
</label>
<ProfilePermissionsFormSelect
onChange={[Function]}
- onSearch={[Function]}
+ profile={
+ Object {
+ "language": "js",
+ "name": "Sonar way",
+ }
+ }
/>
</div>
</div>
@@ -200,7 +199,12 @@ exports[`should render correctly: submitting 1`] = `
</label>
<ProfilePermissionsFormSelect
onChange={[Function]}
- onSearch={[Function]}
+ profile={
+ Object {
+ "language": "js",
+ "name": "Sonar way",
+ }
+ }
/>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsFormSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsFormSelect-test.tsx.snap
index 1d8007c3944..35b8f64412e 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsFormSelect-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsFormSelect-test.tsx.snap
@@ -1,7 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`renders 1`] = `
-<Select
+exports[`should render control correctly: control renderer 1`] = `
+<Control
+ className="abs-height-100 Select-control"
+/>
+`;
+
+exports[`should render correctly 1`] = `
+<SearchSelect
autoFocus={true}
className="Select-big width-100"
components={
@@ -11,38 +17,14 @@ exports[`renders 1`] = `
"SingleValue": [Function],
}
}
- filterOptions={[Function]}
+ defaultOptions={true}
id="change-profile-permission"
inputId="change-profile-permission-input"
isClearable={false}
- isLoading={true}
- isSearchable={true}
+ loadOptions={[Function]}
noOptionsMessage={[Function]}
onChange={[MockFunction]}
- onInputChange={[Function]}
- options={
- Array [
- Object {
- "name": "lambda",
- "value": "group:lambda",
- },
- ]
- }
placeholder=""
- value={
- Array [
- Object {
- "name": "lambda",
- "value": "group:lambda",
- },
- ]
- }
-/>
-`;
-
-exports[`should render control correctly: control renderer 1`] = `
-<Control
- className="abs-height-100 Select-control"
/>
`;