]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23261 Custom software quality severities
authorViktor Vorona <viktor.vorona@sonarsource.com>
Fri, 4 Oct 2024 11:00:41 +0000 (13:00 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 16 Oct 2024 20:03:00 +0000 (20:03 +0000)
14 files changed:
server/sonar-web/design-system/src/components/input/FormField.tsx
server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
server/sonar-web/src/main/js/api/mocks/data/rules.ts
server/sonar-web/src/main/js/api/quality-profiles.ts
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/SeveritySelect.tsx
server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx
server/sonar-web/src/main/js/apps/quality-profiles/changelog/SoftwareImpactChange.tsx
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogContainer-it.tsx
server/sonar-web/src/main/js/helpers/doc-links.ts
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/types/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index be9a4afa4529a363e51820a9c4e6c34559b32120..f40334f033962b83ddc1da7962e3f00278bc4ea9 100644 (file)
@@ -31,6 +31,7 @@ interface Props {
   children: ReactNode;
   className?: string;
   description?: string | ReactNode;
+  disabled?: boolean;
   help?: ReactNode;
   htmlFor?: string;
   id?: string;
@@ -44,6 +45,7 @@ export function FormField({
   children,
   className,
   description,
+  disabled,
   help,
   id,
   required,
@@ -56,7 +58,7 @@ export function FormField({
   return (
     <FieldWrapper className={className} id={id}>
       <Highlight className="sw-mb-2 sw-flex sw-items-center sw-gap-2">
-        <StyledLabel aria-label={ariaLabel} htmlFor={htmlFor} title={title}>
+        <StyledLabel aria-label={ariaLabel} disabled={disabled} htmlFor={htmlFor} title={title}>
           {label}
           {required && (
             <RequiredIcon aria-label={requiredAriaLabel ?? 'required'} className="sw-ml-1" />
@@ -74,8 +76,9 @@ export function FormField({
 
 // This is needed to prevent the target input/button from being focused
 // when clicking/hovering on the label. More info https://stackoverflow.com/questions/9098581/why-is-hover-for-input-triggered-on-corresponding-label-in-css
-const StyledLabel = styled(Label)`
+const StyledLabel = styled(Label)<{ disabled?: boolean }>`
   pointer-events: none;
+  color: ${({ disabled }) => (disabled ? 'var(--echoes-color-text-disabled)' : 'inherit')};
 `;
 
 const FieldWrapper = styled.div`
index 7c869ba07c0ec88ed8bf928cd1ce9db8b33899ad..3235f698a0c3a1df29e16280ed7909b247fb9493 100644 (file)
@@ -31,8 +31,9 @@ import {
   mockRuleActivation,
   mockRuleRepository,
 } from '../../helpers/testMocks';
+import { SoftwareImpactSeverity, SoftwareQuality } from '../../types/clean-code-taxonomy';
 import { RuleRepository, SearchRulesResponse } from '../../types/coding-rules';
-import { RawIssuesResponse } from '../../types/issues';
+import { IssueSeverity, RawIssuesResponse } from '../../types/issues';
 import { RuleStatus, SearchRulesQuery } from '../../types/rules';
 import { SecurityStandard } from '../../types/security';
 import {
@@ -108,6 +109,22 @@ const FACET_RULE_MAP: { [key: string]: keyof Rule } = {
   tags: 'tags',
 };
 
+const MQRtoStandardSeverityMap = {
+  [SoftwareImpactSeverity.Info]: IssueSeverity.Info,
+  [SoftwareImpactSeverity.Low]: IssueSeverity.Minor,
+  [SoftwareImpactSeverity.Medium]: IssueSeverity.Major,
+  [SoftwareImpactSeverity.High]: IssueSeverity.Critical,
+  [SoftwareImpactSeverity.Blocker]: IssueSeverity.Blocker,
+};
+
+const StandardtoMQRSeverityMap = {
+  [IssueSeverity.Info]: SoftwareImpactSeverity.Info,
+  [IssueSeverity.Minor]: SoftwareImpactSeverity.Low,
+  [IssueSeverity.Major]: SoftwareImpactSeverity.Medium,
+  [IssueSeverity.Critical]: SoftwareImpactSeverity.High,
+  [IssueSeverity.Blocker]: SoftwareImpactSeverity.Blocker,
+};
+
 export const RULE_TAGS_MOCK = ['awesome', 'cute', 'nice'];
 
 export default class CodingRulesServiceMock {
@@ -568,6 +585,12 @@ export default class CodingRulesServiceMock {
       activation.inherit = 'INHERITED';
       activation.prioritizedRule = parentActivation?.prioritizedRule ?? false;
       activation.severity = parentActivation?.severity ?? 'MAJOR';
+      activation.impacts = parentActivation?.impacts ?? [
+        {
+          softwareQuality: SoftwareQuality.Maintainability,
+          severity: SoftwareImpactSeverity.Medium,
+        },
+      ];
       activation.params = parentParams;
 
       return this.reply(undefined);
@@ -582,16 +605,53 @@ export default class CodingRulesServiceMock {
         currentActivation.params,
         Object.entries(data.params ?? {}).map(([key, value]) => ({ key, value })),
       ) &&
-      currentActivation.severity === data.severity &&
-      currentActivation.prioritizedRule === data.prioritizedRule
+      (!data.severity || currentActivation.severity === data.severity) &&
+      currentActivation.prioritizedRule === data.prioritizedRule &&
+      (!data.softwareQualityImpact ||
+        isEqual(
+          currentActivation.impacts,
+          Object.entries(data.softwareQualityImpact).map(([softwareQuality, severity]) => ({
+            softwareQuality,
+            severity,
+          })),
+        ))
     ) {
       return this.reply(undefined);
     }
 
+    const ruleImpacts = this.rules.find((r) => r.key === data.rule)?.impacts ?? [];
+    const inheritedImpacts =
+      this.rulesActivations[data.rule]?.find(({ qProfile }) => qProfile === data.key)?.impacts ??
+      [];
+    const severity = data.softwareQualityImpact
+      ? MQRtoStandardSeverityMap[data.softwareQualityImpact[SoftwareQuality.Maintainability]]
+      : data.severity;
+    const impacts = data.severity
+      ? [
+          ...ruleImpacts.filter(
+            (impact) => !inheritedImpacts.some((i) => i.softwareQuality === impact.softwareQuality),
+          ),
+          ...inheritedImpacts.filter(
+            (impact) => impact.softwareQuality !== SoftwareQuality.Maintainability,
+          ),
+          {
+            softwareQuality: SoftwareQuality.Maintainability,
+            severity:
+              StandardtoMQRSeverityMap[data.severity as keyof typeof StandardtoMQRSeverityMap],
+          },
+        ]
+      : Object.entries(data.softwareQualityImpact ?? {}).map(
+          ([softwareQuality, severity]: [SoftwareQuality, SoftwareImpactSeverity]) => ({
+            softwareQuality,
+            severity,
+          }),
+        );
+
     const nextActivations = [
       mockRuleActivation({
         qProfile: data.key,
-        severity: data.severity,
+        severity,
+        impacts,
         prioritizedRule: data.prioritizedRule,
         params: Object.entries(data.params ?? {}).map(([key, value]) => ({ key, value })),
       }),
@@ -604,7 +664,8 @@ export default class CodingRulesServiceMock {
       ...inheritingProfiles.map((profile) =>
         mockRuleActivation({
           qProfile: profile.key,
-          severity: data.severity,
+          severity,
+          impacts,
           prioritizedRule: data.prioritizedRule,
           inherit: 'INHERITED',
           params: Object.entries(data.params ?? {}).map(([key, value]) => ({ key, value })),
index 12d827391a8f9973e8d4061cd94b3ab93b92c7d3..b32fc913940adea297eade82bda0b08aad658ad7 100644 (file)
@@ -101,6 +101,12 @@ export function mockRuleDetailsList() {
         { key: '1', type: 'TEXT', htmlDesc: 'html description for key 1' },
         { key: '2', type: 'NUMBER', defaultValue: 'default value for key 2' },
       ],
+      impacts: [
+        {
+          softwareQuality: SoftwareQuality.Maintainability,
+          severity: SoftwareImpactSeverity.Medium,
+        },
+      ],
     }),
     mockRuleDetails({
       key: RULE_2,
@@ -157,6 +163,12 @@ export function mockRuleDetailsList() {
           content: resourceContent,
         },
       ],
+      impacts: [
+        {
+          softwareQuality: SoftwareQuality.Maintainability,
+          severity: SoftwareImpactSeverity.Medium,
+        },
+      ],
     }),
     mockRuleDetails({
       key: RULE_6,
@@ -166,6 +178,16 @@ export function mockRuleDetailsList() {
       name: 'Bad Python rule',
       isExternal: true,
       descriptionSections: undefined,
+      impacts: [
+        {
+          softwareQuality: SoftwareQuality.Maintainability,
+          severity: SoftwareImpactSeverity.Medium,
+        },
+        {
+          softwareQuality: SoftwareQuality.Security,
+          severity: SoftwareImpactSeverity.Low,
+        },
+      ],
     }),
     mockRuleDetails({
       key: RULE_7,
@@ -194,6 +216,12 @@ export function mockRuleDetailsList() {
           content: resourceContent,
         },
       ],
+      impacts: [
+        {
+          softwareQuality: SoftwareQuality.Maintainability,
+          severity: SoftwareImpactSeverity.Low,
+        },
+      ],
     }),
     mockRuleDetails({
       key: RULE_8,
@@ -213,6 +241,9 @@ export function mockRuleDetailsList() {
       key: RULE_9,
       type: 'BUG',
       severity: 'MINOR',
+      impacts: [
+        { softwareQuality: SoftwareQuality.Reliability, severity: SoftwareImpactSeverity.Low },
+      ],
       lang: 'py',
       langName: 'Python',
       tags: ['awesome', 'cute'],
@@ -240,6 +271,16 @@ export function mockRuleDetailsList() {
           content: resourceContent,
         },
       ],
+      impacts: [
+        {
+          softwareQuality: SoftwareQuality.Maintainability,
+          severity: SoftwareImpactSeverity.Low,
+        },
+        {
+          softwareQuality: SoftwareQuality.Reliability,
+          severity: SoftwareImpactSeverity.High,
+        },
+      ],
       educationPrinciples: ['defense_in_depth', 'never_trust_user_input'],
     }),
     mockRuleDetails({
@@ -266,7 +307,19 @@ export function mockRuleDetailsList() {
 export function mockRulesActivationsInQP() {
   return {
     [RULE_1]: [mockRuleActivation({ qProfile: QP_1 }), mockRuleActivation({ qProfile: QP_6 })],
-    [RULE_7]: [mockRuleActivation({ qProfile: QP_2 })],
+    [RULE_7]: [
+      mockRuleActivation({
+        qProfile: QP_2,
+        impacts: [
+          {
+            softwareQuality: SoftwareQuality.Maintainability,
+            severity: SoftwareImpactSeverity.Medium,
+          },
+          { softwareQuality: SoftwareQuality.Reliability, severity: SoftwareImpactSeverity.High },
+          { softwareQuality: SoftwareQuality.Security, severity: SoftwareImpactSeverity.High },
+        ],
+      }),
+    ],
     [RULE_8]: [mockRuleActivation({ qProfile: QP_2 })],
     [RULE_9]: [
       mockRuleActivation({
@@ -276,11 +329,41 @@ export function mockRulesActivationsInQP() {
           { key: '2', value: 'default value for key 2' },
         ],
         inherit: 'INHERITED',
+        impacts: [
+          { softwareQuality: SoftwareQuality.Reliability, severity: SoftwareImpactSeverity.Medium },
+        ],
       }),
     ],
     [RULE_10]: [
-      mockRuleActivation({ qProfile: QP_2, inherit: 'OVERRIDES', prioritizedRule: true }),
-      mockRuleActivation({ qProfile: QP_2_Parent, severity: 'MINOR' }),
+      mockRuleActivation({
+        qProfile: QP_2,
+        inherit: 'OVERRIDES',
+        impacts: [
+          {
+            softwareQuality: SoftwareQuality.Maintainability,
+            severity: SoftwareImpactSeverity.Medium,
+          },
+          {
+            softwareQuality: SoftwareQuality.Reliability,
+            severity: SoftwareImpactSeverity.Info,
+          },
+        ],
+        prioritizedRule: true,
+      }),
+      mockRuleActivation({
+        qProfile: QP_2_Parent,
+        severity: 'MINOR',
+        impacts: [
+          {
+            softwareQuality: SoftwareQuality.Maintainability,
+            severity: SoftwareImpactSeverity.Low,
+          },
+          {
+            softwareQuality: SoftwareQuality.Reliability,
+            severity: SoftwareImpactSeverity.Blocker,
+          },
+        ],
+      }),
     ],
     [RULE_11]: [
       mockRuleActivation({ qProfile: QP_4 }),
index d2b2898e3308e86169a7e03d31702844d6a990fd..124b2b0abe060f0fad8db9183f1c172bb46a34ca 100644 (file)
@@ -23,7 +23,12 @@ import { getJSON } from '~sonar-aligned/helpers/request';
 import { Exporter, ProfileChangelogEvent } from '../apps/quality-profiles/types';
 import { csvEscape } from '../helpers/csv';
 import { RequestData, post, postJSON } from '../helpers/request';
-import { CleanCodeAttributeCategory, SoftwareImpact } from '../types/clean-code-taxonomy';
+import {
+  CleanCodeAttributeCategory,
+  SoftwareImpact,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
+} from '../types/clean-code-taxonomy';
 import { Dict, Paging, ProfileInheritanceDetails, UserSelected } from '../types/types';
 
 export interface ProfileActions {
@@ -322,17 +327,25 @@ export function bulkDeactivateRules(data: BulkActivateParameters) {
 
 export interface ActivateRuleParameters {
   key: string;
-  params?: Dict<string>;
+  params?: Record<string, string>;
   prioritizedRule?: boolean;
   reset?: boolean;
   rule: string;
   severity?: string;
+  softwareQualityImpact?: Record<SoftwareQuality, SoftwareImpactSeverity>;
 }
 
 export function activateRule(data: ActivateRuleParameters) {
   const params =
     data.params && map(data.params, (value, key) => `${key}=${csvEscape(value)}`).join(';');
-  return post('/api/qualityprofiles/activate_rule', { ...data, params }).catch(throwGlobalError);
+  const softwareQualityImpact =
+    data.softwareQualityImpact &&
+    map(data.softwareQualityImpact, (value, key) => `${key}=${value}`).join(';');
+  return post('/api/qualityprofiles/activate_rule', {
+    ...data,
+    params,
+    softwareQualityImpact,
+  }).catch(throwGlobalError);
 }
 
 export interface DeactivateRuleParameters {
index 7e559096a1f2fc9c93463c66ff75b8e4b7cdce24..7b59e1ef06b8c416acdfd397d3885d1fd849d644 100644 (file)
@@ -20,7 +20,7 @@
 import { fireEvent, screen, within } from '@testing-library/react';
 import CodingRulesServiceMock, { RULE_TAGS_MOCK } from '../../../api/mocks/CodingRulesServiceMock';
 import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
-import { QP_2, RULE_1, RULE_10, RULE_9 } from '../../../api/mocks/data/ids';
+import { QP_2, RULE_1, RULE_10, RULE_7, RULE_9 } from '../../../api/mocks/data/ids';
 import {
   IMPACT_SEVERITIES,
   ISSUE_TYPES,
@@ -244,11 +244,11 @@ describe('Rules app list', () => {
 
       // Filter by software quality
       await user.click(ui.facetItem('software_quality.MAINTAINABILITY').get());
-      expect(ui.getAllRuleListItems()).toHaveLength(9);
+      expect(ui.getAllRuleListItems()).toHaveLength(8);
 
       // Filter by severity
       await user.click(ui.facetItem(/severity_impact.HIGH/).get());
-      expect(ui.getAllRuleListItems()).toHaveLength(8);
+      expect(ui.getAllRuleListItems()).toHaveLength(4);
     });
 
     it('filter by type and severity in standard mode', async () => {
@@ -408,132 +408,363 @@ describe('Rules app list', () => {
     });
   });
 
-  it('can activate/change/deactivate specific rule for quality profile', async () => {
-    const { ui, user } = getPageObjects();
-    rulesHandler.setIsAdmin();
-    renderCodingRulesApp(mockLoggedInUser(), undefined, [Feature.PrioritizedRules]);
-    await ui.appLoaded();
+  describe('old severity', () => {
+    it('can activate/change/deactivate specific rule for quality profile', async () => {
+      const { ui, user } = getPageObjects();
+      rulesHandler.setIsAdmin();
+      renderCodingRulesApp(mockLoggedInUser(), undefined, [Feature.PrioritizedRules]);
+      await ui.appLoaded();
 
-    await user.click(ui.qpFacet.get());
-    await user.click(ui.facetItem('QP Bar Python').get());
+      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);
 
-    // Only 4 rules are activated in selected QP
-    expect(ui.getAllRuleListItems()).toHaveLength(4);
+      // Switch to inactive rules
+      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();
 
-    // Switch to inactive rules
-    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.oldSeveritySelect.get(ui.activateQPDialog.get())).toHaveValue(
+        'coding_rules.custom_severity.severity_with_recommended.severity.MAJOR',
+      );
+      expect(ui.prioritizedSwitch.get(ui.activateQPDialog.get())).not.toBeChecked();
+      await user.click(ui.oldSeveritySelect.get());
+      await user.click(byRole('option', { name: 'severity.MINOR' }).get());
+      expect(ui.notRecommendedSeverity.get()).toBeInTheDocument();
+      expect(ui.notRecommendedSeverity.get()).toHaveTextContent('severity.MAJOR');
+
+      await user.click(ui.prioritizedSwitch.get(ui.activateQPDialog.get()));
+      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.oldSeveritySelect.get(ui.changeQPDialog.get())).toHaveValue('severity.MINOR');
+      expect(ui.notRecommendedSeverity.get()).toBeInTheDocument();
+      expect(ui.notRecommendedSeverity.get()).toHaveTextContent('severity.MAJOR');
+      expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).toBeChecked();
+      await user.click(ui.oldSeveritySelect.get());
+      await user.click(byRole('option', { name: 'severity.BLOCKER' }).get());
+      await user.click(ui.prioritizedSwitch.get(ui.changeQPDialog.get()));
+      expect(ui.notRecommendedSeverity.get()).toBeInTheDocument();
+      expect(ui.notRecommendedSeverity.get()).toHaveTextContent('severity.MAJOR');
+      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.oldSeveritySelect.get(ui.changeQPDialog.get())).toHaveValue('severity.BLOCKER');
+      expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).not.toBeChecked();
+      expect(ui.notRecommendedSeverity.get()).toBeInTheDocument();
+      expect(ui.notRecommendedSeverity.get()).toHaveTextContent('severity.MAJOR');
+      await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
+
+      // Deactivate activated rule
+      await user.click(ui.deactivateButton.get());
+      await user.click(ui.yesButton.get());
+      expect(ui.deactivateButton.query()).not.toBeInTheDocument();
+      expect(ui.activateButton.getAll()).toHaveLength(2);
+    });
 
-    // Activate Rule for qp
-    await user.click(ui.activateButton.getAll()[0]);
-    expect(ui.selectValue.get(ui.activateQPDialog.get())).toHaveTextContent('severity.MAJOR');
-    expect(ui.prioritizedSwitch.get(ui.activateQPDialog.get())).not.toBeChecked();
-    await user.click(ui.oldSeveritySelect.get());
-    await user.click(byRole('option', { name: 'severity.MINOR' }).get());
+    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(), undefined, [Feature.PrioritizedRules]);
+      await ui.appLoaded();
 
-    await user.click(ui.prioritizedSwitch.get(ui.activateQPDialog.get()));
-    await user.click(ui.activateButton.get(ui.activateQPDialog.get()));
+      await user.click(ui.qpFacet.get());
+      await user.click(ui.facetItem('QP Bar Python').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');
-    expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).toBeChecked();
-    await user.click(ui.oldSeveritySelect.get());
-    await user.click(byRole('option', { name: 'severity.BLOCKER' }).get());
-    await user.click(ui.prioritizedSwitch.get(ui.changeQPDialog.get()));
-    await user.click(ui.saveButton.get(ui.changeQPDialog.get()));
+      // Only 4 rules are activated in selected QP
+      expect(ui.getAllRuleListItems()).toHaveLength(4);
 
-    // Check that new severity is saved
-    await user.click(ui.changeButton('QP Bar').get());
-    expect(ui.selectValue.get(ui.changeQPDialog.get())).toHaveTextContent('severity.BLOCKER');
-    expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).not.toBeChecked();
-    await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
+      // 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();
 
-    // Deactivate activated rule
-    await user.click(ui.deactivateButton.get());
-    await user.click(ui.yesButton.get());
-    expect(ui.deactivateButton.query()).not.toBeInTheDocument();
-    expect(ui.activateButton.getAll()).toHaveLength(2);
+      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.oldSeveritySelect.get(ui.changeQPDialog.get())).toHaveValue('severity.MAJOR');
+      expect(ui.notRecommendedSeverity.get()).toBeInTheDocument();
+      expect(ui.notRecommendedSeverity.get()).toHaveTextContent('severity.MINOR');
+      expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).toBeChecked();
+      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.oldSeveritySelect.get(ui.changeQPDialog.get())).toHaveValue(
+        'coding_rules.custom_severity.severity_with_recommended.severity.MINOR',
+      );
+      expect(ui.notRecommendedSeverity.query()).not.toBeInTheDocument();
+      expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).not.toBeChecked();
+      await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
+    });
+
+    it('should not make rule overriden if no changes were done', async () => {
+      const { ui, user } = getPageObjects();
+      settingsHandler.set(SettingsKey.QPAdminCanDisableInheritedRules, 'false');
+      rulesHandler.setIsAdmin();
+      renderCodingRulesApp(mockLoggedInUser(), undefined, [Feature.PrioritizedRules]);
+      await ui.appLoaded();
+
+      await user.click(ui.qpFacet.get());
+      await user.click(ui.facetItem('QP Bar Python').get());
+
+      // filter out everything except INHERITED rule
+      await user.type(ui.searchInput.get(), RULE_9);
+
+      // Only 1 rule left after search
+      expect(ui.getAllRuleListItems()).toHaveLength(1);
+      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.oldSeveritySelect.get(ui.changeQPDialog.get())).toHaveValue('severity.MAJOR');
+      expect(ui.notRecommendedSeverity.get()).toBeInTheDocument();
+      expect(ui.notRecommendedSeverity.get()).toHaveTextContent('severity.MINOR');
+      expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).not.toBeChecked();
+      await user.click(ui.saveButton.get(ui.changeQPDialog.get()));
+
+      expect(ui.revertToParentDefinitionButton.query()).not.toBeInTheDocument();
+    });
   });
 
-  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(), undefined, [Feature.PrioritizedRules]);
-    await ui.appLoaded();
+  describe('new severity', () => {
+    it('can activate/change specific rule with multiple impacts for quality profile', async () => {
+      const { ui, user } = getPageObjects();
+      rulesHandler.setIsAdmin();
+      renderCodingRulesApp(mockLoggedInUser(), undefined, []);
+      await ui.appLoaded();
 
-    await user.click(ui.qpFacet.get());
-    await user.click(ui.facetItem('QP Bar Python').get());
+      await user.click(ui.qpFacet.get());
+      await user.click(ui.facetItem('QP Bar Python').get());
+      await user.click(ui.qpInactiveRadio.get(ui.facetItem('QP Bar Python').get()));
+
+      // Activate Rule for qp
+      await user.click(ui.activateButton.getAll()[1]);
 
-    // Only 4 rules are activated in selected QP
-    expect(ui.getAllRuleListItems()).toHaveLength(4);
+      await user.click(ui.mqrSwitch.get(ui.activateQPDialog.get()));
 
-    // 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();
+      expect(ui.newSeveritySelect(SoftwareQuality.Maintainability).get()).toHaveValue(
+        'coding_rules.custom_severity.severity_with_recommended.severity_impact.MEDIUM',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Security).get()).toHaveValue(
+        'coding_rules.custom_severity.severity_with_recommended.severity_impact.LOW',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Reliability).get()).toBeDisabled();
+      await user.click(ui.newSeveritySelect(SoftwareQuality.Maintainability).get());
+      await user.click(byRole('option', { name: 'severity_impact.LOW' }).get());
+      await user.click(ui.newSeveritySelect(SoftwareQuality.Security).get());
+      await user.click(byRole('option', { name: 'severity_impact.MEDIUM' }).get());
+      expect(ui.notRecommendedSeverity.getAll()).toHaveLength(2);
+      expect(ui.notRecommendedSeverity.getAt(0)).toHaveTextContent('severity_impact.LOW');
+      expect(ui.notRecommendedSeverity.getAt(1)).toHaveTextContent('severity_impact.MEDIUM');
+
+      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);
+
+      await user.click(ui.changeButton('QP Bar').get());
+      expect(ui.oldSeveritySelect.get(ui.changeQPDialog.get())).toHaveValue('severity.MINOR');
+      expect(ui.notRecommendedSeverity.get()).toBeInTheDocument();
+      expect(ui.notRecommendedSeverity.get()).toHaveTextContent('severity.MAJOR');
+      await user.click(ui.mqrSwitch.get());
+
+      expect(ui.newSeveritySelect(SoftwareQuality.Maintainability).get()).toHaveValue(
+        'severity_impact.LOW',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Security).get()).toHaveValue(
+        'severity_impact.MEDIUM',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Reliability).get()).toBeDisabled();
+      expect(ui.notRecommendedSeverity.getAll()).toHaveLength(2);
+      expect(ui.notRecommendedSeverity.getAt(0)).toHaveTextContent('severity_impact.LOW');
+      expect(ui.notRecommendedSeverity.getAt(1)).toHaveTextContent('severity_impact.MEDIUM');
+      await user.click(ui.newSeveritySelect(SoftwareQuality.Security).get());
+      await user.click(byRole('option', { name: 'severity_impact.BLOCKER' }).get());
+      expect(ui.notRecommendedSeverity.getAll()).toHaveLength(2);
+      expect(ui.notRecommendedSeverity.getAt(0)).toHaveTextContent('severity_impact.LOW');
+      expect(ui.notRecommendedSeverity.getAt(1)).toHaveTextContent('severity_impact.MEDIUM');
+      await user.click(ui.saveButton.get(ui.changeQPDialog.get()));
+
+      // Check that new severity is saved
+      await user.click(ui.changeButton('QP Bar').get());
+      await user.click(ui.mqrSwitch.get());
+      expect(ui.oldSeveritySelect.get(ui.changeQPDialog.get())).toHaveValue('severity.MINOR');
+      expect(ui.notRecommendedSeverity.get()).toBeInTheDocument();
+      expect(ui.notRecommendedSeverity.get()).toHaveTextContent('severity.MAJOR');
+      await user.click(ui.mqrSwitch.get());
+
+      expect(ui.newSeveritySelect(SoftwareQuality.Maintainability).get()).toHaveValue(
+        'severity_impact.LOW',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Security).get()).toHaveValue(
+        'severity_impact.BLOCKER',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Reliability).get()).toBeDisabled();
 
-    await user.type(ui.searchInput.get(), RULE_10);
+      await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
+    });
 
-    // Only 1 rule left after search
-    expect(ui.getAllRuleListItems()).toHaveLength(1);
-    expect(ui.revertToParentDefinitionButton.get()).toBeInTheDocument();
-    expect(ui.changeButton('QP Bar').get()).toBeInTheDocument();
+    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(), undefined, [Feature.PrioritizedRules]);
+      await ui.appLoaded();
 
-    // Check that severity is reflected correctly
-    await user.click(ui.changeButton('QP Bar').get());
-    expect(ui.selectValue.get(ui.changeQPDialog.get())).toHaveTextContent('severity.MAJOR');
-    expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).toBeChecked();
-    await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
+      await user.click(ui.qpFacet.get());
+      await user.click(ui.facetItem('QP Bar Python').get());
 
-    await user.click(ui.revertToParentDefinitionButton.get());
-    await user.click(ui.yesButton.get());
+      // Only 4 rules are activated in selected QP
+      expect(ui.getAllRuleListItems()).toHaveLength(4);
 
-    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();
+      // 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();
 
-    // Check that severity is reflected correctly
-    await user.click(ui.changeButton('QP Bar').get());
-    expect(ui.selectValue.get(ui.changeQPDialog.get())).toHaveTextContent('severity.MINOR');
-    expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).not.toBeChecked();
-    await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
-  });
+      await user.type(ui.searchInput.get(), RULE_10);
 
