]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22224 Allow changing the rules severity from the rule list
authorViktor Vorona <viktor.vorona@sonarsource.com>
Wed, 22 May 2024 07:46:10 +0000 (09:46 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 3 Jun 2024 20:02:57 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
server/sonar-web/src/main/js/api/mocks/data/qualityProfiles.ts
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
server/sonar-web/src/main/js/apps/coding-rules/components/ActivatedRuleActions.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx
server/sonar-web/src/main/js/apps/coding-rules/query.ts
server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx

index 71cc1cbfe2261e639e10aedff4023256fec4cc5d..acbd17715659c9fae7ff6f96be246549e27de028 100644 (file)
@@ -251,7 +251,7 @@ export default class CodingRulesServiceMock {
       filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key));
     }
     if (q && q.length > 2) {
-      filteredRules = filteredRules.filter((r) => r.name.includes(q));
+      filteredRules = filteredRules.filter((r) => r.name.includes(q) || r.key.includes(q));
     }
     if (tags) {
       filteredRules = filteredRules.filter((r) => r.tags && r.tags.some((t) => tags.includes(t)));
index 48c31a14b94be9ac220e9f484aceefe5c7a9b8f3..677e9d4e04e8b6696d99ff00ff855a0e9532fca7 100644 (file)
@@ -29,7 +29,14 @@ export function mockQualityProfilesList() {
       languageName: 'Java',
       actions: { edit: true },
     }),
-    mockQualityProfile({ key: QP_2, name: 'QP Bar', language: 'py', languageName: 'Python' }),
+    mockQualityProfile({
+      key: QP_2,
+      name: 'QP Bar',
+      language: 'py',
+      languageName: 'Python',
+      parentKey: 'sonar_way',
+      parentName: 'Sonar Way',
+    }),
     mockQualityProfile({ key: QP_3, name: 'QP FooBar', language: 'java', languageName: 'Java' }),
     mockQualityProfile({
       key: QP_4,
index 87d5ae87ac4883f0abf9922871cbb7c03f0e34b0..0e3c07ffcb7650a3a1c5842b8f96e013a122e982 100644 (file)
@@ -21,7 +21,7 @@ import { fireEvent, screen, within } 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, RULE_1 } from '../../../api/mocks/data/ids';
+import { QP_2, RULE_1, RULE_10 } from '../../../api/mocks/data/ids';
 import { CLEAN_CODE_CATEGORIES, SOFTWARE_QUALITIES } from '../../../helpers/constants';
 import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
 import {
@@ -298,7 +298,7 @@ describe('Rules app list', () => {
     });
   });
 
