diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2022-04-05 18:01:14 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-04-06 20:03:00 +0000 |
commit | 86e4825c2a905c452492d5796bd6d6e112ad3748 (patch) | |
tree | 201a75390c3bd1139c38eca2dd7a23ddf296ab5d | |
parent | 701d3e7366f62ec4ade502e09219ec4859bfd941 (diff) | |
download | sonarqube-86e4825c2a905c452492d5796bd6d6e112ad3748.tar.gz sonarqube-86e4825c2a905c452492d5796bd6d6e112ad3748.zip |
SONAR-16238 Fix QP permission Select behavior
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" /> `; |