diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-09-25 16:00:06 +0200 |
---|---|---|
committer | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-10-02 17:18:15 +0200 |
commit | 77d3e365acbfd9095efe4458c643a9e8ccdada13 (patch) | |
tree | 8b19a0ace2cb8b2ce57005b62eec498c17d72866 | |
parent | 7c42d81e08599b315076118e961361a96df62323 (diff) | |
download | sonarqube-77d3e365acbfd9095efe4458c643a9e8ccdada13.tar.gz sonarqube-77d3e365acbfd9095efe4458c643a9e8ccdada13.zip |
SONAR-1330 Allow only authorized actions on the Quality Profiles page
21 files changed, 517 insertions, 92 deletions
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 953c087579e..77bc1868809 100644 --- a/server/sonar-web/src/main/js/api/quality-profiles.ts +++ b/server/sonar-web/src/main/js/api/quality-profiles.ts @@ -29,7 +29,18 @@ import { import { Paging } from '../app/types'; import throwGlobalError from '../app/utils/throwGlobalError'; +export interface ProfileActions { + copy?: boolean; + edit?: boolean; + setAsDefault?: boolean; +} + +export interface Actions { + create?: boolean; +} + export interface Profile { + actions?: ProfileActions; key: string; name: string; language: string; @@ -56,10 +67,15 @@ export interface SearchQualityProfilesParameters { qualityProfile?: string; } +export interface SearchQualityProfilesResponse { + actions?: Actions; + profiles: Profile[]; +} + export function searchQualityProfiles( parameters: SearchQualityProfilesParameters -): Promise<Profile[]> { - return getJSON('/api/qualityprofiles/search', parameters).then(r => r.profiles); +): Promise<SearchQualityProfilesResponse> { + return getJSON('/api/qualityprofiles/search', parameters); } export function getQualityProfile(data: { 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 48bf597e597..45bdae0cb28 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx @@ -70,8 +70,8 @@ export default class QualityProfiles extends React.PureComponent<Props, State> { const { component } = this.props; const organization = this.props.customOrganizations ? component.organization : undefined; Promise.all([ - searchQualityProfiles({ organization }), - searchQualityProfiles({ organization, project: component.key }) + searchQualityProfiles({ organization }).then(r => r.profiles), + searchQualityProfiles({ organization, project: component.key }).then(r => r.profiles) ]).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 9c027e6d4bc..2323bad9cfe 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 @@ -20,7 +20,7 @@ jest.mock('../../../api/quality-profiles', () => ({ associateProject: jest.fn(() => Promise.resolve()), dissociateProject: jest.fn(() => Promise.resolve()), - searchQualityProfiles: jest.fn() + searchQualityProfiles: jest.fn(() => Promise.resolve()) })); jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx index a044618fd0e..8f8f056be4e 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { searchQualityProfiles, getExporters } from '../../../api/quality-profiles'; +import { searchQualityProfiles, getExporters, Actions } from '../../../api/quality-profiles'; import { sortProfiles } from '../utils'; import { translate } from '../../../helpers/l10n'; import OrganizationHelmet from '../../../components/common/OrganizationHelmet'; @@ -27,13 +27,13 @@ import { Exporter, Profile } from '../types'; interface Props { children: React.ReactElement<any>; - currentUser: { permissions: { global: Array<string> } }; languages: Array<{}>; onRequestFail: (reasong: any) => void; - organization: { name: string; canAdmin?: boolean; key: string } | null; + organization: { name: string; key: string } | null; } interface State { + actions?: Actions; loading: boolean; exporters?: Exporter[]; profiles?: Profile[]; @@ -73,10 +73,11 @@ export default class App extends React.PureComponent<Props, State> { this.setState({ loading: true }); Promise.all([getExporters(), this.fetchProfiles()]).then(responses => { if (this.mounted) { - const [exporters, profiles] = responses; + const [exporters, profilesResponse] = responses; this.setState({ + actions: profilesResponse.actions, exporters, - profiles: sortProfiles(profiles), + profiles: sortProfiles(profilesResponse.profiles), loading: false }); } @@ -84,9 +85,9 @@ export default class App extends React.PureComponent<Props, State> { } updateProfiles = () => { - return this.fetchProfiles().then((profiles: any) => { + return this.fetchProfiles().then(r => { if (this.mounted) { - this.setState({ profiles: sortProfiles(profiles) }); + this.setState({ profiles: sortProfiles(r.profiles) }); } }); }; @@ -98,18 +99,14 @@ export default class App extends React.PureComponent<Props, State> { const { organization } = this.props; const finalLanguages = Object.values(this.props.languages); - const canAdmin = organization - ? organization.canAdmin - : this.props.currentUser.permissions.global.includes('profileadmin'); - return React.cloneElement(this.props.children, { + actions: this.state.actions || {}, profiles: this.state.profiles, languages: finalLanguages, exporters: this.state.exporters, updateProfiles: this.updateProfiles, onRequestFail: this.props.onRequestFail, - organization: organization ? organization.key : null, - canAdmin + organization: organization ? organization.key : null }); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index 0fb083d48f3..164943c43c4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx @@ -30,7 +30,6 @@ import { getProfilePath, getProfileComparePath, getProfilesPath } from '../utils import { Profile } from '../types'; interface Props { - canAdmin: boolean; fromList?: boolean; onRequestFail: (reasong: any) => void; organization: string | null; @@ -45,10 +44,6 @@ interface State { } export default class ProfileActions extends React.PureComponent<Props, State> { - static defaultProps = { - fromList: false - }; - static contextTypes = { router: PropTypes.object }; @@ -119,7 +114,8 @@ export default class ProfileActions extends React.PureComponent<Props, State> { }; render() { - const { profile, canAdmin } = this.props; + const { profile } = this.props; + const { actions = {} } = profile; // FIXME use org, name and lang const backupUrl = @@ -137,7 +133,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> { return ( <ul className="dropdown-menu dropdown-menu-right"> - {canAdmin && + {actions.edit && !profile.isBuiltIn && ( <li> <Link to={activateMoreUrl}>{translate('quality_profiles.activate_more_rules')}</Link> @@ -157,14 +153,14 @@ export default class ProfileActions extends React.PureComponent<Props, State> { {translate('compare')} </Link> </li> - {canAdmin && ( + {actions.copy && ( <li> <a id="quality-profile-copy" href="#" onClick={this.handleCopyClick}> {translate('copy')} </a> </li> )} - {canAdmin && + {actions.edit && !profile.isBuiltIn && ( <li> <a id="quality-profile-rename" href="#" onClick={this.handleRenameClick}> @@ -172,7 +168,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> { </a> </li> )} - {canAdmin && + {actions.setAsDefault && !profile.isDefault && ( <li> <a id="quality-profile-set-as-default" href="#" onClick={this.handleSetDefaultClick}> @@ -180,7 +176,7 @@ export default class ProfileActions extends React.PureComponent<Props, State> { </a> </li> )} - {canAdmin && + {actions.edit && !profile.isDefault && !profile.isBuiltIn && ( <li> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx index c804775b721..1d16a885659 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx @@ -24,7 +24,6 @@ import ProfileHeader from '../details/ProfileHeader'; import { Profile } from '../types'; interface Props { - canAdmin: boolean; children: React.ReactElement<any>; location: { pathname: string; @@ -87,7 +86,6 @@ export default class ProfileContainer extends React.PureComponent<Props> { <div id="quality-profile"> <Helmet title={profile.name} /> <ProfileHeader - canAdmin={this.props.canAdmin} onRequestFail={this.props.onRequestFail} organization={organization} profile={profile} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx new file mode 100644 index 00000000000..f54dde5ed08 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx @@ -0,0 +1,77 @@ +/* + * 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 ProfileActions from '../ProfileActions'; + +const PROFILE = { + activeRuleCount: 68, + activeDeprecatedRuleCount: 0, + childrenCount: 0, + depth: 0, + isBuiltIn: false, + isDefault: false, + isInherited: false, + key: 'foo', + language: 'java', + languageName: 'Java', + name: 'Foo', + organization: 'org', + rulesUpdatedAt: '2017-06-28T12:58:44+0000' +}; + +it('renders with no permissions', () => { + expect( + shallow( + <ProfileActions + onRequestFail={jest.fn()} + organization="org" + profile={PROFILE} + updateProfiles={jest.fn()} + /> + ) + ).toMatchSnapshot(); +}); + +it('renders with permission to edit', () => { + expect( + shallow( + <ProfileActions + onRequestFail={jest.fn()} + organization="org" + profile={{ ...PROFILE, actions: { edit: true } }} + updateProfiles={jest.fn()} + /> + ) + ).toMatchSnapshot(); +}); + +it('renders with all permissions', () => { + expect( + shallow( + <ProfileActions + onRequestFail={jest.fn()} + organization="org" + profile={{ ...PROFILE, actions: { copy: true, edit: true, setAsDefault: true } }} + updateProfiles={jest.fn()} + /> + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx index 1c0efc638ab..24be8e1d635 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx @@ -31,7 +31,6 @@ it('should render ProfileHeader', () => { const updateProfiles = jest.fn(); const output = shallow( <ProfileContainer - canAdmin={false} location={{ pathname: '', query: { language: 'js', name: 'fake' } }} onRequestFail={jest.fn()} organization={null} @@ -44,7 +43,6 @@ it('should render ProfileHeader', () => { const header = output.find(ProfileHeader); expect(header.length).toBe(1); expect(header.prop('profile')).toBe(targetProfile); - expect(header.prop('canAdmin')).toBe(false); expect(header.prop('updateProfiles')).toBe(updateProfiles); }); @@ -55,7 +53,6 @@ it('should render ProfileNotFound', () => { ]; const output = shallow( <ProfileContainer - canAdmin={false} location={{ pathname: '', query: { language: 'js', name: 'random' } }} onRequestFail={jest.fn()} organization={null} @@ -73,7 +70,6 @@ it('should render Helmet', () => { const updateProfiles = jest.fn(); const output = shallow( <ProfileContainer - canAdmin={false} location={{ pathname: '', query: { language: 'js', name: 'First Profile' } }} onRequestFail={jest.fn()} organization={null} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap new file mode 100644 index 00000000000..dae1d0887fc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap @@ -0,0 +1,172 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders with all permissions 1`] = ` +<ul + className="dropdown-menu dropdown-menu-right" +> + <li> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/organizations/org/rules#qprofile=foo|activation=false" + > + quality_profiles.activate_more_rules + </Link> + </li> + <li> + <a + href="/api/qualityprofiles/backup?profileKey=foo" + id="quality-profile-backup" + > + backup_verb + </a> + </li> + <li> + <Link + id="quality-profile-compare" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/organizations/org/quality_profiles/compare", + "query": Object { + "language": "java", + "name": "Foo", + }, + } + } + > + compare + </Link> + </li> + <li> + <a + href="#" + id="quality-profile-copy" + onClick={[Function]} + > + copy + </a> + </li> + <li> + <a + href="#" + id="quality-profile-rename" + onClick={[Function]} + > + rename + </a> + </li> + <li> + <a + href="#" + id="quality-profile-set-as-default" + onClick={[Function]} + > + set_as_default + </a> + </li> + <li> + <a + href="#" + id="quality-profile-delete" + onClick={[Function]} + > + delete + </a> + </li> +</ul> +`; + +exports[`renders with no permissions 1`] = ` +<ul + className="dropdown-menu dropdown-menu-right" +> + <li> + <a + href="/api/qualityprofiles/backup?profileKey=foo" + id="quality-profile-backup" + > + backup_verb + </a> + </li> + <li> + <Link + id="quality-profile-compare" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/organizations/org/quality_profiles/compare", + "query": Object { + "language": "java", + "name": "Foo", + }, + } + } + > + compare + </Link> + </li> +</ul> +`; + +exports[`renders with permission to edit 1`] = ` +<ul + className="dropdown-menu dropdown-menu-right" +> + <li> + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/organizations/org/rules#qprofile=foo|activation=false" + > + quality_profiles.activate_more_rules + </Link> + </li> + <li> + <a + href="/api/qualityprofiles/backup?profileKey=foo" + id="quality-profile-backup" + > + backup_verb + </a> + </li> + <li> + <Link + id="quality-profile-compare" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/organizations/org/quality_profiles/compare", + "query": Object { + "language": "java", + "name": "Foo", + }, + } + } + > + compare + </Link> + </li> + <li> + <a + href="#" + id="quality-profile-rename" + onClick={[Function]} + > + rename + </a> + </li> + <li> + <a + href="#" + id="quality-profile-delete" + onClick={[Function]} + > + delete + </a> + </li> +</ul> +`; 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 59566996434..524a148dadd 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 @@ -26,7 +26,6 @@ import ProfilePermissions from './ProfilePermissions'; import { Exporter, Profile } from '../types'; interface Props { - canAdmin: boolean; exporters: Exporter[]; onRequestFail: (reasong: any) => void; organization: string | null; @@ -36,18 +35,17 @@ interface Props { } export default function ProfileDetails(props: Props) { + const { profile } = props; return ( <div> <div className="quality-profile-grid"> <div className="quality-profile-grid-left"> <ProfileRules {...props} /> <ProfileExporters {...props} /> - {props.canAdmin && - !props.profile.isBuiltIn && ( - <ProfilePermissions - organization={props.organization || undefined} - profile={props.profile} - /> + {profile.actions && + profile.actions.edit && + !profile.isBuiltIn && ( + <ProfilePermissions organization={props.organization || undefined} profile={profile} /> )} </div> <div className="quality-profile-grid-right"> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx index d0091653d08..5b04d60fdd4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx @@ -33,7 +33,6 @@ import { import { Profile } from '../types'; interface Props { - canAdmin: boolean; onRequestFail: (reasong: any) => void; profile: Profile; organization: string | null; @@ -113,7 +112,6 @@ export default class ProfileHeader extends React.PureComponent<Props> { {translate('actions')} <i className="icon-dropdown" /> </button> <ProfileActions - canAdmin={this.props.canAdmin} onRequestFail={this.props.onRequestFail} organization={organization} profile={profile} 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 1e71f1bd612..77f8a92a7f1 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 @@ -26,7 +26,6 @@ import { getProfileInheritance } from '../../../api/quality-profiles'; import { Profile } from '../types'; interface Props { - canAdmin: boolean; onRequestFail: (reason: any) => void; organization: string | null; profile: Profile; @@ -120,8 +119,9 @@ export default class ProfileInheritance extends React.PureComponent<Props, State return ( <div className="boxed-group quality-profile-inheritance"> - {this.props.canAdmin && - !this.props.profile.isBuiltIn && ( + {profile.actions && + profile.actions.edit && + !profile.isBuiltIn && ( <div className="boxed-group-actions"> <button className="pull-right js-change-parent" onClick={this.handleChangeParentClick}> {translate('quality_profiles.change_parent')} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx index ca284571441..b809abd6a6f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx @@ -26,7 +26,6 @@ import { translate } from '../../../helpers/l10n'; import { Profile } from '../types'; interface Props { - canAdmin: boolean; organization: string | null; profile: Profile; updateProfiles: () => Promise<void>; @@ -127,10 +126,12 @@ export default class ProfileProjects extends React.PureComponent<Props, State> { } render() { + const { profile } = this.props; return ( <div className="boxed-group quality-profile-projects"> - {this.props.canAdmin && - !this.props.profile.isDefault && ( + {profile.actions && + profile.actions.edit && + !profile.isDefault && ( <div className="boxed-group-actions"> <button className="js-change-projects" onClick={this.handleChangeClick}> {translate('quality_profiles.change_projects')} @@ -145,7 +146,7 @@ export default class ProfileProjects extends React.PureComponent<Props, State> { <div className="boxed-group-inner"> {this.state.loading ? ( <i className="spinner" /> - ) : this.props.profile.isDefault ? ( + ) : profile.isDefault ? ( this.renderDefault() ) : ( this.renderProjects() @@ -156,7 +157,7 @@ export default class ProfileProjects extends React.PureComponent<Props, State> { <ChangeProjectsForm onClose={this.closeForm} organization={this.props.organization} - profile={this.props.profile} + profile={profile} /> )} </div> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx index b9ee6c91fac..a036a636f62 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx @@ -33,7 +33,6 @@ import { Profile } from '../types'; const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; interface Props { - canAdmin: boolean; organization: string | null; profile: Profile; } @@ -181,7 +180,8 @@ export default class ProfileRules extends React.PureComponent<Props, State> { </tbody> </table> - {this.props.canAdmin && + {profile.actions && + profile.actions.edit && !profile.isBuiltIn && ( <div className="text-right big-spacer-top"> <Link to={activateMoreUrl} className="button js-activate-rules"> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx new file mode 100644 index 00000000000..f799c8e3f28 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx @@ -0,0 +1,53 @@ +/* + * 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 ProfileDetails from '../ProfileDetails'; +import { Profile } from '../../types'; + +it('renders without permissions', () => { + expect( + shallow( + <ProfileDetails + exporters={[]} + onRequestFail={jest.fn()} + organization="org" + profile={{} as Profile} + profiles={[]} + updateProfiles={jest.fn()} + /> + ) + ).toMatchSnapshot(); +}); + +it('renders with edit permission', () => { + expect( + shallow( + <ProfileDetails + exporters={[]} + onRequestFail={jest.fn()} + organization="org" + profile={{ actions: { edit: true } } as Profile} + profiles={[]} + updateProfiles={jest.fn()} + /> + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx index 9b282b2e2a1..95a3665df08 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx @@ -40,6 +40,8 @@ const PROFILE = { rulesUpdatedAt: '2017-06-28T12:58:44+0000' }; +const EDITABLE_PROFILE = { ...PROFILE, actions: { edit: true } }; + const apiResponseAll = { total: 243, facets: [ @@ -81,7 +83,7 @@ const apiResponseActive = { }); it('should render the quality profiles rules with sonarway comparison', () => { - const wrapper = shallow(<ProfileRules canAdmin={false} organization="foo" profile={PROFILE} />); + const wrapper = shallow(<ProfileRules organization="foo" profile={PROFILE} />); const instance = wrapper.instance() as any; instance.mounted = true; instance.loadRules(); @@ -93,16 +95,15 @@ it('should render the quality profiles rules with sonarway comparison', () => { }); it('should show a button to activate more rules for admins', () => { - const wrapper = shallow(<ProfileRules canAdmin={true} organization="foo" profile={PROFILE} />); + const wrapper = shallow(<ProfileRules organization="foo" profile={EDITABLE_PROFILE} />); expect(wrapper.find('.js-activate-rules')).toMatchSnapshot(); }); it('should show a deprecated rules warning message', () => { const wrapper = shallow( <ProfileRules - canAdmin={true} organization="foo" - profile={{ ...PROFILE, activeDeprecatedRuleCount: 8 }} + profile={{ ...EDITABLE_PROFILE, activeDeprecatedRuleCount: 8 }} /> ); expect(wrapper.find('ProfileRulesDeprecatedWarning')).toMatchSnapshot(); @@ -110,14 +111,7 @@ it('should show a deprecated rules warning message', () => { it('should not show a button to activate more rules on built in profiles', () => { const wrapper = shallow( - <ProfileRules canAdmin={true} organization={null} profile={{ ...PROFILE, isBuiltIn: true }} /> - ); - expect(wrapper.find('.js-activate-rules')).toHaveLength(0); -}); - -it('should not show a button to activate more rules on built in profiles', () => { - const wrapper = shallow( - <ProfileRules canAdmin={true} organization={null} profile={{ ...PROFILE, isBuiltIn: true }} /> + <ProfileRules organization={null} profile={{ ...EDITABLE_PROFILE, isBuiltIn: true }} /> ); expect(wrapper.find('.js-activate-rules')).toHaveLength(0); }); @@ -125,7 +119,7 @@ it('should not show a button to activate more rules on built in profiles', () => it('should not show sonarway comparison for built in profiles', () => { (apiQP as any).getQualityProfile = jest.fn(() => Promise.resolve()); const wrapper = shallow( - <ProfileRules canAdmin={true} organization={null} profile={{ ...PROFILE, isBuiltIn: true }} /> + <ProfileRules organization={null} profile={{ ...PROFILE, isBuiltIn: true }} /> ); const instance = wrapper.instance() as any; instance.mounted = true; @@ -147,7 +141,7 @@ it('should not show sonarway comparison if there is no missing rules', () => { } }) ); - const wrapper = shallow(<ProfileRules canAdmin={true} organization={null} profile={PROFILE} />); + const wrapper = shallow(<ProfileRules organization={null} profile={PROFILE} />); const instance = wrapper.instance() as any; instance.mounted = true; instance.loadRules(); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap new file mode 100644 index 00000000000..85f0f92c7a5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders with edit permission 1`] = ` +<div> + <div + className="quality-profile-grid" + > + <div + className="quality-profile-grid-left" + > + <ProfileRules + exporters={Array []} + onRequestFail={[Function]} + organization="org" + profile={ + Object { + "actions": Object { + "edit": true, + }, + } + } + profiles={Array []} + updateProfiles={[Function]} + /> + <ProfileExporters + exporters={Array []} + onRequestFail={[Function]} + organization="org" + profile={ + Object { + "actions": Object { + "edit": true, + }, + } + } + profiles={Array []} + updateProfiles={[Function]} + /> + <ProfilePermissions + organization="org" + profile={ + Object { + "actions": Object { + "edit": true, + }, + } + } + /> + </div> + <div + className="quality-profile-grid-right" + > + <ProfileInheritance + exporters={Array []} + onRequestFail={[Function]} + organization="org" + profile={ + Object { + "actions": Object { + "edit": true, + }, + } + } + profiles={Array []} + updateProfiles={[Function]} + /> + <ProfileProjects + exporters={Array []} + onRequestFail={[Function]} + organization="org" + profile={ + Object { + "actions": Object { + "edit": true, + }, + } + } + profiles={Array []} + updateProfiles={[Function]} + /> + </div> + </div> +</div> +`; + +exports[`renders without permissions 1`] = ` +<div> + <div + className="quality-profile-grid" + > + <div + className="quality-profile-grid-left" + > + <ProfileRules + exporters={Array []} + onRequestFail={[Function]} + organization="org" + profile={Object {}} + profiles={Array []} + updateProfiles={[Function]} + /> + <ProfileExporters + exporters={Array []} + onRequestFail={[Function]} + organization="org" + profile={Object {}} + profiles={Array []} + updateProfiles={[Function]} + /> + </div> + <div + className="quality-profile-grid-right" + > + <ProfileInheritance + exporters={Array []} + onRequestFail={[Function]} + organization="org" + profile={Object {}} + profiles={Array []} + updateProfiles={[Function]} + /> + <ProfileProjects + exporters={Array []} + onRequestFail={[Function]} + organization="org" + profile={Object {}} + profiles={Array []} + updateProfiles={[Function]} + /> + </div> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx index bbea46ca28a..a64777071e2 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx @@ -22,9 +22,10 @@ import PageHeader from './PageHeader'; import Evolution from './Evolution'; import ProfilesList from './ProfilesList'; import { Profile } from '../types'; +import { Actions } from '../../../api/quality-profiles'; interface Props { - canAdmin: boolean; + actions: Actions; languages: Array<{ key: string; name: string }>; location: { query: { [p: string]: string } }; onRequestFail: (reason: any) => void; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx index d80b8cc1b7e..f46db303e10 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx @@ -24,9 +24,10 @@ import RestoreProfileForm from './RestoreProfileForm'; import { getProfilePath } from '../utils'; import { translate } from '../../../helpers/l10n'; import { Profile } from '../types'; +import { Actions } from '../../../api/quality-profiles'; interface Props { - canAdmin: boolean; + actions: Actions; languages: Array<{ key: string; name: string }>; onRequestFail: (reason: any) => void; organization: string | null; @@ -80,7 +81,7 @@ export default class PageHeader extends React.PureComponent<Props, State> { <header className="page-header"> <h1 className="page-title">{translate('quality_profiles.page')}</h1> - {this.props.canAdmin && ( + {this.props.actions.create && ( <div className="page-actions button-group dropdown"> <button id="quality-profiles-create" onClick={this.handleCreateClick}> {translate('create')} 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 e1d7b4d55b5..49674ef4d46 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 @@ -25,7 +25,6 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Profile } from '../types'; interface Props { - canAdmin: boolean; languages: Array<{ key: string; name: string }>; location: { query: { [p: string]: string } }; onRequestFail: (reason: any) => void; @@ -38,7 +37,6 @@ export default class ProfilesList extends React.PureComponent<Props> { renderProfiles(profiles: Profile[]) { return profiles.map(profile => ( <ProfilesListRow - canAdmin={this.props.canAdmin} key={profile.key} onRequestFail={this.props.onRequestFail} organization={this.props.organization} @@ -67,7 +65,7 @@ export default class ProfilesList extends React.PureComponent<Props> { <th className="text-right nowrap">{translate('quality_profiles.list.rules')}</th> <th className="text-right nowrap">{translate('quality_profiles.list.updated')}</th> <th className="text-right nowrap">{translate('quality_profiles.list.used')}</th> - {this.props.canAdmin && <th> </th>} + <th> </th> </tr> </thead> ); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx index 4768acc094f..b838725a953 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx @@ -30,7 +30,6 @@ import { Profile } from '../types'; import Tooltip from '../../../components/controls/Tooltip'; interface Props { - canAdmin: boolean; onRequestFail: (reason: any) => void; organization: string | null; profile: Profile; @@ -139,23 +138,20 @@ export default class ProfilesListRow extends React.PureComponent<Props> { <td className="quality-profiles-table-date thin nowrap text-right"> {this.renderUsageDate()} </td> - {this.props.canAdmin && ( - <td className="quality-profiles-table-actions thin nowrap text-right"> - <div className="dropdown"> - <button className="dropdown-toggle" data-toggle="dropdown"> - <i className="icon-dropdown" /> - </button> - <ProfileActions - canAdmin={this.props.canAdmin} - fromList={true} - onRequestFail={this.props.onRequestFail} - organization={this.props.organization} - profile={this.props.profile} - updateProfiles={this.props.updateProfiles} - /> - </div> - </td> - )} + <td className="quality-profiles-table-actions thin nowrap text-right"> + <div className="dropdown"> + <button className="dropdown-toggle" data-toggle="dropdown"> + <i className="icon-dropdown" /> + </button> + <ProfileActions + fromList={true} + onRequestFail={this.props.onRequestFail} + organization={this.props.organization} + profile={this.props.profile} + updateProfiles={this.props.updateProfiles} + /> + </div> + </td> </tr> ); } |