-  it('should not make rule overriden if no changes were done', async () => {
-    const { ui, user } = getPageObjects();
-    settingsHandler.set(SettingsKey.QPAdminCanDisableInheritedRules, 'false');
-    rulesHandler.setIsAdmin();
-    renderCodingRulesApp(mockLoggedInUser(), undefined, [Feature.PrioritizedRules]);
-    await ui.appLoaded();
+      // 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());
+      await user.click(ui.mqrSwitch.get(ui.changeQPDialog.get()));
+      expect(ui.newSeveritySelect(SoftwareQuality.Maintainability).get()).toHaveValue(
+        'severity_impact.MEDIUM',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Reliability).get()).toHaveValue(
+        'severity_impact.INFO',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Security).get()).toBeDisabled();
+      expect(ui.notRecommendedSeverity.getAll()).toHaveLength(2);
+      expect(ui.notRecommendedSeverity.getAt(0)).toHaveTextContent('severity_impact.HIGH');
+      expect(ui.notRecommendedSeverity.getAt(1)).toHaveTextContent('severity_impact.LOW');
+      expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).toBeChecked();
+      await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
 
-    await user.click(ui.qpFacet.get());
-    await user.click(ui.facetItem('QP Bar Python').get());
+      await user.click(ui.revertToParentDefinitionButton.get());
+      await user.click(ui.yesButton.get());
 
