aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-09-25 16:00:06 +0200
committerStas Vilchik <stas.vilchik@sonarsource.com>2017-10-02 17:18:15 +0200
commit77d3e365acbfd9095efe4458c643a9e8ccdada13 (patch)
tree8b19a0ace2cb8b2ce57005b62eec498c17d72866
parent7c42d81e08599b315076118e961361a96df62323 (diff)
downloadsonarqube-77d3e365acbfd9095efe4458c643a9e8ccdada13.tar.gz
sonarqube-77d3e365acbfd9095efe4458c643a9e8ccdada13.zip
SONAR-1330 Allow only authorized actions on the Quality Profiles page
-rw-r--r--server/sonar-web/src/main/js/api/quality-profiles.ts20
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx23
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx77
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap172
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap133
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx32
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>&nbsp;</th>}
+ <th>&nbsp;</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>
);
}