From: Stas Vilchik Date: Mon, 25 Sep 2017 13:59:36 +0000 (+0200) Subject: SONAR-1330 add widget to manage quality profile permissions X-Git-Tag: 6.6-RC1~109 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=e52aa133123efef9ae7b9099b1a620bad795e010;p=sonarqube.git SONAR-1330 add widget to manage quality profile permissions --- diff --git a/server/sonar-web/src/main/js/api/quality-profiles.ts b/server/sonar-web/src/main/js/api/quality-profiles.ts index 9ee43f3eabf..953c087579e 100644 --- a/server/sonar-web/src/main/js/api/quality-profiles.ts +++ b/server/sonar-web/src/main/js/api/quality-profiles.ts @@ -26,6 +26,8 @@ import { postJSON, RequestData } from '../helpers/request'; +import { Paging } from '../app/types'; +import throwGlobalError from '../app/utils/throwGlobalError'; export interface Profile { key: string; @@ -46,14 +48,21 @@ export interface Profile { projectCount?: number; } -export function searchQualityProfiles(data: { +export interface SearchQualityProfilesParameters { + defaults?: boolean; + language?: string; organization?: string; - projectKey?: string; -}): Promise { - return getJSON('/api/qualityprofiles/search', data).then(r => r.profiles); + project?: string; + qualityProfile?: string; } -export function getQualityProfiles(data: { +export function searchQualityProfiles( + parameters: SearchQualityProfilesParameters +): Promise { + return getJSON('/api/qualityprofiles/search', parameters).then(r => r.profiles); +} + +export function getQualityProfile(data: { compareToSonarWay?: boolean; profile: string; }): Promise { @@ -129,3 +138,66 @@ export function associateProject(profileKey: string, projectKey: string): Promis export function dissociateProject(profileKey: string, projectKey: string): Promise { return post('/api/qualityprofiles/remove_project', { profileKey, projectKey }); } + +export interface SearchUsersGroupsParameters { + language: string; + organization?: string; + qualityProfile: string; + q?: string; + selected?: 'all' | 'selected' | 'deselected'; +} + +export interface SearchUsersResponse { + users: Array<{ + avatar?: string; + login: string; + name: string; + selected?: boolean; + }>; + paging: Paging; +} + +export function searchUsers(parameters: SearchUsersGroupsParameters): Promise { + return getJSON('/api/qualityprofiles/search_users', parameters).catch(throwGlobalError); +} + +export interface SearchGroupsResponse { + groups: Array<{ name: string }>; + paging: Paging; +} + +export function searchGroups( + parameters: SearchUsersGroupsParameters +): Promise { + return getJSON('/api/qualityprofiles/search_groups', parameters).catch(throwGlobalError); +} + +export interface AddRemoveUserParameters { + language: string; + login: string; + organization?: string; + qualityProfile: string; +} + +export function addUser(parameters: AddRemoveUserParameters): Promise { + return post('/api/qualityprofiles/add_user', parameters).catch(throwGlobalError); +} + +export function removeUser(parameters: AddRemoveUserParameters): Promise { + return post('/api/qualityprofiles/remove_user', parameters).catch(throwGlobalError); +} + +export interface AddRemoveGroupParameters { + group: string; + language: string; + organization?: string; + qualityProfile: string; +} + +export function addGroup(parameters: AddRemoveGroupParameters): Promise { + return post('/api/qualityprofiles/add_group', parameters).catch(throwGlobalError); +} + +export function removeGroup(parameters: AddRemoveGroupParameters): Promise { + return post('/api/qualityprofiles/remove_group', parameters).catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js index bf2962ca4f3..12a9c2af1ba 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js @@ -19,7 +19,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import GroupIcon from './GroupIcon'; +import GroupIcon from '../../../../components/icons-components/GroupIcon'; export default class GroupHolder extends React.PureComponent { static propTypes = { diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js deleted file mode 100644 index 2e6a55fdba3..00000000000 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 React from 'react'; - -const GroupIcon = () => { - /* eslint max-len: 0 */ - return ( -
- - - -
- ); -}; - -export default GroupIcon; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx index 25c725019b1..48bf597e597 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx @@ -71,7 +71,7 @@ export default class QualityProfiles extends React.PureComponent { const organization = this.props.customOrganizations ? component.organization : undefined; Promise.all([ searchQualityProfiles({ organization }), - searchQualityProfiles({ organization, projectKey: component.key }) + searchQualityProfiles({ organization, project: component.key }) ]).then( ([allProfiles, profiles]) => { if (this.mounted) { diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx index 4a68195eba5..9c027e6d4bc 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx @@ -74,7 +74,7 @@ it('fetches profiles', () => { mount(); expect(searchQualityProfiles.mock.calls).toHaveLength(2); expect(searchQualityProfiles).toBeCalledWith({ organization: undefined }); - expect(searchQualityProfiles).toBeCalledWith({ organization: undefined, projectKey: 'foo' }); + expect(searchQualityProfiles).toBeCalledWith({ organization: undefined, project: 'foo' }); }); it('fetches profiles with organization', () => { @@ -82,7 +82,7 @@ it('fetches profiles with organization', () => { mount(); expect(searchQualityProfiles.mock.calls).toHaveLength(2); expect(searchQualityProfiles).toBeCalledWith({ organization: 'org' }); - expect(searchQualityProfiles).toBeCalledWith({ organization: 'org', projectKey: 'foo' }); + expect(searchQualityProfiles).toBeCalledWith({ organization: 'org', project: 'foo' }); }); it('changes profile', () => { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx index 27cc49278d9..9d05395586e 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx @@ -169,7 +169,7 @@ export default class ChangelogContainer extends React.PureComponent +
-
+
+
+
+ +
)}
); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx index f1408a5277c..59566996434 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx @@ -22,6 +22,7 @@ import ProfileRules from './ProfileRules'; import ProfileProjects from './ProfileProjects'; import ProfileInheritance from './ProfileInheritance'; import ProfileExporters from './ProfileExporters'; +import ProfilePermissions from './ProfilePermissions'; import { Exporter, Profile } from '../types'; interface Props { @@ -41,6 +42,13 @@ export default function ProfileDetails(props: Props) {
+ {props.canAdmin && + !props.profile.isBuiltIn && ( + + )}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx index be7f0d8b433..4c3f452af50 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx @@ -53,19 +53,22 @@ export default class ProfileExporters extends React.PureComponent { } return ( -
-
-

{translate('quality_profiles.exporters')}

-
- +
+

{translate('quality_profiles.exporters')}

+
+
    + {exportersForLanguage.map((exporter, index) => ( +
  • 0 ? 'spacer-top' : undefined}> + + {exporter.name} + +
  • + ))} +
+
); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx index 53a6333126a..1e71f1bd612 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx @@ -119,58 +119,65 @@ export default class ProfileInheritance extends React.PureComponent profile.isBuiltIn); return ( -
-
-

{translate('quality_profiles.profile_inheritance')}

- {this.props.canAdmin && - !this.props.profile.isBuiltIn && ( +
+ {this.props.canAdmin && + !this.props.profile.isBuiltIn && ( +
- )} +
+ )} + +
+

{translate('quality_profiles.profile_inheritance')}

- {!this.state.loading && ( - - - {ancestors != null && - ancestors.map((ancestor, index) => ( +
+ {this.state.loading ? ( + + ) : ( +
+ + {ancestors != null && + ancestors.map((ancestor, index) => ( + + ))} + + {this.state.profile != null && ( - ))} - - {this.state.profile != null && ( - - )} - - {this.state.children != null && - this.state.children.map(child => ( - - ))} - -
- )} + )} + + {this.state.children != null && + this.state.children.map(child => ( + + ))} + + + )} +
{this.state.formOpen && ( { + mounted: boolean; + state: State = { addUserForm: false, loading: true }; + + componentDidMount() { + this.mounted = true; + this.fetchUsersAndGroups(); + } + + componentDidUpdate(prevProps: Props) { + if ( + prevProps.organization !== this.props.organization || + prevProps.profile !== this.props.profile + ) { + this.fetchUsersAndGroups(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchUsersAndGroups() { + this.setState({ loading: true }); + const { organization, profile } = this.props; + const parameters: SearchUsersGroupsParameters = { + language: profile.language, + organization, + qualityProfile: profile.name, + selected: 'selected' + }; + Promise.all([searchUsers(parameters), searchGroups(parameters)]).then( + ([usersResponse, groupsResponse]) => { + if (this.mounted) { + this.setState({ + groups: groupsResponse.groups, + loading: false, + users: usersResponse.users + }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + } + + handleAddUserButtonClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ addUserForm: true }); + }; + + handleAddUserFormClose = () => { + if (this.mounted) { + this.setState({ addUserForm: false }); + } + }; + + handleUserAdd = (addedUser: User) => { + if (this.mounted) { + this.setState((state: State) => ({ + addUserForm: false, + users: state.users && uniqBy([...state.users, addedUser], user => user.login) + })); + } + }; + + handleUserDelete = (removedUser: User) => { + if (this.mounted) { + this.setState((state: State) => ({ + users: state.users && state.users.filter(user => user !== removedUser) + })); + } + }; + + handleGroupAdd = (addedGroup: Group) => { + if (this.mounted) { + this.setState((state: State) => ({ + addUserForm: false, + groups: state.groups && uniqBy([...state.groups, addedGroup], group => group.name) + })); + } + }; + + handleGroupDelete = (removedGroup: Group) => { + if (this.mounted) { + this.setState((state: State) => ({ + groups: state.groups && state.groups.filter(group => group !== removedGroup) + })); + } + }; + + render() { + return ( +
+

{translate('permissions.page')}

+
+

{translate('quality_profiles.default_permissions')}

+ + {this.state.loading ? ( +
+ +
+ ) : ( +
+ {this.state.users && + sortBy(this.state.users, 'name').map(user => ( + + ))} + {this.state.groups && + sortBy(this.state.groups, 'name').map(group => ( + + ))} +
+ +
+
+ )} +
+ + {this.state.addUserForm && ( + + )} +
+ ); + } +} 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 new file mode 100644 index 00000000000..fe94b38b03d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx @@ -0,0 +1,152 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 Modal from 'react-modal'; +import ProfilePermissionsFormSelect from './ProfilePermissionsFormSelect'; +import { + searchUsers, + searchGroups, + addUser, + addGroup, + SearchUsersGroupsParameters +} from '../../../api/quality-profiles'; +import { translate } from '../../../helpers/l10n'; +import { User, Group } from './ProfilePermissions'; + +interface Props { + onClose: () => void; + onGroupAdd: (group: Group) => void; + onUserAdd: (user: User) => void; + organization?: string; + profile: { language: string; name: string }; +} + +interface State { + selected?: User | Group; + submitting: boolean; +} + +export default class ProfilePermissionsForm extends React.PureComponent { + mounted: boolean; + state: State = { submitting: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + stopSubmitting = () => { + if (this.mounted) { + this.setState({ submitting: false }); + } + }; + + handleUserAdd = (user: User) => + addUser({ + language: this.props.profile.language, + login: user.login, + organization: this.props.organization, + qualityProfile: this.props.profile.name + }).then(() => this.props.onUserAdd(user), this.stopSubmitting); + + handleGroupAdd = (group: Group) => + addGroup({ + group: group.name, + language: this.props.profile.language, + organization: this.props.organization, + qualityProfile: this.props.profile.name + }).then(() => this.props.onGroupAdd(group), this.stopSubmitting); + + handleFormSubmit = (event: React.SyntheticEvent) => { + event.preventDefault(); + const { selected } = this.state; + if (selected) { + this.setState({ submitting: true }); + if ((selected as User).login != undefined) { + this.handleUserAdd(selected as User); + } else { + this.handleGroupAdd(selected as Group); + } + } + }; + + handleSearch = (q: string) => { + const { organization, profile } = this.props; + const parameters: SearchUsersGroupsParameters = { + language: profile.language, + organization, + q, + qualityProfile: profile.name, + selected: 'deselected' + }; + return Promise.all([ + searchUsers(parameters), + searchGroups(parameters) + ]).then(([usersResponse, groupsResponse]) => [ + ...usersResponse.users, + ...groupsResponse.groups + ]); + }; + + handleValueChange = (selected: User | Group) => { + this.setState({ selected }); + }; + + render() { + const header = translate('quality_profiles.grant_permissions_to_user_or_group'); + const submitDisabled = !this.state.selected || this.state.submitting; + return ( + +
+

{header}

+
+
+
+
+ + +
+
+
+ {this.state.submitting && } + + +
+ +
+ ); + } +} 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 new file mode 100644 index 00000000000..e265206a49f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx @@ -0,0 +1,136 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 * as Select from 'react-select'; +import { debounce, identity } from 'lodash'; +import { User, Group } from './ProfilePermissions'; +import Avatar from '../../../components/ui/Avatar'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import GroupIcon from '../../../components/icons-components/GroupIcon'; + +type Option = User | Group; +type OptionWithValue = Option & { value: string }; + +interface Props { + onChange: (option: OptionWithValue) => void; + onSearch: (query: string) => Promise; + selected?: Option; +} + +interface State { + loading: boolean; + query: string; + searchResults: Option[]; +} + +export default class ProfilePermissionsFormSelect extends React.PureComponent { + mounted: boolean; + + 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 = (query: string) => { + this.setState({ query }); + if (query.length > 1) { + this.handleSearch(query); + } + }; + + render() { + const noResultsText = + this.state.query.length === 1 + ? translateWithParameters('select2.tooShort', 2) + : translate('no_results'); + + // create a uniq string both for users and groups + const options = this.state.searchResults.map(r => ({ ...r, value: getStringValue(r) })); + + return ( + +`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsGroup-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsGroup-test.tsx.snap new file mode 100644 index 00000000000..b72e86d9dd7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsGroup-test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +
+ + + + +
+ + lambda + +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsUser-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsUser-test.tsx.snap new file mode 100644 index 00000000000..f606faf8ad1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsUser-test.tsx.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +
+ + + + +
+ + Luke Skywalker + +
+ luke +
+
+
+`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap index 22be4264e2a..315e117ffba 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap @@ -2,7 +2,7 @@ exports[`should render the quality profiles rules with sonarway comparison 1`] = `
-p.activeDeprecatedRuleCount); return ( -
+
{translate('quality_profiles.deprecated_rules')}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx index 0d526ee0063..03a9aed243e 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx @@ -103,7 +103,7 @@ export default class EvolutionRules extends React.PureComponent { ); return ( -
+
{translate('quality_profiles.latest_new_rules')}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx index eaf2ab86589..3ef0e4167e9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx @@ -39,7 +39,7 @@ export default function EvolutionStagnant(props: Props) { } return ( -
+
{translate('quality_profiles.stagnant_profiles')}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx index bdc506df3a9..e1d7b4d55b5 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx @@ -101,7 +101,7 @@ export default class ProfilesList extends React.PureComponent { )} {languagesToShow.map(languageKey => ( -
+
{profilesToShow[languageKey] != null && this.renderHeader(languageKey, profilesToShow[languageKey].length)} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/styles.css b/server/sonar-web/src/main/js/apps/quality-profiles/styles.css index 9320f748773..84993d5134f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/styles.css +++ b/server/sonar-web/src/main/js/apps/quality-profiles/styles.css @@ -1,14 +1,5 @@ -.quality-profile-box { - padding: 20px; - border: 1px solid #e6e6e6; - border-radius: 2px; - background-color: #fff; -} - .quality-profiles-table { - margin-top: 20px; - padding-top: 10px; - padding-bottom: 10px; + padding-top: 7px; } .quality-profiles-table-name { @@ -26,6 +17,7 @@ .quality-profiles-list-header { line-height: 24px; + margin-bottom: 20px; padding: 5px 10px; border-bottom: 1px solid #e6e6e6; } @@ -46,68 +38,23 @@ margin-left: 20px; } -.quality-profile-rules, -.quality-profile-projects, -.quality-profile-inheritance, -.quality-profile-evolution { - border: 1px solid #e6e6e6; - border-radius: 2px; - background-color: #fff; -} - -.quality-profile-evolution { - padding: 20px; -} - -.quality-profile-projects, -.quality-profile-inheritance { - padding: 15px 20px 20px; -} - -.quality-profile-rules { - min-height: 182px; -} - -.quality-profile-rules > header { - padding: 15px 20px; -} - .quality-profile-rules-distribution { - margin-bottom: 20px; - padding: 5px 20px 0; + margin-bottom: 15px; + padding: 7px 20px 0; } .quality-profile-rules-deprecated { + margin-top: 20px; padding: 15px 20px; background-color: #f2dede; } .quality-profile-rules-sonarway-missing { + margin-top: 20px; padding: 15px 20px; background-color: #fcf8e3; } -.quality-profile-exporters { - margin-top: 20px; -} - -.quality-profile-evolution { - display: flex; - margin-top: 20px; -} - -.quality-profile-evolution > div { - width: 50%; - text-align: center; -} - -.quality-profile-projects { - margin-top: 20px; -} - -.quality-profile-inheritance { -} - .quality-profile-not-found { padding-top: 100px; text-align: center; @@ -118,22 +65,15 @@ } .quality-profiles-evolution-deprecated { - margin-bottom: 20px; border-color: #ebccd1; background-color: #f2dede; } .quality-profiles-evolution-stagnant { - margin-bottom: 20px; border-color: #faebcc; background-color: #fcf8e3; } -.quality-profiles-evolution-rules { - border: 1px solid #e6e6e6; - background-color: #fff; -} - .quality-profile-comparison-table { table-layout: fixed; } diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchOption.js b/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchOption.js index f1f0dfe3014..46f0794ddac 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchOption.js +++ b/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchOption.js @@ -33,7 +33,7 @@ type Props = { }; */ -const AVATAR_SIZE /*: number */ = 20; +const AVATAR_SIZE /*: number */ = 16; export default class UsersSelectSearchOption extends React.PureComponent { /*:: props: Props; */ diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js b/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js index 70643cb34a8..a420dad822c 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js +++ b/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js @@ -29,7 +29,7 @@ type Props = { }; */ -const AVATAR_SIZE /*: number */ = 20; +const AVATAR_SIZE /*: number */ = 16; export default class UsersSelectSearchValue extends React.PureComponent { /*:: props: Props; */ diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap index 56c7a055882..b3e752014e7 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap @@ -10,7 +10,7 @@ exports[`should render correctly with email instead of hash 1`] = ` ) => void; + onSubmitClick: (event: React.SyntheticEvent) => void; + submitting: boolean; +} + +interface Props { + children: (props: ChildrenProps) => React.ReactNode; + header: string; + onClose: () => void; + onSubmit: () => void | Promise; +} + +interface State { + submitting: boolean; +} + +export default class SimpleModal extends React.PureComponent { + mounted: boolean; + state: State = { submitting: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + stopSubmitting = () => { + if (this.mounted) { + this.setState({ submitting: false }); + } + }; + + handleCloseClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onClose(); + }; + + handleSubmitClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + const result = this.props.onSubmit(); + if (result) { + this.setState({ submitting: true }); + result.then(this.stopSubmitting, this.stopSubmitting); + } + }; + + render() { + return ( + + {this.props.children({ + onCloseClick: this.handleCloseClick, + onSubmitClick: this.handleSubmitClick, + submitting: this.state.submitting + })} + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/SimpleModal-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/SimpleModal-test.tsx new file mode 100644 index 00000000000..42c3c9989c2 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/SimpleModal-test.tsx @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 { shallow } from 'enzyme'; +import SimpleModal, { ChildrenProps } from '../SimpleModal'; +import { click } from '../../../helpers/testUtils'; + +it('renders', () => { + const inner = () =>
; + expect( + shallow( + + {inner} + + ) + ).toMatchSnapshot(); +}); + +it('closes', () => { + const onClose = jest.fn(); + const inner = ({ onCloseClick }: ChildrenProps) => ; + const wrapper = shallow( + + {inner} + + ); + click(wrapper.find('button')); + expect(onClose).toBeCalled(); +}); + +it('submits', async () => { + const onSubmit = jest.fn(() => Promise.resolve()); + const inner = ({ onSubmitClick, submitting }: ChildrenProps) => ( + + ); + const wrapper = shallow( + + {inner} + + ); + (wrapper.instance() as SimpleModal).mounted = true; + expect(wrapper).toMatchSnapshot(); + + click(wrapper.find('button')); + expect(onSubmit).toBeCalled(); + expect(wrapper).toMatchSnapshot(); + + await new Promise(setImmediate); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SimpleModal-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SimpleModal-test.tsx.snap new file mode 100644 index 00000000000..4778ef47dbc --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SimpleModal-test.tsx.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + +
+ +`; + +exports[`submits 1`] = ` + + + +`; + +exports[`submits 2`] = ` + + + +`; + +exports[`submits 3`] = ` + + + +`; diff --git a/server/sonar-web/src/main/js/components/icons-components/GroupIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/GroupIcon.tsx new file mode 100644 index 00000000000..cb971f1c4d9 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/GroupIcon.tsx @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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'; + +interface Props { + className?: string; + fill?: string; + size?: number; +} + +export default function GroupIcon({ className, fill = '#aaa', size = 36 }: Props) { + return ( + + + + + + ); +} diff --git a/server/sonar-web/src/main/less/components/react-select.less b/server/sonar-web/src/main/less/components/react-select.less index 33649781706..c3d9e98a579 100644 --- a/server/sonar-web/src/main/less/components/react-select.less +++ b/server/sonar-web/src/main/less/components/react-select.less @@ -370,14 +370,17 @@ .Select-big .Select-value-label { display: inline-block; - margin-top: 5px; + margin-top: 7px; + line-height: 16px; } .Select-big .Select-option { - padding: 4px 8px; + padding: 7px 8px; + line-height: 16px; } -.Select-big img { +.Select-big img, +.Select-big svg { padding-top: 0; } diff --git a/server/sonar-web/tsconfig.json b/server/sonar-web/tsconfig.json index 427192d12e7..8186016873d 100644 --- a/server/sonar-web/tsconfig.json +++ b/server/sonar-web/tsconfig.json @@ -8,7 +8,9 @@ "strict": true, "target": "es5", "jsx": "react", - "lib": ["es2017", "dom"], + // remove "es2015.promise", "es2015.iterable" when upgrading to TypeScript 2.5 + // see https://github.com/Microsoft/TypeScript/issues/16017 + "lib": ["es2015.promise", "es2015.iterable", "es2017", "dom"], "module": "esnext", "moduleResolution": "node", "typeRoots": ["./src/main/js/typings", "./node_modules/@types"] 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 6ed28db4edb..973400b7713 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1573,6 +1573,11 @@ quality_profiles.built_in=Built-in quality_profiles.built_in.description.1=This quality profile is provided by a plugin. quality_profiles.built_in.description.2=It will automatically be updated when a new version of the supplying plugin changes its definition. quality_profiles.extends_built_in=Because it inherits from a built-in quality profile, this quality profile can be automatically updated when a new version of the corresponding plugin is deployed. +quality_profiles.default_permissions=Users with the global "Manage Quality Profile" permission can manage this quality profile. +quality_profiles.grant_permissions_to_more_users=Grant permissions to more users +quality_profiles.grant_permissions_to_user_or_group=Grant permissions to a user or a group +quality_profiles.additional_user_groups=Additional users / groups: +quality_profiles.search_description=Search users by login or name, and groups by name @@ -1911,15 +1916,19 @@ login.login_with_x=Log in with {0} #------------------------------------------------------------------------------ # -# USERS PAGE +# USERS & GROUPS PAGE # #------------------------------------------------------------------------------ users.add=Add user users.remove=Remove user +users.remove.confirmation=Are you sure you want to remove user "{user}"? users.search_description=Search users by login or name users.update=Update users users.update_details=Update details +groups.remove=Remove group +groups.remove.confirmation=Are you sure you want to remove group "{user}"? + #------------------------------------------------------------------------------ # # MY PROFILE & MY ACCOUNT