-    // filter out everything except INHERITED rule
-    await user.type(ui.searchInput.get(), RULE_9);
+      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.newSeveritySelect(SoftwareQuality.Maintainability).get()).toHaveValue(
+        'coding_rules.custom_severity.severity_with_recommended.severity_impact.LOW',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Reliability).get()).toHaveValue(
+        'severity_impact.BLOCKER',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Security).get()).toBeDisabled();
+      expect(ui.notRecommendedSeverity.getAll()).toHaveLength(1);
+      expect(ui.notRecommendedSeverity.getAt(0)).toHaveTextContent('severity_impact.HIGH');
+      expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).not.toBeChecked();
+      await user.click(ui.cancelButton.get(ui.changeQPDialog.get()));
+    });
 
-    // Only 1 rule left after search
-    expect(ui.getAllRuleListItems()).toHaveLength(1);
-    expect(ui.deactivateButton.get()).toBeInTheDocument();
-    expect(ui.deactivateButton.get()).toBeDisabled();
-    expect(ui.changeButton('QP Bar').get()).toBeInTheDocument();
+    it('should not make rule overriden if no changes were done', async () => {
+      const { ui, user } = getPageObjects();
+      settingsHandler.set(SettingsKey.QPAdminCanDisableInheritedRules, 'false');
+      rulesHandler.setIsAdmin();
+      renderCodingRulesApp(mockLoggedInUser(), undefined, []);
+      await ui.appLoaded();
 
-    // Check that severity is reflected correctly
-    await user.click(ui.changeButton('QP Bar').get());
-    expect(ui.selectValue.get(ui.changeQPDialog.get())).toHaveTextContent('severity.MAJOR');
-    expect(ui.prioritizedSwitch.get(ui.changeQPDialog.get())).not.toBeChecked();
-    await user.click(ui.saveButton.get(ui.changeQPDialog.get()));
+      await user.click(ui.qpFacet.get());
+      await user.click(ui.facetItem('QP Bar Python').get());
+
+      // filter out everything except INHERITED rule
+      await user.type(ui.searchInput.get(), RULE_9);
+      expect(ui.changeButton('QP Bar').get()).toBeInTheDocument();
+
+      // Check that severity is reflected correctly
+      await user.click(ui.changeButton('QP Bar').get());
+      await user.click(ui.mqrSwitch.get(ui.changeQPDialog.get()));
+      expect(ui.newSeveritySelect(SoftwareQuality.Reliability).get()).toHaveValue(
+        'severity_impact.MEDIUM',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Security).get()).toBeDisabled();
+      expect(ui.newSeveritySelect(SoftwareQuality.Maintainability).get()).toBeDisabled();
+      expect(ui.notRecommendedSeverity.get()).toBeInTheDocument();
+      expect(ui.notRecommendedSeverity.get()).toHaveTextContent('severity_impact.LOW');
+      await user.click(ui.saveButton.get(ui.changeQPDialog.get()));
+
+      expect(ui.revertToParentDefinitionButton.query()).not.toBeInTheDocument();
+    });
 