-  it('can activate/deactivate specific rule for quality profile', async () => {
+  it('can activate/change/deactivate specific rule for quality profile', async () => {
     const { ui, user } = getPageObjects();
     rulesHandler.setIsAdmin();
     renderCodingRulesApp(mockLoggedInUser());
@@ -314,15 +314,29 @@ describe('Rules app list', () => {
     await user.click(ui.qpInactiveRadio.get(ui.facetItem('QP Bar Python').get()));
     expect(ui.getAllRuleListItems()).toHaveLength(2);
     expect(ui.activateButton.getAll()).toHaveLength(2);
+    expect(ui.changeButton(QP_2).query()).not.toBeInTheDocument();
 
     // Activate Rule for qp
     await user.click(ui.activateButton.getAll()[0]);
+    expect(ui.selectValue.get(ui.activateQPDialog.get())).toHaveTextContent('severity.MAJOR');
     await selectEvent.select(ui.oldSeveritySelect.get(), 'severity.MINOR');
     await user.click(ui.activateButton.get(ui.activateQPDialog.get()));
 
     expect(ui.activateButton.getAll()).toHaveLength(1);
+    expect(ui.changeButton('QP Bar').get()).toBeInTheDocument();
     expect(ui.deactivateButton.getAll()).toHaveLength(1);
 
+    // Change Rule for qp
+    await user.click(ui.changeButton('QP Bar').get());
+    expect(ui.selectValue.get(ui.changeQPDialog.get())).toHaveTextContent('severity.MINOR');
+    await selectEvent.select(ui.oldSeveritySelect.get(), 'severity.BLOCKER');
+    await user.click(ui.saveButton.get(ui.changeQPDialog.get()));
+
+    // Check that new severity is saved
+    await user.click(ui.changeButton('QP Bar').get());
+    expect(ui.selectValue.get(ui.changeQPDialog.get())).toHaveTextContent('severity.BLOCKER');
+    await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
+
     // Deactivate activated rule
     await user.click(ui.deactivateButton.get());
     await user.click(ui.yesButton.get());
@@ -330,6 +344,50 @@ describe('Rules app list', () => {
     expect(ui.activateButton.getAll()).toHaveLength(2);
   });
 
+  it('can revert to parent definition specific rule for quality profile', async () => {
+    const { ui, user } = getPageObjects();
+    settingsHandler.set(SettingsKey.QPAdminCanDisableInheritedRules, 'false');
+    rulesHandler.setIsAdmin();
+    renderCodingRulesApp(mockLoggedInUser());
+    await ui.appLoaded();
+
+    await user.click(ui.qpFacet.get());
+    await user.click(ui.facetItem('QP Bar Python').get());
+
+    // Only 4 rules are activated in selected QP
+    expect(ui.getAllRuleListItems()).toHaveLength(4);
+
+    // 3 rules have deactivate button and 1 rule has revert to parent definition button
+    expect(ui.deactivateButton.getAll()).toHaveLength(3);
+    expect(ui.revertToParentDefinitionButton.get()).toBeInTheDocument();
+
+    await user.type(ui.searchInput.get(), RULE_10);
+
+    // Only 1 rule left after search
+    expect(ui.getAllRuleListItems()).toHaveLength(1);
+    expect(ui.revertToParentDefinitionButton.get()).toBeInTheDocument();
+    expect(ui.changeButton('QP Bar').get()).toBeInTheDocument();
+
+    // Check that severity is reflected correctly
+    await user.click(ui.changeButton('QP Bar').get());
+    expect(ui.selectValue.get(ui.changeQPDialog.get())).toHaveTextContent('severity.MAJOR');
+    await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
+
+    await user.click(ui.revertToParentDefinitionButton.get());
+    await user.click(ui.yesButton.get());
+
+    expect(ui.getAllRuleListItems()).toHaveLength(1);
+    expect(ui.revertToParentDefinitionButton.query()).not.toBeInTheDocument();
+    expect(ui.deactivateButton.get()).toBeInTheDocument();
+    expect(ui.deactivateButton.get()).toBeDisabled();
+    expect(ui.changeButton('QP Bar').get()).toBeInTheDocument();
+
+    // Check that severity is reflected correctly
+    await user.click(ui.changeButton('QP Bar').get());
+    expect(ui.selectValue.get(ui.changeQPDialog.get())).toHaveTextContent('severity.MINOR');
+    await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
+  });
+
   it('can not deactivate rules for quality profile if setting is false', async () => {
     const { ui } = getPageObjects();
     rulesHandler.setIsAdmin();
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivatedRuleActions.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivatedRuleActions.tsx
new file mode 100644 (file)
index 0000000..c697a90
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { DangerButtonSecondary, Tooltip } from 'design-system';
+import * as React from 'react';
+import { Profile } from '../../../api/quality-profiles';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { Rule, RuleActivation } from '../../../types/types';
+import ActivationButton from './ActivationButton';
+
+interface Props {
+  activation: RuleActivation;
+  profile: Profile;
+  ruleDetails: Rule;
+  onActivate: (severity: string) => Promise<void> | void;
+  handleRevert: (key?: string) => void;
+  handleDeactivate: (key?: string) => void;
+  showDeactivated?: boolean;
+  canDeactivateInherited?: boolean;
+}
+
+export default function ActivatedRuleActions(props: Readonly<Props>) {
+  const {
+    activation,
+    profile,
+    ruleDetails,
+    onActivate,
+    handleRevert,
+    handleDeactivate,
+    showDeactivated,
+    canDeactivateInherited,
+  } = props;
+
+  const canEdit = profile.actions?.edit && !profile.isBuiltIn;
+  const hasParent = activation.inherit !== 'NONE' && profile.parentKey !== undefined;
+
+  return (
+    <>
+      {canEdit && (
+        <>
+          {!ruleDetails.isTemplate && (
+            <ActivationButton
+              className="sw-ml-2"
+              activation={activation}
+              ariaLabel={translateWithParameters('coding_rules.change_details_x', profile.name)}
+              buttonText={translate('change_verb')}
+              modalHeader={translate('coding_rules.change_details')}
+              onDone={onActivate}
+              profiles={[profile]}
+              rule={ruleDetails}
+            />
+          )}
+
+          {hasParent && activation.inherit === 'OVERRIDES' && profile.parentName && (
+            <ConfirmButton
+              confirmButtonText={translate('yes')}
+              confirmData={profile.key}
+              isDestructive
+              modalBody={translateWithParameters(
+                'coding_rules.revert_to_parent_definition.confirm',
+                profile.parentName,
+              )}
+              modalHeader={translate('coding_rules.revert_to_parent_definition')}
+              onConfirm={handleRevert}
+            >
+              {({ onClick }) => (
+                <DangerButtonSecondary className="sw-ml-2 sw-whitespace-nowrap" onClick={onClick}>
+                  {translate('coding_rules.revert_to_parent_definition')}
+                </DangerButtonSecondary>
+              )}
+            </ConfirmButton>
+          )}
+
+          {(!hasParent || canDeactivateInherited) && (
+            <ConfirmButton
+              confirmButtonText={translate('yes')}
+              confirmData={profile.key}
+              modalBody={translate('coding_rules.deactivate.confirm')}
+              modalHeader={translate('coding_rules.deactivate')}
+              onConfirm={handleDeactivate}
+            >
+              {({ onClick }) => (
+                <DangerButtonSecondary
+                  className="sw-ml-2 sw-whitespace-nowrap"
+                  aria-label={translateWithParameters(
+                    'coding_rules.deactivate_in_quality_profile_x',
+                    profile.name,
+                  )}
+                  onClick={onClick}
+                >
+                  {translate('coding_rules.deactivate')}
+                </DangerButtonSecondary>
+              )}
+            </ConfirmButton>
+          )}
+
+          {showDeactivated &&
+            hasParent &&
+            !canDeactivateInherited &&
+            activation.inherit !== 'OVERRIDES' && (
+              <Tooltip overlay={translate('coding_rules.can_not_deactivate')}>
+                <DangerButtonSecondary
+                  disabled
+                  className="sw-ml-2"
+                  aria-label={translateWithParameters(
+                    'coding_rules.deactivate_in_quality_profile_x',
+                    profile.name,
+                  )}
+                >
+                  {translate('coding_rules.deactivate')}
+                </DangerButtonSecondary>
+              </Tooltip>
+            )}
+        </>
+      )}
+    </>
+  );
+}
index 970b3a959e6a50dca3af0ce1de43b966e097064f..2ed87f2e2cb30a63756d347e1b10442b2faf2a87 100644 (file)
@@ -57,7 +57,6 @@ import {
   shouldOpenStandardsFacet,
 } from '../../issues/utils';
 import {
-  Activation,
   Actives,
   FacetKey,
   Facets,
@@ -512,7 +511,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {
     }
   };
 
-  handleRuleActivate = (profile: string, rule: string, activation: Activation) =>
+  handleRuleActivate = (profile: string, rule: string, activation: RuleActivation) =>
     this.setState((state: State) => {
       const { actives = {} } = state;
       if (!actives[rule]) {
@@ -701,8 +700,8 @@ function parseActives(rawActives: Dict<RuleActivation[]>) {
   const actives: Actives = {};
   for (const [rule, activations] of Object.entries(rawActives)) {
     actives[rule] = {};
-    for (const { inherit, qProfile, severity } of activations) {
-      actives[rule][qProfile] = { inherit, severity };
+    for (const activation of activations) {
+      actives[rule][activation.qProfile] = { ...activation };
     }
   }
   return actives;
index 1d975ba4494ca389d76b0c64e45528fcc400fd12..09e0dda08ff306f9ef633569d53e98aafd9a4078 100644 (file)
@@ -38,8 +38,7 @@ import {
   useRuleDetailsQuery,
   useUpdateRuleMutation,
 } from '../../../queries/rules';
-import { Dict } from '../../../types/types';
-import { Activation } from '../query';
+import { Dict, RuleActivation } from '../../../types/types';
 import CustomRuleButton from './CustomRuleButton';
 import RuleDetailsCustomRules from './RuleDetailsCustomRules';
 import RuleDetailsDescription from './RuleDetailsDescription';
@@ -52,7 +51,7 @@ interface Props {
   allowCustomRules?: boolean;
   canWrite?: boolean;
   canDeactivateInherited?: boolean;
-  onActivate: (profile: string, rule: string, activation: Activation) => void;
+  onActivate: (profile: string, rule: string, activation: RuleActivation) => void;
   onDeactivate: (profile: string, rule: string) => void;
   onDelete: (rule: string) => void;
   referencedProfiles: Dict<Profile>;
index abe0965eda6bb92fa7e44667cb69f21edc8cf091..ffeadf6c0551c14cc550eb220f821d12acdd6f90 100644 (file)
@@ -23,7 +23,6 @@ import {
   ActionCell,
   CellComponent,
   ContentCell,
-  DangerButtonSecondary,
   DiscreetLink,
   InheritanceIcon,
   Link,
@@ -36,8 +35,7 @@ import { filter } from 'lodash';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { Profile } from '../../../api/quality-profiles';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
 import { getQualityProfileUrl } from '../../../helpers/urls';
 import {
   useActivateRuleMutation,
@@ -45,6 +43,7 @@ import {
 } from '../../../queries/quality-profiles';
 import { Dict, RuleActivation, RuleDetails } from '../../../types/types';
 import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge';
+import ActivatedRuleActions from './ActivatedRuleActions';
 import ActivationButton from './ActivationButton';
 
 interface Props {
@@ -90,69 +89,17 @@ export default function RuleDetailsProfiles(props: Readonly<Props>) {
   };
 
   const renderRowActions = (activation: RuleActivation, profile: Profile) => {
-    const canEdit = profile.actions?.edit && !profile.isBuiltIn;
-    const hasParent = activation.inherit !== 'NONE' && profile.parentKey;
-
     return (
       <ActionCell>
-        {canEdit && (
-          <>
-            {!ruleDetails.isTemplate && (
-              <ActivationButton
-                activation={activation}
-                ariaLabel={translateWithParameters('coding_rules.change_details_x', profile.name)}
-                buttonText={translate('change_verb')}
-                modalHeader={translate('coding_rules.change_details')}
-                onDone={props.onActivate}
-                profiles={[profile]}
-                rule={ruleDetails}
-              />
-            )}
-
-            {hasParent && activation.inherit === 'OVERRIDES' && profile.parentName && (
-              <ConfirmButton
-                confirmButtonText={translate('yes')}
-                confirmData={profile.key}
-                isDestructive
-                modalBody={translateWithParameters(
-                  'coding_rules.revert_to_parent_definition.confirm',
-                  profile.parentName,
-                )}
-                modalHeader={translate('coding_rules.revert_to_parent_definition')}
-                onConfirm={handleRevert}
-              >
-                {({ onClick }) => (
-                  <DangerButtonSecondary className="sw-ml-2 sw-whitespace-nowrap" onClick={onClick}>
-                    {translate('coding_rules.revert_to_parent_definition')}
-                  </DangerButtonSecondary>
-                )}
-              </ConfirmButton>
-            )}
-
-            {(!hasParent || canDeactivateInherited) && (
-              <ConfirmButton
-                confirmButtonText={translate('yes')}
-                confirmData={profile.key}
-                modalBody={translate('coding_rules.deactivate.confirm')}
-                modalHeader={translate('coding_rules.deactivate')}
-                onConfirm={handleDeactivate}
-              >
-                {({ onClick }) => (
-                  <DangerButtonSecondary
-                    className="sw-ml-2 sw-whitespace-nowrap"
-                    aria-label={translateWithParameters(
-                      'coding_rules.deactivate_in_quality_profile_x',
-                      profile.name,
-                    )}
-                    onClick={onClick}
-                  >
-                    {translate('coding_rules.deactivate')}
-                  </DangerButtonSecondary>
-                )}
-              </ConfirmButton>
-            )}
-          </>
-        )}
+        <ActivatedRuleActions
+          activation={activation}
+          profile={profile}
+          ruleDetails={ruleDetails}
+          onActivate={props.onActivate}
+          handleDeactivate={handleDeactivate}
+          handleRevert={handleRevert}
+          canDeactivateInherited={canDeactivateInherited}
+        />
       </ActionCell>
     );
   };
index 8351a52a53441b8fad512426025dfcc6969473b2..71e3a55a5261aeb97b3fd3a1a517f335f89d17a2 100644 (file)
@@ -31,25 +31,27 @@ import {
 } from 'design-system';
 import * as React from 'react';
 import DocHelpTooltip from '~sonar-aligned/components/controls/DocHelpTooltip';
-import { Profile, deactivateRule } from '../../../api/quality-profiles';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { Profile } from '../../../api/quality-profiles';
 import Tooltip from '../../../components/controls/Tooltip';
 import { CleanCodeAttributePill } from '../../../components/shared/CleanCodeAttributePill';
 import SoftwareImpactPillList from '../../../components/shared/SoftwareImpactPillList';
 import TypeHelper from '../../../components/shared/TypeHelper';
 import TagsList from '../../../components/tags/TagsList';
-import { DocLink } from '../../../helpers/doc-links';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getRuleUrl } from '../../../helpers/urls';
-import { Rule } from '../../../types/types';
-import { Activation } from '../query';
+import {
+  useActivateRuleMutation,
+  useDeactivateRuleMutation,
+} from '../../../queries/quality-profiles';
+import { Rule, RuleActivation } from '../../../types/types';
+import ActivatedRuleActions from './ActivatedRuleActions';
 import ActivationButton from './ActivationButton';
 
 interface Props {
-  activation?: Activation;
+  activation?: RuleActivation;
   isLoggedIn: boolean;
   canDeactivateInherited?: boolean;
-  onActivate: (profile: string, rule: string, activation: Activation) => void;
+  onActivate: (profile: string, rule: string, activation: RuleActivation) => void;
   onDeactivate: (profile: string, rule: string) => void;
   onOpen: (ruleKey: string) => void;
   rule: Rule;
@@ -58,31 +60,66 @@ interface Props {
   selectedProfile?: Profile;
 }
 
-export default class RuleListItem extends React.PureComponent<Props> {
-  handleDeactivate = () => {
-    if (this.props.selectedProfile) {
+export default function RuleListItem(props: Readonly<Props>) {
+  const {
+    activation,
+    rule,
+    selectedProfile,
+    isLoggedIn,
+    selected,
+    selectRule,
+    canDeactivateInherited,
+    onDeactivate,
+    onActivate,
+    onOpen,
+  } = props;
+  const { mutate: activateRule } = useActivateRuleMutation((data) => {
+    if (data.reset && activation) {
+      onActivate(data.key, data.rule, {
+        ...activation,
+        // Actually the severity should be taken from the inherited qprofile, but we don't have this information
+        severity: rule.severity,
+        inherit: 'INHERITED',
+      });
+    }
+  });
+  const { mutate: deactivateRule } = useDeactivateRuleMutation((data) =>
+    onDeactivate(data.key, data.rule),
+  );
+  const handleDeactivate = () => {
+    if (selectedProfile) {
       const data = {
-        key: this.props.selectedProfile.key,
-        rule: this.props.rule.key,
+        key: selectedProfile.key,
+        rule: rule.key,
       };
-      deactivateRule(data).then(
-        () => this.props.onDeactivate(data.key, data.rule),
-        () => {},
-      );
+      deactivateRule(data);
     }
   };
 
-  handleActivate = (severity: string) => {
-    if (this.props.selectedProfile) {
-      this.props.onActivate(this.props.selectedProfile.key, this.props.rule.key, {
+  const handleActivate = (severity: string) => {
+    if (selectedProfile) {
+      onActivate(selectedProfile.key, rule.key, {
+        createdAt: new Date().toISOString(),
         severity,
-        inherit: 'NONE',
+        params: [],
+        qProfile: selectedProfile.key,
+        inherit: activation ? 'OVERRIDES' : 'NONE',
       });
     }
     return Promise.resolve();
   };
 
-  handleNameClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+  const handleRevert = (key?: string) => {
+    if (key !== undefined) {
+      activateRule({
+        key,
+        rule: rule.key,
+        reset: true,
+      });
+    }
+  };
+
+  const handleNameClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
     // cmd(ctrl) + click should open a rule permalink in a new tab
     const isLeftClickEvent = event.button === 0;
     const isModifiedEvent = !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
@@ -92,12 +129,11 @@ export default class RuleListItem extends React.PureComponent<Props> {
 
     event.preventDefault();
     event.stopPropagation();
-    this.props.onOpen(this.props.rule.key);
+    onOpen(rule.key);
   };
 
-  renderActivation = () => {
-    const { activation, selectedProfile } = this.props;
-    if (!activation || !selectedProfile?.parentName) {
+  const renderActivation = () => {
+    if (!activation || selectedProfile?.parentName === undefined) {
       return null;
     }
 
@@ -133,9 +169,7 @@ export default class RuleListItem extends React.PureComponent<Props> {
     );
   };
 
-  renderActions = () => {
-    const { activation, isLoggedIn, canDeactivateInherited, rule, selectedProfile } = this.props;
-
+  const renderActions = () => {
     if (!selectedProfile || !isLoggedIn) {
       return null;
     }
@@ -160,28 +194,16 @@ export default class RuleListItem extends React.PureComponent<Props> {
 
     if (activation) {
       return (
-        <div className="sw-ml-4">
-          {activation.inherit === 'NONE' || canDeactivateInherited ? (
-            <ConfirmButton
-              confirmButtonText={translate('yes')}
-              modalBody={translate('coding_rules.deactivate.confirm')}
-              modalHeader={translate('coding_rules.deactivate')}
-              onConfirm={this.handleDeactivate}
-            >
-              {({ onClick }) => (
-                <DangerButtonSecondary onClick={onClick}>
-                  {translate('coding_rules.deactivate')}
-                </DangerButtonSecondary>
-              )}
-            </ConfirmButton>
-          ) : (
-            <Tooltip content={translate('coding_rules.can_not_deactivate')}>
-              <DangerButtonSecondary disabled>
-                {translate('coding_rules.deactivate')}
-              </DangerButtonSecondary>
-            </Tooltip>
-          )}
-        </div>
+        <ActivatedRuleActions
+          activation={activation}
+          profile={selectedProfile}
+          ruleDetails={rule}
+          onActivate={handleActivate}
+          handleDeactivate={handleDeactivate}
+          handleRevert={handleRevert}
+          showDeactivated
+          canDeactivateInherited={canDeactivateInherited}
+        />
       );
     }
 
@@ -191,7 +213,7 @@ export default class RuleListItem extends React.PureComponent<Props> {
           <ActivationButton
             buttonText={translate('coding_rules.activate')}
             modalHeader={translate('coding_rules.activate_in_quality_profile')}
-            onDone={this.handleActivate}
+            onDone={handleActivate}
             profiles={[selectedProfile]}
             rule={rule}
           />
@@ -200,118 +222,115 @@ export default class RuleListItem extends React.PureComponent<Props> {
     );
   };
 
-  render() {
-    const { rule, selected } = this.props;
-    const allTags = [...(rule.tags ?? []), ...(rule.sysTags ?? [])];
-    return (
-      <ListItemStyled
-        selected={selected}
-        className="it__coding-rule sw-p-3 sw-mb-4 sw-rounded-1 sw-bg-white"
-        aria-current={selected}
-        data-rule={rule.key}
-        onClick={() => this.props.selectRule(rule.key)}
-      >
-        <div className="sw-flex sw-flex-col sw-gap-3">
-          <div className="sw-flex sw-justify-between sw-items-center">
-            <div className="sw-flex sw-items-center">
-              {this.renderActivation()}
+  const allTags = [...(rule.tags ?? []), ...(rule.sysTags ?? [])];
+  return (
+    <ListItemStyled
+      selected={selected}
+      className="it__coding-rule sw-p-3 sw-mb-4 sw-rounded-1 sw-bg-white"
+      aria-current={selected}
+      data-rule={rule.key}
+      onClick={() => selectRule(rule.key)}
+    >
+      <div className="sw-flex sw-flex-col sw-gap-3">
+        <div className="sw-flex sw-justify-between sw-items-center">
+          <div className="sw-flex sw-items-center">
+            {renderActivation()}
 
-              <Link
-                className="sw-body-sm-highlight"
-                onClick={this.handleNameClick}
-                to={getRuleUrl(rule.key)}
-              >
-                {rule.name}
-              </Link>
-            </div>
+            <Link
+              className="sw-body-sm-highlight"
+              onClick={handleNameClick}
+              to={getRuleUrl(rule.key)}
+            >
+              {rule.name}
+            </Link>
+          </div>
 
-            <div>
-              {rule.cleanCodeAttributeCategory !== undefined && (
-                <CleanCodeAttributePill
-                  cleanCodeAttributeCategory={rule.cleanCodeAttributeCategory}
-                  type="rule"
-                />
-              )}
-            </div>
+          <div>
+            {rule.cleanCodeAttributeCategory !== undefined && (
+              <CleanCodeAttributePill
+                cleanCodeAttributeCategory={rule.cleanCodeAttributeCategory}
+                type="rule"
+              />
+            )}
           </div>
+        </div>
 
-          <div className="sw-flex sw-items-center">
-            <div className="sw-grow sw-flex sw-gap-2 sw-items-center sw-body-xs">
-              {rule.impacts.length > 0 && (
-                <SoftwareImpactPillList softwareImpacts={rule.impacts} type="rule" />
-              )}
-            </div>
+        <div className="sw-flex sw-items-center">
+          <div className="sw-grow sw-flex sw-gap-2 sw-items-center sw-body-xs">
+            {rule.impacts.length > 0 && (
+              <SoftwareImpactPillList softwareImpacts={rule.impacts} type="rule" />
+            )}
+          </div>
 
-            <TextSubdued as="ul" className="sw-flex sw-gap-1 sw-items-center sw-body-xs">
-              <li>{rule.langName}</li>
+          <TextSubdued as="ul" className="sw-flex sw-gap-1 sw-items-center sw-body-xs">
+            <li>{rule.langName}</li>
 
-              <SeparatorCircleIcon aria-hidden as="li" />
-              <li>
-                <DocHelpTooltip
-                  content={
-                    <div>
-                      <p className="sw-mb-2">{translate('coding_rules.type.deprecation.title')}</p>
-                      <p>{translate('coding_rules.type.deprecation.filter_by')}</p>
-                    </div>
-                  }
-                  links={[
-                    {
-                      href: DocLink.CleanCodeIntroduction,
-                      label: translate('learn_more'),
-                    },
-                  ]}
-                >
-                  <TypeHelper
-                    className="sw-flex sw-items-center"
-                    iconFill="iconTypeDisabled"
-                    type={rule.type}
-                  />
-                </DocHelpTooltip>
-              </li>
+            <SeparatorCircleIcon aria-hidden as="li" />
+            <li>
+              <DocHelpTooltip
+                content={
+                  <div>
+                    <p className="sw-mb-2">{translate('coding_rules.type.deprecation.title')}</p>
+                    <p>{translate('coding_rules.type.deprecation.filter_by')}</p>
+                  </div>
+                }
+                links={[
+                  {
+                    href: '/user-guide/clean-code/introduction',
+                    label: translate('learn_more'),
+                  },
+                ]}
+              >
+                <TypeHelper
+                  className="sw-flex sw-items-center"
+                  iconFill="iconTypeDisabled"
+                  type={rule.type}
+                />
+              </DocHelpTooltip>
+            </li>
 
-              {rule.isTemplate && (
-                <>
-                  <SeparatorCircleIcon aria-hidden as="li" />
-                  <li>
-                    <Tooltip content={translate('coding_rules.rule_template.title')}>
-                      <span>
-                        <Badge>{translate('coding_rules.rule_template')}</Badge>
-                      </span>
-                    </Tooltip>
-                  </li>
-                </>
-              )}
+            {rule.isTemplate && (
+              <>
+                <SeparatorCircleIcon aria-hidden as="li" />
+                <li>
+                  <Tooltip overlay={translate('coding_rules.rule_template.title')}>
+                    <span>
+                      <Badge>{translate('coding_rules.rule_template')}</Badge>
+                    </span>
+                  </Tooltip>
+                </li>
+              </>
+            )}
 
-              {rule.status !== 'READY' && (
-                <>
-                  <SeparatorCircleIcon aria-hidden as="li" />
-                  <li>
-                    <Badge variant="deleted">{translate('rules.status', rule.status)}</Badge>
-                  </li>
-                </>
-              )}
+            {rule.status !== 'READY' && (
+              <>
+                <SeparatorCircleIcon aria-hidden as="li" />
+                <li>
+                  <Badge variant="deleted">{translate('rules.status', rule.status)}</Badge>
+                </li>
+              </>
+            )}
 
-              {allTags.length > 0 && (
-                <>
-                  <SeparatorCircleIcon aria-hidden as="li" />
-                  <li>
-                    <TagsList
-                      allowUpdate={false}
-                      className="sw-body-xs"
-                      tagsClassName="sw-body-xs"
-                      tags={allTags}
-                    />
-                  </li>
-                </>
-              )}
-            </TextSubdued>
+            {allTags.length > 0 && (
+              <>
+                <SeparatorCircleIcon aria-hidden as="li" />
+                <li>
+                  <TagsList
+                    allowUpdate={false}
+                    className="sw-body-xs"
+                    tagsClassName="sw-body-xs"
+                    tags={allTags}
+                  />
+                </li>
+              </>
+            )}
+          </TextSubdued>
 
-            <div className="sw-flex sw-items-center">{this.renderActions()}</div>
-          </div>
+          <div className="sw-flex sw-items-center">{renderActions()}</div>
         </div>
-      </ListItemStyled>
-    );
-  }
+      </div>
+    </ListItemStyled>
+  );
 }
 
 const ListItemStyled = styled.li<{ selected: boolean }>`
index 5a4659072ffb2273ec37e623c07b037a573ece84..4950906bd5aae726e95abb5505b5c1b8f7758e49 100644 (file)
@@ -36,7 +36,7 @@ import {
   SoftwareImpactSeverity,
   SoftwareQuality,
 } from '../../types/clean-code-taxonomy';
-import { Dict, RuleInheritance } from '../../types/types';
+import { Dict, RuleActivation, RuleInheritance } from '../../types/types';
 
 export interface Query {
   activation: boolean | undefined;
@@ -72,14 +72,9 @@ export type Facets = { [F in FacetKey]?: Facet };
 
 export type OpenFacets = Dict<boolean>;
 
-export interface Activation {
-  inherit: RuleInheritance;
-  severity: string;
-}
-
 export interface Actives {
   [rule: string]: {
-    [profile: string]: Activation;
+    [profile: string]: RuleActivation;
   };
 }
 
index c81db6ff094e326220e3b529f78586d1b8244371..0e59dce516c4f053451cfdccf17b548a9b4e98ac 100644 (file)
@@ -147,9 +147,10 @@ const selectors = {
   // Rule Quality Profiles
   qpLink: (name: string) => byRole('link', { name }),
   activateButton: byRole('button', { name: 'coding_rules.activate' }),
-  deactivateButton: byRole('button', { name: 'coding_rules.deactivate' }),
+  deactivateButton: byRole('button', { name: /coding_rules.deactivate_in_quality_profile/ }),
   oldSeveritySelect: byRole('combobox', { name: 'severity' }),
   qualityProfileSelect: byRole('combobox', { name: 'coding_rules.quality_profile' }),
+  selectValue: byText(/severity\./),
   activateQPDialog: byRole('dialog', { name: 'coding_rules.activate_in_quality_profile' }),
   changeButton: (profile: string) =>
     byRole('button', { name: `coding_rules.change_details_x.${profile}` }),