]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11007 Update buttons on rules list
authorSiegfried Ehret <siegfried.ehret@sonarsource.com>
Fri, 12 Jul 2019 12:54:03 +0000 (14:54 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 24 Jul 2019 06:49:42 +0000 (08:49 +0200)
- Make Bulk Change available only to user who can use it.
- Make «Deactivate» button available only to user who can use it.

server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChange-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChange-test.tsx.snap
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 291b64054f502dddf4512311a9294080c636d641..fbb1acafa4d0a8e553efe2df929d348da21401e9 100644 (file)
@@ -41,6 +41,8 @@ import FiltersHeader from '../../../components/common/FiltersHeader';
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
 import '../../../components/search-navigator.css';
 import { hasPrivateAccess } from '../../../helpers/organizations';
+import { isSonarCloud } from '../../../helpers/system';
+import { isLoggedIn } from '../../../helpers/users';
 import {
   getAppState,
   getCurrentUser,
@@ -522,6 +524,28 @@ export class App extends React.PureComponent<Props, State> {
 
   isFiltered = () => Object.keys(serializeQuery(this.state.query)).length > 0;
 
+  renderBulkButton = () => {
+    const { currentUser, languages } = this.props;
+    const { canWrite, paging, query, referencedProfiles } = this.state;
+    const organization = this.props.organization && this.props.organization.key;
+
+    if (!isLoggedIn(currentUser) || (isSonarCloud() && !organization) || !canWrite) {
+      return null;
+    }
+
+    return (
+      paging && (
+        <BulkChange
+          languages={languages}
+          organization={organization}
+          query={query}
+          referencedProfiles={referencedProfiles}
+          total={paging.total}
+        />
+      )
+    );
+  };
+
   render() {
     const { paging, rules } = this.state;
     const selectedIndex = this.getSelectedIndex();
@@ -531,6 +555,7 @@ export class App extends React.PureComponent<Props, State> {
       this.props.organization,
       this.props.userOrganizations
     );
+
     return (
       <>
         <Suggestions suggestions="coding_rules" />
@@ -586,15 +611,7 @@ export class App extends React.PureComponent<Props, State> {
                       {translate('coding_rules.return_to_list')}
                     </a>
                   ) : (
-                    this.state.paging && (
-                      <BulkChange
-                        languages={this.props.languages}
-                        organization={organization}
-                        query={this.state.query}
-                        referencedProfiles={this.state.referencedProfiles}
-                        total={this.state.paging.total}
-                      />
-                    )
+                    this.renderBulkButton()
                   )}
                   <PageActions
                     loading={this.state.loading}
@@ -627,6 +644,8 @@ export class App extends React.PureComponent<Props, State> {
                   {rules.map(rule => (
                     <RuleListItem
                       activation={this.getRuleActivation(rule.key)}
+                      canWrite={this.state.canWrite}
+                      isLoggedIn={isLoggedIn(this.props.currentUser)}
                       key={rule.key}
                       onActivate={this.handleRuleActivate}
                       onDeactivate={this.handleRuleDeactivate}
index 12edc48e775d6514452b17308970a0e1d5264deb..655b4aa5ad5ac850049e0cc7aeb9cd428540795c 100644 (file)
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import { Button } from 'sonar-ui-common/components/controls/buttons';
 import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
+import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { Profile } from '../../../api/quality-profiles';
 import { Query } from '../query';
@@ -79,7 +80,13 @@ export default class BulkChange extends React.PureComponent<Props, State> {
       Boolean(profile.actions && profile.actions.edit)
     );
     if (!canBulkChange) {
-      return null;
+      return (
+        <Tooltip overlay={translate('coding_rules.can_not_bulk_change')}>
+          <Button className="js-bulk-change" disabled={true}>
+            {translate('bulk_change')}
+          </Button>
+        </Tooltip>
+      );
     }
 
     const { activation } = this.props.query;
index f99010447e9e9c6998a627682c0e14e306d13956..5fdd95ec7d64f2d3b3e778bfcb30956590cab31b 100644 (file)
@@ -36,6 +36,8 @@ import SimilarRulesFilter from './SimilarRulesFilter';
 
 interface Props {
   activation?: Activation;
+  canWrite?: boolean;
+  isLoggedIn: boolean;
   onActivate: (profile: string, rule: string, activation: Activation) => void;
   onDeactivate: (profile: string, rule: string) => void;
   onFilterChange: (changes: Partial<Query>) => void;
@@ -124,13 +126,13 @@ export default class RuleListItem extends React.PureComponent<Props> {
   };
 
   renderActions = () => {
-    const { activation, rule, selectedProfile } = this.props;
-    if (!selectedProfile) {
+    const { activation, isLoggedIn, rule, selectedProfile } = this.props;
+    if (!selectedProfile || !isLoggedIn) {
       return null;
     }
 
-    const canEdit = selectedProfile.actions && selectedProfile.actions.edit;
-    if (!canEdit || selectedProfile.isBuiltIn) {
+    const canCopy = selectedProfile.actions && selectedProfile.actions.copy;
+    if (selectedProfile.isBuiltIn && canCopy) {
       return (
         <td className="coding-rule-table-meta-cell coding-rule-activation-actions">
           {this.renderDeactivateButton('', 'coding_rules.need_extend_or_copy')}
@@ -138,6 +140,11 @@ export default class RuleListItem extends React.PureComponent<Props> {
       );
     }
 
+    const canEdit = selectedProfile.actions && selectedProfile.actions.edit;
+    if (!canEdit) {
+      return null;
+    }
+
     return (
       <td className="coding-rule-table-meta-cell coding-rule-activation-actions">
         {activation
@@ -177,7 +184,9 @@ export default class RuleListItem extends React.PureComponent<Props> {
       </ConfirmButton>
     ) : (
       <Tooltip overlay={translate(overlayTranslationKey)}>
-        <Button className="coding-rules-detail-quality-profile-deactivate button-red disabled">
+        <Button
+          className="coding-rules-detail-quality-profile-deactivate button-red"
+          disabled={true}>
           {translate('coding_rules.deactivate')}
         </Button>
       </Tooltip>
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/App-test.tsx
new file mode 100644 (file)
index 0000000..30348da
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { App } from '../App';
+import {
+  mockAppState,
+  mockCurrentUser,
+  mockLocation,
+  mockOrganization,
+  mockRouter
+} from '../../../../helpers/testMocks';
+import { getRulesApp } from '../../../../api/rules';
+import { isSonarCloud } from '../../../../helpers/system';
+
+jest.mock('../../../../api/rules', () => ({
+  getRulesApp: jest.fn().mockResolvedValue({ canWrite: true, repositories: [] }),
+  searchRules: jest.fn().mockResolvedValue({
+    actives: [],
+    rawActives: [],
+    facets: [],
+    rawFacets: [],
+    p: 0,
+    ps: 100,
+    rules: [],
+    total: 0
+  })
+}));
+
+jest.mock('../../../../api/quality-profiles', () => ({
+  searchQualityProfiles: jest.fn().mockResolvedValue({ profiles: [] })
+}));
+
+jest.mock('../../../../helpers/system', () => ({
+  isSonarCloud: jest.fn().mockResolvedValue(false)
+}));
+
+it('should render correctly', async () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+describe('renderBulkButton', () => {
+  it('should be null when the user is not logged in', () => {
+    const wrapper = shallowRender({
+      currentUser: mockCurrentUser()
+    });
+    expect(wrapper.instance().renderBulkButton()).toBeNull();
+  });
+
+  it('should be null when on SonarCloud and no organization is given', () => {
+    (isSonarCloud as jest.Mock).mockReturnValue(true);
+
+    const wrapper = shallowRender({
+      organization: undefined
+    });
+    expect(wrapper.instance().renderBulkButton()).toBeNull();
+  });
+
+  it('should be null when the user does not have the sufficient permission', () => {
+    (getRulesApp as jest.Mock).mockReturnValue({ canWrite: false, repositories: [] });
+
+    const wrapper = shallowRender();
+    expect(wrapper.instance().renderBulkButton()).toBeNull();
+  });
+
+  it('should show bulk change button when everything is fine', async () => {
+    (getRulesApp as jest.Mock).mockReturnValue({ canWrite: true, repositories: [] });
+    const wrapper = shallowRender();
+    await waitAndUpdate(wrapper);
+
+    expect(wrapper.instance().renderBulkButton()).toMatchSnapshot();
+  });
+});
+
+function shallowRender(props: Partial<App['props']> = {}) {
+  const organization = mockOrganization();
+  return shallow<App>(
+    <App
+      appState={mockAppState()}
+      currentUser={mockCurrentUser({
+        isLoggedIn: true
+      })}
+      languages={{ js: { key: 'js', name: 'JavaScript' } }}
+      location={mockLocation()}
+      organization={organization}
+      params={{}}
+      router={mockRouter()}
+      routes={[]}
+      userOrganizations={[organization]}
+      {...props}
+    />
+  );
+}
index b96125caf16b02a689d5f11e67e04d1d79b3a765..0e1470d7aa4e3e51c3afb472c41cd2b7758c8d35 100644 (file)
@@ -37,11 +37,11 @@ it('should render correctly', () => {
   expect(wrapper).toMatchSnapshot();
 });
 
-it('should not render anything', () => {
+it('should not a disabled button when edition is not possible', () => {
   const wrapper = shallowRender({
     referencedProfiles: { key: { ...profile, actions: { ...profile.actions, edit: false } } }
   });
-  expect(wrapper.type()).toBeNull();
+  expect(wrapper).toMatchSnapshot();
 });
 
 it('should display BulkChangeModal', () => {
index 589e92bcb21fc2cbe21587faf994f5e702b878c9..7df50122cb743ddf8b081e5d89450beffc8828e7 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { mockEvent, mockRule } from '../../../../helpers/testMocks';
+import { mockEvent, mockQualityProfile, mockRule } from '../../../../helpers/testMocks';
 import RuleListItem from '../RuleListItem';
 
 it('should render', () => {
@@ -41,9 +41,84 @@ it('should render deactivate button', () => {
   expect(instance.renderDeactivateButton('', 'coding_rules.need_extend_or_copy')).toMatchSnapshot();
 });
 
+describe('renderActions', () => {
+  it('should be null when there is no selected profile', () => {
+    const wrapper = shallowRender({
+      isLoggedIn: true
+    });
+
+    expect(wrapper.instance().renderActions()).toBeNull();
+  });
+
+  it('should be null when I am not logged in', () => {
+    const wrapper = shallowRender({
+      isLoggedIn: false,
+      selectedProfile: mockQualityProfile()
+    });
+
+    expect(wrapper.instance().renderActions()).toBeNull();
+  });
+
+  it('should be null when the user does not have the sufficient permissions', () => {
+    const wrapper = shallowRender({
+      isLoggedIn: true,
+      selectedProfile: mockQualityProfile()
+    });
+
+    expect(wrapper.instance().renderActions()).toBeNull();
+  });
+
+  it('should disable the button when I am on a built-in profile', () => {
+    const wrapper = shallowRender({
+      selectedProfile: mockQualityProfile({
+        actions: {
+          copy: true
+        },
+        isBuiltIn: true
+      })
+    });
+
+    expect(wrapper.instance().renderActions()).toMatchSnapshot();
+  });
+
+  it('should render the deactivate button', () => {
+    const wrapper = shallowRender({
+      activation: {
+        inherit: 'NONE',
+        severity: 'warning'
+      },
+      selectedProfile: mockQualityProfile({
+        actions: {
+          edit: true
+        },
+        isBuiltIn: false
+      })
+    });
+
+    expect(wrapper.instance().renderActions()).toMatchSnapshot();
+  });
+
+  it('should render the activate button', () => {
+    const wrapper = shallowRender({
+      rule: mockRule({
+        isTemplate: false
+      }),
+      selectedProfile: mockQualityProfile({
+        actions: {
+          edit: true
+        },
+        isBuiltIn: false
+      })
+    });
+
+    expect(wrapper.instance().renderActions()).toMatchSnapshot();
+  });
+});
+
 function shallowRender(props?: Partial<RuleListItem['props']>) {
   return shallow<RuleListItem>(
     <RuleListItem
+      isLoggedIn={true}
       onActivate={jest.fn()}
       onDeactivate={jest.fn()}
       onFilterChange={jest.fn()}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap
new file mode 100644 (file)
index 0000000..3184fc1
--- /dev/null
@@ -0,0 +1,198 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renderBulkButton should show bulk change button when everything is fine 1`] = `
+<BulkChange
+  languages={
+    Object {
+      "js": Object {
+        "key": "js",
+        "name": "JavaScript",
+      },
+    }
+  }
+  organization="foo"
+  query={
+    Object {
+      "activation": undefined,
+      "activationSeverities": Array [],
+      "availableSince": undefined,
+      "compareToProfile": undefined,
+      "cwe": Array [],
+      "inheritance": undefined,
+      "languages": Array [],
+      "owaspTop10": Array [],
+      "profile": undefined,
+      "repositories": Array [],
+      "ruleKey": undefined,
+      "sansTop25": Array [],
+      "searchQuery": undefined,
+      "severities": Array [],
+      "sonarsourceSecurity": Array [],
+      "statuses": Array [],
+      "tags": Array [],
+      "template": undefined,
+      "types": Array [],
+    }
+  }
+  referencedProfiles={Object {}}
+  total={0}
+/>
+`;
+
+exports[`should render correctly 1`] = `
+<Fragment>
+  <Suggestions
+    suggestions="coding_rules"
+  />
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="coding_rules.page"
+  >
+    <meta
+      content="noindex"
+      name="robots"
+    />
+  </HelmetWrapper>
+  <div
+    className="layout-page"
+    id="coding-rules-page"
+  >
+    <ScreenPositionHelper
+      className="layout-page-side-outer"
+    >
+      <Component />
+    </ScreenPositionHelper>
+    <div
+      className="layout-page-main"
+    >
+      <div
+        className="layout-page-header-panel layout-page-main-header"
+      >
+        <div
+          className="layout-page-header-panel-inner layout-page-main-header-inner"
+        >
+          <div
+            className="layout-page-main-inner"
+          >
+            <A11ySkipTarget
+              anchor="rules_main"
+            />
+            <PageActions
+              loading={true}
+              onReload={[Function]}
+            />
+          </div>
+        </div>
+      </div>
+      <div
+        className="layout-page-main-inner"
+      />
+    </div>
+  </div>
+</Fragment>
+`;
+
+exports[`should render correctly 2`] = `
+<Fragment>
+  <Suggestions
+    suggestions="coding_rules"
+  />
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="coding_rules.page"
+  >
+    <meta
+      content="noindex"
+      name="robots"
+    />
+  </HelmetWrapper>
+  <div
+    className="layout-page"
+    id="coding-rules-page"
+  >
+    <ScreenPositionHelper
+      className="layout-page-side-outer"
+    >
+      <Component />
+    </ScreenPositionHelper>
+    <div
+      className="layout-page-main"
+    >
+      <div
+        className="layout-page-header-panel layout-page-main-header"
+      >
+        <div
+          className="layout-page-header-panel-inner layout-page-main-header-inner"
+        >
+          <div
+            className="layout-page-main-inner"
+          >
+            <A11ySkipTarget
+              anchor="rules_main"
+            />
+            <BulkChange
+              languages={
+                Object {
+                  "js": Object {
+                    "key": "js",
+                    "name": "JavaScript",
+                  },
+                }
+              }
+              organization="foo"
+              query={
+                Object {
+                  "activation": undefined,
+                  "activationSeverities": Array [],
+                  "availableSince": undefined,
+                  "compareToProfile": undefined,
+                  "cwe": Array [],
+                  "inheritance": undefined,
+                  "languages": Array [],
+                  "owaspTop10": Array [],
+                  "profile": undefined,
+                  "repositories": Array [],
+                  "ruleKey": undefined,
+                  "sansTop25": Array [],
+                  "searchQuery": undefined,
+                  "severities": Array [],
+                  "sonarsourceSecurity": Array [],
+                  "statuses": Array [],
+                  "tags": Array [],
+                  "template": undefined,
+                  "types": Array [],
+                }
+              }
+              referencedProfiles={Object {}}
+              total={0}
+            />
+            <PageActions
+              loading={false}
+              onReload={[Function]}
+              paging={
+                Object {
+                  "pageIndex": 0,
+                  "pageSize": 100,
+                  "total": 0,
+                }
+              }
+            />
+          </div>
+        </div>
+      </div>
+      <div
+        className="layout-page-main-inner"
+      >
+        <ListFooter
+          count={0}
+          loadMore={[Function]}
+          ready={true}
+          total={0}
+        />
+      </div>
+    </div>
+  </div>
+</Fragment>
+`;
index 2cc9fb4b0d55d76d92bc293e3d5c2330fddd2140..4cd43934d35f01b98d46f2202de7f3a654243ebe 100644 (file)
@@ -48,6 +48,19 @@ exports[`should display BulkChangeModal 1`] = `
 />
 `;
 
+exports[`should not a disabled button when edition is not possible 1`] = `
+<Tooltip
+  overlay="coding_rules.can_not_bulk_change"
+>
+  <Button
+    className="js-bulk-change"
+    disabled={true}
+  >
+    bulk_change
+  </Button>
+</Tooltip>
+`;
+
 exports[`should render correctly 1`] = `
 <Fragment>
   <Dropdown
index dd03a7730c325459dd97fa5567c69b2f740d3281..279ddfd36e6145e978e2bf45aa8837a1530bd36a 100644 (file)
@@ -1,5 +1,92 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`renderActions should disable the button when I am on a built-in profile 1`] = `
+<td
+  className="coding-rule-table-meta-cell coding-rule-activation-actions"
+>
+  <Tooltip
+    overlay="coding_rules.need_extend_or_copy"
+  >
+    <Button
+      className="coding-rules-detail-quality-profile-deactivate button-red"
+      disabled={true}
+    >
+      coding_rules.deactivate
+    </Button>
+  </Tooltip>
+</td>
+`;
+
+exports[`renderActions should render the activate button 1`] = `
+<td
+  className="coding-rule-table-meta-cell coding-rule-activation-actions"
+>
+  <ActivationButton
+    buttonText="coding_rules.activate"
+    className="coding-rules-detail-quality-profile-activate"
+    modalHeader="coding_rules.activate_in_quality_profile"
+    onDone={[Function]}
+    organization="org"
+    profiles={
+      Array [
+        Object {
+          "actions": Object {
+            "edit": true,
+          },
+          "activeDeprecatedRuleCount": 2,
+          "activeRuleCount": 10,
+          "childrenCount": 0,
+          "depth": 1,
+          "isBuiltIn": false,
+          "isDefault": false,
+          "isInherited": false,
+          "key": "key",
+          "language": "js",
+          "languageName": "JavaScript",
+          "name": "name",
+          "organization": "foo",
+          "projectCount": 3,
+        },
+      ]
+    }
+    rule={
+      Object {
+        "isTemplate": false,
+        "key": "javascript:S1067",
+        "lang": "js",
+        "langName": "JavaScript",
+        "name": "Use foo",
+        "severity": "MAJOR",
+        "status": "READY",
+        "sysTags": Array [
+          "a",
+          "b",
+        ],
+        "tags": Array [
+          "x",
+        ],
+        "type": "CODE_SMELL",
+      }
+    }
+  />
+</td>
+`;
+
+exports[`renderActions should render the deactivate button 1`] = `
+<td
+  className="coding-rule-table-meta-cell coding-rule-activation-actions"
+>
+  <ConfirmButton
+    confirmButtonText="yes"
+    modalBody="coding_rules.deactivate.confirm"
+    modalHeader="coding_rules.deactivate"
+    onConfirm={[Function]}
+  >
+    [Function]
+  </ConfirmButton>
+</td>
+`;
+
 exports[`should render 1`] = `
 <div
   className="coding-rule"
@@ -113,7 +200,8 @@ exports[`should render deactivate button 2`] = `
   overlay="coding_rules.need_extend_or_copy"
 >
   <Button
-    className="coding-rules-detail-quality-profile-deactivate button-red disabled"
+    className="coding-rules-detail-quality-profile-deactivate button-red"
+    disabled={true}
   >
     coding_rules.deactivate
   </Button>
index 8f74212b0ef6400113f2609dd3db71021093bab2..eb22b0b96af747193100e7a2b1ccd339f82c14e2 100644 (file)
@@ -1302,6 +1302,7 @@ coding_rules.available_since=Available Since
 coding_rules.bulk_change=Bulk Change
 coding_rules.bulk_change.success={2} rule(s) changed in profile {0} - {1}
 coding_rules.bulk_change.warning={2} rule(s) changed, {3} rule(s) ignored in profile {0} - {1}
+coding_rules.can_not_bulk_change=Bulk change is only available when you have a custom Quality Profile to target. You can create a customizable Quality Profile based on a built-in one by Copying or Extending it in the Quality Profiles list.
 coding_rules.can_not_deactivate=This rule is inherited and can not be deactivated.
 coding_rules.change_details=Change Details of Quality Profile
 coding_rules.create=Create