-    expect(ui.revertToParentDefinitionButton.query()).not.toBeInTheDocument();
+    it('should ignore excessive activation impacts', async () => {
+      const { ui, user } = getPageObjects();
+      settingsHandler.set(SettingsKey.QPAdminCanDisableInheritedRules, 'false');
+      rulesHandler.setIsAdmin();
+      renderCodingRulesApp(mockLoggedInUser(), undefined, []);
+      await ui.appLoaded();
+
+      await user.click(ui.qpFacet.get());
+      await user.click(ui.facetItem('QP Bar Python').get());
+
+      await user.type(ui.searchInput.get(), RULE_7);
+      expect(ui.changeButton('QP Bar').get()).toBeInTheDocument();
+
+      await user.click(ui.changeButton('QP Bar').get());
+      await user.click(ui.mqrSwitch.get(ui.changeQPDialog.get()));
+      expect(ui.newSeveritySelect(SoftwareQuality.Maintainability).get()).toHaveValue(
+        'severity_impact.MEDIUM',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Security).get()).toBeDisabled();
+      expect(ui.newSeveritySelect(SoftwareQuality.Reliability).get()).toBeDisabled();
+      expect(ui.notRecommendedSeverity.get()).toBeInTheDocument();
+      expect(ui.notRecommendedSeverity.get()).toHaveTextContent('severity_impact.LOW');
+      await user.click(ui.newSeveritySelect(SoftwareQuality.Maintainability).get());
+      await user.click(
+        byRole('option', {
+          name: 'coding_rules.custom_severity.severity_with_recommended.severity_impact.LOW',
+        }).get(),
+      );
+      await user.click(ui.saveButton.get(ui.changeQPDialog.get()));
+
+      await user.click(ui.changeButton('QP Bar').get());
+      expect(ui.newSeveritySelect(SoftwareQuality.Maintainability).get()).toHaveValue(
+        'coding_rules.custom_severity.severity_with_recommended.severity_impact.LOW',
+      );
+      expect(ui.newSeveritySelect(SoftwareQuality.Security).get()).toBeDisabled();
+      expect(ui.newSeveritySelect(SoftwareQuality.Reliability).get()).toBeDisabled();
+      expect(ui.notRecommendedSeverity.query()).not.toBeInTheDocument();
+    });
   });
 
   it('should not show prioritized rule switcher if feature is not enabled', async () => {
index efa190f33f024fea6e425d172e5e57eb6b36be49..a06bb85d86d7e845651685722b22e47048a4e6ea 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { Button, ButtonVariety, Modal } from '@sonarsource/echoes-react';
+import { Button, ButtonVariety, Checkbox, Modal, Text } from '@sonarsource/echoes-react';
 import {
   FlagMessage,
   FormField,
@@ -32,15 +32,16 @@ import {
   Switch,
 } from 'design-system';
 import * as React from 'react';
-import { useIntl } from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
 import { Profile } from '../../../api/quality-profiles';
 import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures';
 import DocumentationLink from '../../../components/common/DocumentationLink';
 import { DocLink } from '../../../helpers/doc-links';
 import { useActivateRuleMutation } from '../../../queries/quality-profiles';
+import { SoftwareImpactSeverity, SoftwareQuality } from '../../../types/clean-code-taxonomy';
 import { Feature } from '../../../types/features';
 import { IssueSeverity } from '../../../types/issues';
-import { Dict, Rule, RuleActivation, RuleDetails } from '../../../types/types';
+import { Rule, RuleActivation, RuleDetails } from '../../../types/types';
 import { sortProfiles } from '../../quality-profiles/utils';
 import { SeveritySelect } from './SeveritySelect';
 
@@ -82,6 +83,10 @@ export default function ActivationFormModal(props: Readonly<Props>) {
   const [changedSeverity, setChangedSeverity] = React.useState<IssueSeverity | undefined>(
     undefined,
   );
+  const [changedImpactSeveritiesMap, setChangedImpactSeverities] = React.useState<
+    Map<SoftwareQuality, SoftwareImpactSeverity>
+  >(new Map());
+  const [isMQRMode, setIsMQRMode] = React.useState<boolean>(false);
 
   const profilesWithDepth = React.useMemo(() => {
     return getQualityProfilesWithDepth(profiles, rule.lang);
@@ -93,6 +98,13 @@ export default function ActivationFormModal(props: Readonly<Props>) {
   const params = changedParams ?? getRuleParams({ activation, rule });
   const severity =
     changedSeverity ?? ((activation ? activation.severity : rule.severity) as IssueSeverity);
+  const impacts = new Map<SoftwareQuality, SoftwareImpactSeverity>([
+    ...rule.impacts.map((impact) => [impact.softwareQuality, impact.severity] as const),
+    ...(activation?.impacts
+      ?.filter((impact) => rule.impacts.some((i) => i.softwareQuality === impact.softwareQuality))
+      .map((impact) => [impact.softwareQuality, impact.severity] as const) ?? []),
+    ...changedImpactSeveritiesMap,
+  ]);
   const profileOptions = profilesWithDepth.map((p) => ({ label: p.name, value: p }));
   const isCustomRule = !!(rule as RuleDetails).templateKey;
   const activeInAllProfiles = profilesWithDepth.length <= 0;
@@ -104,6 +116,7 @@ export default function ActivationFormModal(props: Readonly<Props>) {
       setChangedProfile(undefined);
       setChangedParams(undefined);
       setChangedSeverity(undefined);
+      setChangedImpactSeverities(new Map());
     }
   }, [isOpen]);
 
@@ -113,8 +126,11 @@ export default function ActivationFormModal(props: Readonly<Props>) {
       key: profile?.key ?? '',
       params,
       rule: rule.key,
-      severity,
+      severity: !isMQRMode ? severity : undefined,
       prioritizedRule,
+      softwareQualityImpact: isMQRMode
+        ? (Object.fromEntries(impacts) as Record<SoftwareQuality, SoftwareImpactSeverity>)
+        : undefined,
     };
     activateRule(data);
   };
@@ -194,47 +210,109 @@ export default function ActivationFormModal(props: Readonly<Props>) {
                 </div>
               }
             >
-              <label
+              <Checkbox
+                onCheck={(checked) => setChangedPrioritizedRule(!!checked)}
+                label={intl.formatMessage({ id: 'coding_rules.prioritized_rule.switch_label' })}
                 id="coding-rules-prioritized-rule"
-                className="sw-flex sw-items-center sw-gap-2"
-              >
-                <Switch
-                  onChange={setChangedPrioritizedRule}
-                  name={intl.formatMessage({ id: 'coding_rules.prioritized_rule.title' })}
-                  value={prioritizedRule}
-                />
-                <span className="sw-text-xs">
-                  {intl.formatMessage({ id: 'coding_rules.prioritized_rule.switch_label' })}
-                </span>
-              </label>
+                checked={prioritizedRule}
+              />
             </FormField>
           )}
 
-          <FormField
-            ariaLabel={intl.formatMessage({ id: 'severity_deprecated' })}
-            label={intl.formatMessage({ id: 'severity_deprecated' })}
-            htmlFor="coding-rules-severity-select"
-          >
-            <SeveritySelect
-              isDisabled={submitting}
-              onChange={({ value }: LabelValueSelectOption<IssueSeverity>) => {
-                setChangedSeverity(value);
-              }}
-              severity={severity}
-            />
-            <FlagMessage className="sw-mb-4 sw-mt-2" variant="info">
-              <div>
-                {intl.formatMessage({ id: 'coding_rules.severity_deprecated' })}
-                <DocumentationLink
-                  className="sw-ml-2 sw-whitespace-nowrap"
-                  to={DocLink.CleanCodeIntroduction}
-                >
-                  {intl.formatMessage({ id: 'learn_more' })}
-                </DocumentationLink>
-              </div>
-            </FlagMessage>
+          <FormField label="MQR Mode">
+            <Switch value={isMQRMode} onChange={setIsMQRMode} />
           </FormField>
 
+          {!isMQRMode && (
+            <>
+              <FormField label={intl.formatMessage({ id: 'coding_rules.custom_severity.title' })}>
+                <Text>
+                  <FormattedMessage
+                    id="coding_rules.custom_severity.description.standard"
+                    values={{
+                      link: (
+                        <DocumentationLink to={DocLink.RuleSeverity}>
+                          {intl.formatMessage({
+                            id: 'coding_rules.custom_severity.description.standard.link',
+                          })}
+                        </DocumentationLink>
+                      ),
+                    }}
+                  />
+                </Text>
+              </FormField>
+
+              <FormField
+                ariaLabel={intl.formatMessage({
+                  id: 'coding_rules.custom_severity.choose_severity',
+                })}
+                label={intl.formatMessage({ id: 'coding_rules.custom_severity.choose_severity' })}
+                htmlFor="coding-rules-custom-severity-select"
+              >
+                <SeveritySelect
+                  id="coding-rules-custom-severity-select"
+                  isDisabled={submitting}
+                  recommendedSeverity={rule.severity}
+                  onChange={(value: string) => {
+                    setChangedSeverity(value as IssueSeverity);
+                  }}
+                  severity={severity}
+                />
+              </FormField>
+            </>
+          )}
+
+          {isMQRMode && (
+            <>
+              <FormField label={intl.formatMessage({ id: 'coding_rules.custom_severity.title' })}>
+                <Text>
+                  <FormattedMessage
+                    id="coding_rules.custom_severity.description.mqr"
+                    values={{
+                      link: (
+                        <DocumentationLink to={DocLink.RuleSeverity}>
+                          {intl.formatMessage({
+                            id: 'coding_rules.custom_severity.description.mqr.link',
+                          })}
+                        </DocumentationLink>
+                      ),
+                    }}
+                  />
+                </Text>
+              </FormField>
+
+              {Object.values(SoftwareQuality).map((quality) => {
+                const impact = rule.impacts.find((impact) => impact.softwareQuality === quality);
+                const id = `coding-rules-custom-severity-${quality}-select`;
+                return (
+                  <FormField
+                    htmlFor={id}
+                    key={quality}
+                    disabled={!impact}
+                    ariaLabel={intl.formatMessage({ id: `software_quality.${quality}` })}
+                    label={intl.formatMessage({ id: `software_quality.${quality}` })}
+                  >
+                    <SeveritySelect
+                      id={id}
+                      impactSeverity
+                      isDisabled={submitting || !impact}
+                      recommendedSeverity={impact?.severity ?? ''}
+                      onChange={(value: string) => {
+                        setChangedImpactSeverities(
+                          new Map(changedImpactSeveritiesMap).set(
+                            quality,
+                            value as SoftwareImpactSeverity,
+                          ),
+                        );
+                      }}
+                      severity={impacts.get(quality) ?? ''}
+                    />
+                  </FormField>
+                );
+              })}
+            </>
+          )}
+
           {isCustomRule ? (
             <Note as="p" className="sw-my-4">
               {intl.formatMessage({ id: 'coding_rules.custom_rule.activation_notice' })}
@@ -308,7 +386,7 @@ function getRuleParams({
   activation?: RuleActivation;
   rule: RuleDetails | Rule;
 }) {
-  const params: Dict<string> = {};
+  const params: Record<string, string> = {};
   if (rule?.params) {
     for (const param of rule.params) {
       params[param.key] = param.defaultValue ?? '';
index 6aa134c43ccf0e11bb84a38ad59cc289022b8a43..2822ee68ab4b992e0eb20115b7654bdecc01b867 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { InputSelect, LabelValueSelectOption } from 'design-system';
+import { HelperText, Select } from '@sonarsource/echoes-react';
+import { isEmpty } from 'lodash';
 import * as React from 'react';
-import { OptionProps, SingleValueProps, components } from 'react-select';
-import SeverityHelper from '../../../components/shared/SeverityHelper';
+import { FormattedMessage, useIntl } from 'react-intl';
+import SeverityIcon from '../../../components/icon-mappers/SeverityIcon';
+import SoftwareImpactSeverityIcon from '../../../components/icon-mappers/SoftwareImpactSeverityIcon';
 import { SEVERITIES } from '../../../helpers/constants';
-import { translate } from '../../../helpers/l10n';
-import { IssueSeverity } from '../../../types/issues';
+import { SoftwareImpactSeverity } from '../../../types/clean-code-taxonomy';
 
 export interface SeveritySelectProps {
+  id: string;
+  impactSeverity?: boolean;
   isDisabled: boolean;
-  onChange: (value: LabelValueSelectOption<IssueSeverity>) => void;
+  onChange: (value: string) => void;
+  recommendedSeverity: string;
   severity: string;
 }
 
-function Option(props: Readonly<OptionProps<LabelValueSelectOption<IssueSeverity>, false>>) {
-  // For tests and a11y
-  props.innerProps.role = 'option';
-  props.innerProps['aria-selected'] = props.isSelected;
-
-  return (
-    <components.Option {...props}>
-      <SeverityHelper className="sw-flex sw-items-center" severity={props.data.value} />
-    </components.Option>
-  );
-}
-
-function SingleValue(
-  props: Readonly<SingleValueProps<LabelValueSelectOption<IssueSeverity>, false>>,
-) {
-  return (
-    <components.SingleValue {...props}>
-      <SeverityHelper className="sw-flex sw-items-center" severity={props.data.value} />
-    </components.SingleValue>
-  );
-}
-
 export function SeveritySelect(props: SeveritySelectProps) {
-  const { isDisabled, severity } = props;
-  const serverityOption = SEVERITIES.map((severity) => ({
-    label: translate('severity', severity),
-    value: severity,
-  }));
+  const { isDisabled, severity, recommendedSeverity, impactSeverity, id } = props;
+  const intl = useIntl();
+  const Icon = impactSeverity ? SoftwareImpactSeverityIcon : SeverityIcon;
+  const getSeverityTranslation = (severity: string) =>
+    impactSeverity
+      ? intl.formatMessage({ id: `severity_impact.${severity}` })
+      : intl.formatMessage({ id: `severity.${severity}` });
+  const serverityOption = (impactSeverity ? Object.values(SoftwareImpactSeverity) : SEVERITIES).map(
+    (severity) => ({
+      label:
+        severity === recommendedSeverity
+          ? intl.formatMessage(
+              { id: 'coding_rules.custom_severity.severity_with_recommended' },
+              { severity: getSeverityTranslation(severity) },
+            )
+          : getSeverityTranslation(severity),
+      value: severity,
+      prefix: <Icon severity={severity} aria-hidden />,
+    }),
+  );
 
   return (
-    <InputSelect
-      aria-label={translate('severity')}
-      inputId="coding-rules-severity-select"
-      isDisabled={isDisabled}
-      onChange={props.onChange}
-      components={{ Option, SingleValue }}
-      options={serverityOption}
-      isSearchable={false}
-      value={serverityOption.find((s) => s.value === severity)}
-    />
+    <>
+      <Select
+        id={id}
+        isDisabled={isDisabled}
+        onChange={props.onChange}
+        data={serverityOption}
+        isSearchable={false}
+        isNotClearable
+        placeholder={
+          isDisabled && !isEmpty(severity) ? intl.formatMessage({ id: 'not_impacted' }) : undefined
+        }
+        value={severity}
+        valueIcon={<Icon severity={severity} aria-hidden />}
+      />
+      {severity !== recommendedSeverity && (
+        <HelperText className="sw-mt-2">
+          <FormattedMessage
+            id="coding_rules.custom_severity.not_recommended"
+            values={{
+              recommended: (
+                <b className="sw-lowercase">{getSeverityTranslation(recommendedSeverity)}</b>
+              ),
+            }}
+          />
+        </HelperText>
+      )}
+    </>
   );
 }
index 10e845a5d04bd0fa3f19e1c4baa8ef111a6bdad6..ddfc1ffe0fc058a15412a4c213efe0b99893fc8e 100644 (file)
@@ -156,10 +156,13 @@ const selectors = {
     name: /coding_rules.deactivate_in_quality_profile/,
     hidden: true,
   }),
-  oldSeveritySelect: byLabelText('severity'),
   qualityProfileSelect: byLabelText('coding_rules.quality_profile'),
-  prioritizedSwitch: byRole('switch', { hidden: true }),
-  selectValue: byText(/severity\./),
+  oldSeveritySelect: byRole('combobox', { name: 'coding_rules.custom_severity.choose_severity' }),
+  mqrSwitch: byRole('switch'),
+  newSeveritySelect: (quality: SoftwareQuality) =>
+    byRole('combobox', { name: `software_quality.${quality}` }),
+  notRecommendedSeverity: byText('coding_rules.custom_severity.not_recommended'),
+  prioritizedSwitch: byRole('checkbox', { name: 'coding_rules.prioritized_rule.switch_label' }),
   activateQPDialog: byRole('dialog', { name: 'coding_rules.activate_in_quality_profile' }),
   changeButton: (profile: string) =>
     byRole('button', { name: `coding_rules.change_details_x.${profile}` }),
index 92c717b29cd2793de5230bf2b726dabf439a3baa..ea1a59fae19da59cc1f45b2db8707da31414cca2 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import { useIntl } from 'react-intl';
+import SoftwareImpactSeverityIcon from '../../../components/icon-mappers/SoftwareImpactSeverityIcon';
 import { ProfileChangelogEventImpactChange } from '../types';
 
 interface Props {
@@ -31,9 +32,19 @@ export default function SoftwareImpactChange({ impactChange }: Readonly<Props>)
   const intl = useIntl();
 
   const labels = {
-    oldSeverity: intl.formatMessage({ id: `severity.${oldSeverity}` }),
+    oldSeverity: (
+      <>
+        <SoftwareImpactSeverityIcon severity={oldSeverity} />{' '}
+        {intl.formatMessage({ id: `severity_impact.${oldSeverity}` })}
+      </>
+    ),
     oldSoftwareQuality: intl.formatMessage({ id: `software_quality.${oldSoftwareQuality}` }),
-    newSeverity: intl.formatMessage({ id: `severity.${newSeverity}` }),
+    newSeverity: (
+      <>
+        <SoftwareImpactSeverityIcon severity={newSeverity} />{' '}
+        {intl.formatMessage({ id: `severity_impact.${newSeverity}` })}
+      </>
+    ),
     newSoftwareQuality: intl.formatMessage({ id: `software_quality.${newSoftwareQuality}` }),
   };
 
index c77099634ccab3787598f3b95d8f6f7c7250866c..b1d4c2b24fffaea9d94d7eb2b9fe48e42aad3c9f 100644 (file)
@@ -92,8 +92,8 @@ it('should see the changelog', async () => {
   ui.checkRow(3, '', '', '', 'Rule 1', [
     /quality_profiles.deprecated_severity_set_to severity.CRITICAL/,
     /quality_profiles.changelog.cca_and_category_changed.*COMPLETE.*INTENTIONAL.*LAWFUL.*RESPONSIBLE/,
-    /quality_profiles.changelog.impact_added.severity.*MEDIUM.*RELIABILITY/,
-    /quality_profiles.changelog.impact_removed.severity.HIGH.*MAINTAINABILITY/,
+    /quality_profiles.changelog.impact_added.severity_impact.*MEDIUM.*RELIABILITY/,
+    /quality_profiles.changelog.impact_removed.severity_impact.HIGH.*MAINTAINABILITY/,
   ]);
   await user.click(ui.link.get(rows[1]));
   expect(screen.getByText('/coding_rules?rule_key=c%3Arule0')).toBeInTheDocument();
index 8d5efa930a02a26c469cfb80566185d764942284..cce7a9231910d3c170b4798f3d756edb3a83fcd8 100644 (file)
@@ -74,6 +74,7 @@ export enum DocLink {
   PullRequestAnalysis = '/analyzing-source-code/pull-request-analysis/introduction/',
   QualityGates = '/instance-administration/analysis-functions/quality-gates/',
   Root = '/',
+  RuleSeverity = '/instance-administration/analysis-functions/quality-profiles/#rule-severity',
   RulesOverview = '/user-guide/rules/overview',
   SecurityHotspots = '/user-guide/security-hotspots/',
   SecurityReports = '/user-guide/viewing-reports/security-reports/',
index 7050f6de7e68cab149179492a16613493f11a99a..2408be0427c914df7e863a4ec0749e259c8819a9 100644 (file)
@@ -641,6 +641,9 @@ export function mockRuleActivation(overrides: Partial<RuleActivation> = {}): Rul
     qProfile: 'baz',
     severity: 'MAJOR',
     prioritizedRule: false,
+    impacts: [
+      { softwareQuality: SoftwareQuality.Maintainability, severity: SoftwareImpactSeverity.Medium },
+    ],
     ...overrides,
   };
 }
index c0ce6ab4654bdd30bffefeee683faa02a555c60a..bd13aa9c2198531dab608fb866c0f2c47653e0f1 100644 (file)
@@ -25,6 +25,8 @@ import {
   CleanCodeAttribute,
   CleanCodeAttributeCategory,
   SoftwareImpact,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
 } from './clean-code-taxonomy';
 import { MessageFormatting, RawIssue } from './issues';
 import { NewCodeDefinitionType } from './new-code-definition';
@@ -519,6 +521,7 @@ export interface RestRule {
 
 export interface RuleActivation {
   createdAt: string;
+  impacts: { severity: SoftwareImpactSeverity; softwareQuality: SoftwareQuality }[];
   inherit: RuleInheritance;
   params: { key: string; value: string }[];
   prioritizedRule: boolean;
index 00d977c03b3401c46475ca554d698d3256e75427..fc7617ef0e3f36bbee365d7833330179301fd639 100644 (file)
@@ -163,6 +163,7 @@ none=None
 no_file_selected=No file selected
 no_tags=No tags
 not_now=Not now
+not_impacted=Not impacted
 off=off
 on=on
 or=Or
@@ -2522,6 +2523,14 @@ rules.status.REMOVED.help=The rule that generated this issue has been removed. S
 #------------------------------------------------------------------------------
 coding_rules.active_in_all_profiles=The rule is already activated on all available Quality Profiles.
 coding_rules.severity_deprecated=Changing rule severities is deprecated and will not be possible in the future.
+coding_rules.custom_severity.title=Rule severity in this quality profile
+coding_rules.custom_severity.description.standard=Changing rule severity in a quality profile {link}
+coding_rules.custom_severity.description.standard.link=does not change its recommended severity
+coding_rules.custom_severity.description.mqr=In the multi-quality mode, severities are directly tied to the software quality impacted. This means that {link}
+coding_rules.custom_severity.description.mqr.link=each software quality has its own severity per rule.
+coding_rules.custom_severity.not_recommended=This is a custom rule severity, the recommended one is {recommended}
+coding_rules.custom_severity.choose_severity=Choose severity
+coding_rules.custom_severity.severity_with_recommended={severity} (recommended)
 coding_rules.activate=Activate
 coding_rules.activate_in=Activate In
 coding_rules.activate_in_quality_profile=Activate In Quality Profile
@@ -2606,7 +2615,7 @@ coding_rules.type.deprecation.filter_by=You can now filter rules by Clean Code A
 coding_rules.severity.deprecation.title=Severities are now directly tied to the software quality impacted. This old severity is deprecated and it will no longer be possible to change it in the future.
 coding_rules.severity.deprecation.filter_by=You can now filter rules by Software Quality and new Severity.
 coding_rules.prioritized_rule.title=Prioritized rule
-coding_rules.prioritized_rule.switch_label=Indicates that all corresponding issues in Overall Code should be fixed
+coding_rules.prioritized_rule.switch_label=All corresponding issues in the overall code should be fixed
 coding_rules.prioritized_rule.note=For your Quality Gate to fail when corresponding issues exist in the overall code, you must add a condition that checks whether any issues have been raised from prioritized rules.
 
 coding_rules.update_custom_rule=Update Custom Rule