]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-1330 Allow only authorized actions on the Quality Profiles page
authorStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 25 Sep 2017 14:00:06 +0000 (16:00 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 2 Oct 2017 15:18:15 +0000 (17:18 +0200)
21 files changed:
server/sonar-web/src/main/js/api/quality-profiles.ts
server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx

index 953c087579ed07dab4261371bd226c60696b2383..77bc186880951790211c0e50205c528298d020d5 100644 (file)
@@ -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: {
index 48bf597e5979752798e275fa199574ec0581b351..45bdae0cb28d6f6565c8899a5e046ba2988645cb 100644 (file)
@@ -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) {
index 9c027e6d4bcf921f2acb6208f0f4dee39f31ec7d..2323bad9cfe9980122796861167c9f66f704cef4 100644 (file)
@@ -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', () => ({
index a044618fd0e1a8de3a5ddd1579c5db71402cc928..8f8f056be4e5f31ea2acd6f96179385f1597b0ca 100644 (file)
@@ -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
     });
   }
 
index 0fb083d48f35a311c89767b429db3e5a8b078e89..164943c43c4171b91f351c31faf6bc1f5198b2c7 100644 (file)
@@ -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>
index c804775b721494952618e467e344e5ce8763ef3c..1d16a88565951abd01d3e4b7f4e53f2a7aa6761a 100644 (file)
@@ -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 (file)
index 0000000..f54dde5
--- /dev/null
@@ -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();
+});
index 1c0efc638abd71d11d540d8bcc2d4daf16fac5ed..24be8e1d635afb957ab72740db9f9f52238b35fa 100644 (file)
@@ -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 (file)
index 0000000..dae1d08
--- /dev/null
@@ -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>
+`;
index 59566996434efe6c9b7fcbae2cc4d384799d2128..524a148dadd3abbbd1aea81a8e0183929efdaf05 100644 (file)
@@ -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">
index d0091653d08d7d2e74ccdfd6089b4642c0a0224b..5b04d60fdd42ddc840415ebb4fec07c2b8ad2f1d 100644 (file)
@@ -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}
index 1e71f1bd6128bb312b25d2f473f66a4ff1fb2426..77f8a92a7f1ba1e03b68adb1751581cd12d503b1 100644 (file)
@@ -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')}
index ca2845714416840f13a32f4d343743bcf417a75a..b809abd6a6f186978bcca65dd9974fcc6951ee05 100644 (file)
@@ -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>
index b9ee6c91fac94395e53aa80d881027c7366b81c0..a036a636f62d6fb8257e7315d6e396acc6d261a8 100644 (file)
@@ -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 (file)
index 0000000..f799c8e
--- /dev/null
@@ -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();
+});
index 9b282b2e2a19e69db77e740402eb95737399380d..95a3665df089c4e4504e2329327c3c5b9cf93b07 100644 (file)
@@ -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 (file)
index 0000000..85f0f92
--- /dev/null
@@ -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>
+`;
index bbea46ca28acff71c6f29e4e8d4de840d3dde86e..a64777071e222fe6951f7f164742fa6d5c567608 100644 (file)
@@ -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;
index d80b8cc1b7eb8cb8db10b0cc868dd7d78d865afe..f46db303e10608a25118e1dced38e297e77c067a 100644 (file)
@@ -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')}
index e1d7b4d55b538629a7915110e25a4b3767c345bf..49674ef4d46209dea41f7467a50e23a1a93fd8ee 100644 (file)
@@ -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>
     );
index 4768acc094fb13f15f8afef83d537a14567a3f28..b838725a9538e62810b374e75d4bb6c2467b7b23 100644 (file)
@@ -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>
     );
   }