]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20355 Enable Desactivate button for inherited rules if allowed by settings
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 12 Sep 2023 13:30:23 +0000 (15:30 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 14 Sep 2023 20:02:39 +0000 (20:02 +0000)
Co-authored-by: Benjamin Raymond <31401273+7PH@users.noreply.github.com>
server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts
server/sonar-web/src/main/js/api/mocks/data/qualityProfiles.ts
server/sonar-web/src/main/js/api/mocks/data/rules.ts
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx
server/sonar-web/src/main/js/types/settings.ts

index 2b8241ebdcf9c29ebe09a178b8596418589772b8..84d1a4fa5fad248c54631813426176a9698ceef0 100644 (file)
@@ -464,6 +464,7 @@ export default class CodingRulesServiceMock {
     }
     const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs);
     return this.reply({
+      actives: qprofile ? this.rulesActivations : undefined,
       rules: responseRules,
       facets: facetCounts,
       paging: mockPaging({
index 3c9d84e6828f3aebd86145bb07f19521013db421..ec624fa4722a7549d7e42115213ef04b6424b8c9 100644 (file)
@@ -132,6 +132,10 @@ export default class SettingsServiceMock {
       key: 'sonar.javascript.globals',
       values: ['angular', 'google', 'd3'],
     },
+    {
+      key: SettingsKey.QPAdminCanDisableInheritedRules,
+      value: 'true',
+    },
   ];
 
   #settingValues: SettingValue[] = cloneDeep(this.#defaultValues);
index c7f584669e69c7b14a82722921656e9f065f40e4..7a1bdd68f78ebd54dd76bf97b68b22e95e814f1c 100644 (file)
@@ -29,7 +29,7 @@ export function mockQualityProfilesList() {
       languageName: 'Java',
       actions: { edit: true },
     }),
-    mockQualityProfile({ key: QP_2, name: 'QP Bar', language: 'js' }),
+    mockQualityProfile({ key: QP_2, name: 'QP Bar', language: 'py', languageName: 'Python' }),
     mockQualityProfile({ key: QP_3, name: 'QP FooBar', language: 'java', languageName: 'Java' }),
     mockQualityProfile({
       key: QP_4,
index 63855d8ac1e293e554c33ceabf0be5e28c2832e4..3b07b24525947b553f79cb7131e23cec4ef3c8a3 100644 (file)
@@ -27,6 +27,8 @@ import {
 } from '../../../types/clean-code-taxonomy';
 import {
   ADVANCED_RULE,
+  QP_1,
+  QP_2,
   RULE_1,
   RULE_10,
   RULE_11,
@@ -243,6 +245,10 @@ export function mockRuleDetailsList() {
 
 export function mockRulesActivationsInQP() {
   return {
-    [RULE_1]: [mockRuleActivation({ qProfile: 'p1' })],
+    [RULE_1]: [mockRuleActivation({ qProfile: QP_1 })],
+    [RULE_7]: [mockRuleActivation({ qProfile: QP_2 })],
+    [RULE_8]: [mockRuleActivation({ qProfile: QP_2 })],
+    [RULE_9]: [mockRuleActivation({ qProfile: QP_2, inherit: 'INHERITED' })],
+    [RULE_10]: [mockRuleActivation({ qProfile: QP_2, inherit: 'OVERRIDES' })],
   };
 }
index c3be7edfde1f67200153ad4f8b2e8bf511f64daa..8c7625b851d774986b1cf615835e7d61b8f5a45a 100644 (file)
@@ -20,6 +20,8 @@
 import { act, fireEvent, screen } from '@testing-library/react';
 import selectEvent from 'react-select-event';
 import CodingRulesServiceMock, { RULE_TAGS_MOCK } from '../../../api/mocks/CodingRulesServiceMock';
+import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
+import { QP_2 } from '../../../api/mocks/data/ids';
 import { CLEAN_CODE_CATEGORIES, SOFTWARE_QUALITIES } from '../../../helpers/constants';
 import { parseDate } from '../../../helpers/dates';
 import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
@@ -29,13 +31,18 @@ import {
   CleanCodeAttributeCategory,
   SoftwareQuality,
 } from '../../../types/clean-code-taxonomy';
+import { SettingsKey } from '../../../types/settings';
 import { CurrentUser } from '../../../types/users';
 import routes from '../routes';
 import { getPageObjects } from '../utils-tests';
 
-const handler: CodingRulesServiceMock = new CodingRulesServiceMock();
+const rulesHandler = new CodingRulesServiceMock();
+const settingsHandler = new SettingsServiceMock();
 
-afterEach(() => handler.reset());
+afterEach(() => {
+  rulesHandler.reset();
+  settingsHandler.reset();
+});
 
 describe('Rules app list', () => {
   it('renders correctly', async () => {
@@ -45,7 +52,7 @@ describe('Rules app list', () => {
     await ui.appLoaded();
 
     // Renders list
-    handler
+    rulesHandler
       .allRulesName()
       .forEach((name) => expect(ui.ruleListItemLink(name).get()).toBeInTheDocument());
 
@@ -257,7 +264,7 @@ describe('Rules app list', () => {
   describe('bulk change', () => {
     it('no quality profile for bulk change based on language search', async () => {
       const { ui, user } = getPageObjects();
-      handler.setIsAdmin();
+      rulesHandler.setIsAdmin();
       renderCodingRulesApp(mockLoggedInUser());
       await ui.appLoaded();
 
@@ -276,13 +283,13 @@ describe('Rules app list', () => {
 
     it('should be able to bulk activate quality profile', async () => {
       const { ui, user } = getPageObjects();
-      handler.setIsAdmin();
+      rulesHandler.setIsAdmin();
       renderCodingRulesApp(mockLoggedInUser());
       await ui.appLoaded();
 
-      const [selectQPSuccess, selectQPWarning] = handler.allQualityProfile('java');
+      const [selectQPSuccess, selectQPWarning] = rulesHandler.allQualityProfile('java');
 
-      const rulesCount = handler.allRulesCount();
+      const rulesCount = rulesHandler.allRulesCount();
 
       await ui.bulkActivate(rulesCount, selectQPSuccess);
 
@@ -293,7 +300,7 @@ describe('Rules app list', () => {
       await user.click(ui.bulkClose.get());
 
       // Try bulk change when quality profile has warnning.
-      handler.activateWithWarning();
+      rulesHandler.activateWithWarning();
 
       await ui.bulkActivate(rulesCount, selectQPWarning);
       expect(
@@ -305,12 +312,12 @@ describe('Rules app list', () => {
 
     it('should be able to bulk deactivate quality profile', async () => {
       const { ui } = getPageObjects();
-      handler.setIsAdmin();
+      rulesHandler.setIsAdmin();
       renderCodingRulesApp(mockLoggedInUser());
       await ui.appLoaded();
 
-      const [selectQP] = handler.allQualityProfile('java');
-      const rulesCount = handler.allRulesCount();
+      const [selectQP] = rulesHandler.allQualityProfile('java');
+      const rulesCount = rulesHandler.allRulesCount();
 
       await ui.bulkDeactivate(rulesCount, selectQP);
 
@@ -322,34 +329,51 @@ describe('Rules app list', () => {
 
   it('can activate/deactivate specific rule for quality profile', async () => {
     const { ui, user } = getPageObjects();
+    rulesHandler.setIsAdmin();
     renderCodingRulesApp(mockLoggedInUser());
     await ui.appLoaded();
 
     await act(async () => {
       await user.click(ui.qpFacet.get());
-      await user.click(ui.facetItem('QP Foo Java').get());
+      await user.click(ui.facetItem('QP Bar Python').get());
     });
 
-    // Only one rule is activated in selected QP
-    expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
+    // Only 4 rules are activated in selected QP
+    expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(4);
 
     // Switch to inactive rules
     await act(async () => {
-      await user.click(ui.qpInactiveRadio.get(ui.facetItem('QP Foo Java').get()));
+      await user.click(ui.qpInactiveRadio.get(ui.facetItem('QP Bar Python').get()));
     });
-    expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
+    expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2);
+    expect(ui.activateButton.getAll()).toHaveLength(2);
 
     // Activate Rule for qp
-    await user.click(ui.activateButton.get());
+    await user.click(ui.activateButton.getAll()[0]);
     await user.click(ui.activateButton.get(ui.activateQPDialog.get()));
-    expect(ui.activateButton.query()).not.toBeInTheDocument();
-    expect(ui.deactivateButton.get()).toBeInTheDocument();
+    expect(ui.activateButton.getAll()).toHaveLength(1);
+    expect(ui.deactivateButton.getAll()).toHaveLength(1);
 
     // Deactivate activated rule
     await user.click(ui.deactivateButton.get());
     await user.click(ui.yesButton.get());
     expect(ui.deactivateButton.query()).not.toBeInTheDocument();
-    expect(ui.activateButton.get()).toBeInTheDocument();
+    expect(ui.activateButton.getAll()).toHaveLength(2);
+  });
+
+  it('can not deactivate rules for quality profile if setting is false', async () => {
+    const { ui } = getPageObjects();
+    rulesHandler.setIsAdmin();
+    settingsHandler.set(SettingsKey.QPAdminCanDisableInheritedRules, 'false');
+    renderCodingRulesApp(
+      mockLoggedInUser(),
+      'coding_rules?activation=true&tags=cute&qprofile=' + QP_2,
+    );
+    await ui.appLoaded();
+
+    // Only rule 9 is shown (inherited, activated)
+    expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
+    expect(ui.deactivateButton.get()).toBeDisabled();
   });
 
   it('navigates by keyboard', async () => {
@@ -508,7 +532,7 @@ describe('Rule app details', () => {
 
   it('can activate/change/deactivate rule in quality profile', async () => {
     const { ui, user } = getPageObjects();
-    handler.setIsAdmin();
+    rulesHandler.setIsAdmin();
     renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1');
     await ui.appLoaded();
     expect(ui.qpLink('QP Foo').get()).toBeInTheDocument();
@@ -552,7 +576,7 @@ describe('Rule app details', () => {
 
   it('can extend the rule description', async () => {
     const { ui, user } = getPageObjects();
-    handler.setIsAdmin();
+    rulesHandler.setIsAdmin();
     renderCodingRulesApp(undefined, 'coding_rules?open=rule5');
     await ui.appLoaded();
     expect(ui.ruleTitle('Awsome Python rule').get()).toBeInTheDocument();
@@ -585,7 +609,7 @@ describe('Rule app details', () => {
 
   it('can set tags', async () => {
     const { ui, user } = getPageObjects();
-    handler.setIsAdmin();
+    rulesHandler.setIsAdmin();
     renderCodingRulesApp(undefined, 'coding_rules?open=rule10');
     await ui.appLoaded();
 
@@ -613,7 +637,7 @@ describe('Rule app details', () => {
   describe('custom rule', () => {
     it('can create custom rule', async () => {
       const { ui, user } = getPageObjects();
-      handler.setIsAdmin();
+      rulesHandler.setIsAdmin();
       renderCodingRulesApp(mockLoggedInUser());
       await ui.appLoaded();
 
@@ -655,7 +679,7 @@ describe('Rule app details', () => {
 
     it('can edit custom rule', async () => {
       const { ui, user } = getPageObjects();
-      handler.setIsAdmin();
+      rulesHandler.setIsAdmin();
       renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
       await ui.appLoaded();
 
@@ -675,7 +699,7 @@ describe('Rule app details', () => {
 
     it('can delete custom rule', async () => {
       const { ui, user } = getPageObjects();
-      handler.setIsAdmin();
+      rulesHandler.setIsAdmin();
       renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
       await ui.appLoaded();
 
@@ -690,7 +714,7 @@ describe('Rule app details', () => {
 
     it('can delete custom rule from template page', async () => {
       const { ui, user } = getPageObjects();
-      handler.setIsAdmin();
+      rulesHandler.setIsAdmin();
       renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
       await ui.appLoaded();
 
index b4807c09316c34fe4717cafe449e260a8c2817e0..8ed2bebc61b67abefb7d241473c5c1e96d6cce17 100644 (file)
@@ -22,6 +22,7 @@ import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { Profile, searchQualityProfiles } from '../../../api/quality-profiles';
 import { getRulesApp, searchRules } from '../../../api/rules';
+import { getValue } from '../../../api/settings';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
 import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
 import FiltersHeader from '../../../components/common/FiltersHeader';
@@ -42,6 +43,7 @@ import {
   removeWhitePageClass,
 } from '../../../helpers/pages';
 import { SecurityStandard } from '../../../types/security';
+import { SettingsKey } from '../../../types/settings';
 import { Dict, Paging, RawQuery, Rule, RuleActivation } from '../../../types/types';
 import { CurrentUser, isLoggedIn } from '../../../types/users';
 import {
@@ -85,6 +87,7 @@ interface Props {
 interface State {
   actives?: Actives;
   canWrite?: boolean;
+  canDeactivateInherited?: boolean;
   facets?: Facets;
   loading: boolean;
   openFacets: OpenFacets;
@@ -236,17 +239,20 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {
 
   fetchInitialData = () => {
     this.setState({ loading: true });
-    Promise.all([getRulesApp(), searchQualityProfiles()]).then(
-      ([{ canWrite, repositories }, { profiles }]) => {
-        this.setState({
-          canWrite,
-          referencedProfiles: keyBy(profiles, 'key'),
-          referencedRepositories: keyBy(repositories, 'key'),
-        });
-        this.fetchFirstRules();
-      },
-      this.stopLoading,
-    );
+
+    Promise.all([
+      getRulesApp(),
+      searchQualityProfiles(),
+      getValue({ key: SettingsKey.QPAdminCanDisableInheritedRules }),
+    ]).then(([{ canWrite, repositories }, { profiles }, setting]) => {
+      this.setState({
+        canWrite,
+        canDeactivateInherited: setting?.value === 'true',
+        referencedProfiles: keyBy(profiles, 'key'),
+        referencedRepositories: keyBy(repositories, 'key'),
+      });
+      this.fetchFirstRules();
+    }, this.stopLoading);
   };
 
   makeFetchRequest = (query?: RawQuery) =>
@@ -664,6 +670,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {
                       <RuleListItem
                         activation={this.getRuleActivation(rule.key)}
                         isLoggedIn={isLoggedIn(this.props.currentUser)}
+                        canDeactivateInherited={this.state.canDeactivateInherited}
                         key={rule.key}
                         onActivate={this.handleRuleActivate}
                         onDeactivate={this.handleRuleDeactivate}
index ab88c8a381e7575bc668056088cb10bf55de4f51..885fe4a9267ed08815f2eeb8c75e41be11b32d6b 100644 (file)
@@ -38,6 +38,7 @@ import RuleInheritanceIcon from './RuleInheritanceIcon';
 interface Props {
   activation?: Activation;
   isLoggedIn: boolean;
+  canDeactivateInherited?: boolean;
   onActivate: (profile: string, rule: string, activation: Activation) => void;
   onDeactivate: (profile: string, rule: string) => void;
   onOpen: (ruleKey: string) => void;
@@ -128,7 +129,7 @@ export default class RuleListItem extends React.PureComponent<Props> {
   };
 
   renderActions = () => {
-    const { activation, isLoggedIn, rule, selectedProfile } = this.props;
+    const { activation, isLoggedIn, canDeactivateInherited, rule, selectedProfile } = this.props;
 
     if (!selectedProfile || !isLoggedIn) {
       return null;
@@ -155,7 +156,7 @@ export default class RuleListItem extends React.PureComponent<Props> {
     if (activation) {
       return (
         <td className="coding-rule-table-meta-cell coding-rule-activation-actions">
-          {activation.inherit === 'NONE' ? (
+          {activation.inherit === 'NONE' || canDeactivateInherited ? (
             <ConfirmButton
               confirmButtonText={translate('yes')}
               modalBody={translate('coding_rules.deactivate.confirm')}
@@ -173,7 +174,10 @@ export default class RuleListItem extends React.PureComponent<Props> {
             </ConfirmButton>
           ) : (
             <Tooltip overlay={translate('coding_rules.can_not_deactivate')}>
-              <Button className="coding-rules-detail-quality-profile-deactivate button-red disabled">
+              <Button
+                className="coding-rules-detail-quality-profile-deactivate button-red"
+                disabled
+              >
                 {translate('coding_rules.deactivate')}
               </Button>
             </Tooltip>
index 76f3d45751ccef57c978f734d369629ad01143b1..8444f8112f435f41e1c90d49c824284dd80aa993 100644 (file)
@@ -27,6 +27,7 @@ export const enum SettingsKey {
   PluginRiskConsent = 'sonar.plugins.risk.consent',
   LicenceRemainingLocNotificationThreshold = 'sonar.license.notifications.remainingLocThreshold',
   TokenMaxAllowedLifetime = 'sonar.auth.token.max.allowed.lifetime',
+  QPAdminCanDisableInheritedRules = 'sonar.allowQualityProfileAdminsDisableInheritedRules',
 }
 
 export enum GlobalSettingKeys {