From f2ad7fba476f20d96043b28e6b1a981e893ed3bd Mon Sep 17 00:00:00 2001 From: Viktor Vorona Date: Mon, 7 Oct 2024 12:38:18 +0200 Subject: [PATCH] SONAR-23262 Show if rule is customized on the quality profile list? --- .../js/api/mocks/CodingRulesServiceMock.ts | 4 +- .../coding-rules/__tests__/CodingRules-it.ts | 136 +++++++++++++++++- .../components/RuleDetailsProfiles.tsx | 115 ++++++++++++++- .../main/js/apps/coding-rules/utils-tests.tsx | 5 + .../src/main/js/types/clean-code-taxonomy.ts | 1 + .../resources/org/sonar/l10n/core.properties | 3 + 6 files changed, 253 insertions(+), 11 deletions(-) diff --git a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts index 3235f698a0c..27e43c8dee5 100644 --- a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts @@ -629,7 +629,9 @@ export default class CodingRulesServiceMock { const impacts = data.severity ? [ ...ruleImpacts.filter( - (impact) => !inheritedImpacts.some((i) => i.softwareQuality === impact.softwareQuality), + (impact) => + impact.softwareQuality !== SoftwareQuality.Maintainability && + !inheritedImpacts.some((i) => i.softwareQuality === impact.softwareQuality), ), ...inheritedImpacts.filter( (impact) => impact.softwareQuality !== SoftwareQuality.Maintainability, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts index 7b59e1ef06b..e9fe34527e5 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -943,23 +943,40 @@ describe('Rule app details', () => { expect(ui.caycNotificationButton.query()).not.toBeInTheDocument(); }); + + it('should show customized severity and prioritized rule', async () => { + const { ui, user } = getPageObjects(); + renderCodingRulesApp(mockCurrentUser(), 'coding_rules?open=rule10'); + + await ui.detailsloaded(); + await user.click(ui.moreInfoTab.get()); + + expect(ui.caycNotificationButton.query()).not.toBeInTheDocument(); + }); }); it('can activate/change/deactivate rule in quality profile', async () => { const { ui, user } = getPageObjects(); rulesHandler.setIsAdmin(); renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1', [Feature.PrioritizedRules]); - await ui.detailsloaded(); - expect(ui.qpLink('QP Foo').get()).toBeInTheDocument(); + expect(await ui.qpLink('QP Foo').find()).toBeInTheDocument(); // Activate rule in quality profile expect(ui.prioritizedRuleCell.query()).not.toBeInTheDocument(); await user.click(ui.activateButton.get()); await user.click(ui.prioritizedSwitch.get()); + await user.click(ui.mqrSwitch.get()); + await user.click(ui.newSeveritySelect(SoftwareQuality.Maintainability).get()); + await user.click(byRole('option', { name: 'severity_impact.LOW' }).get()); await user.click(ui.activateButton.get(ui.activateQPDialog.get())); expect(ui.qpLink('QP FooBar').get()).toBeInTheDocument(); expect(ui.prioritizedRuleCell.get()).toBeInTheDocument(); + expect(ui.oldSeverityCustomizedCell.query()).not.toBeInTheDocument(); + expect(ui.newSeverityCustomizedCell.get()).toBeInTheDocument(); + await expect(ui.newSeverityCustomizedCell.get()).toHaveATooltipWithContent( + 'coding_rules.impact_customized.detailsoftware_quality.MAINTAINABILITYseverity_impact.MEDIUMseverity_impact.LOW', + ); // Activate last java rule await user.click(ui.activateButton.get()); @@ -978,13 +995,26 @@ describe('Rule app details', () => { await user.click(ui.changeButton('QP FooBaz').get()); await user.clear(ui.paramInput('1').get()); await user.type(ui.paramInput('1').get(), 'New'); + await user.click(ui.mqrSwitch.get()); + await user.click(ui.newSeveritySelect(SoftwareQuality.Maintainability).get()); + await user.click(byRole('option', { name: 'severity_impact.BLOCKER' }).get()); await user.click(ui.saveButton.get(ui.changeQPDialog.get())); - expect(await screen.findByText('New')).toBeInTheDocument(); + expect(await ui.qualityProfileRow.findAt(5)).toHaveTextContent('QP FooBaz'); + expect(ui.qualityProfileRow.getAt(5)).toHaveTextContent('New'); + await expect( + ui.newSeverityCustomizedCell.get(ui.qualityProfileRow.getAt(5)), + ).toHaveATooltipWithContent( + 'coding_rules.impact_customized.detailsoftware_quality.MAINTAINABILITYseverity_impact.MEDIUMseverity_impact.BLOCKER', + ); // Revert rule details in quality profile await user.click(ui.revertToParentDefinitionButton.get()); await user.click(ui.yesButton.get()); - expect(screen.queryByText('New')).not.toBeInTheDocument(); + expect(await ui.qualityProfileRow.findAt(5)).toHaveTextContent('QP FooBaz'); + expect(await ui.qualityProfileRow.findAt(5)).not.toHaveTextContent('New'); + expect( + ui.newSeverityCustomizedCell.query(ui.qualityProfileRow.getAt(5)), + ).not.toBeInTheDocument(); // Deactivate rule in quality profile await user.click(ui.deactivateInQPButton('QP FooBar').get()); @@ -992,6 +1022,104 @@ describe('Rule app details', () => { expect(ui.qpLink('QP FooBar').query()).not.toBeInTheDocument(); }); + it('can activate/change/deactivate rule in quality profile for legacy mode', async () => { + const { ui, user } = getPageObjects(); + settingsHandler.set(SettingsKey.LegacyMode, 'true'); + rulesHandler.setIsAdmin(); + renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1', [Feature.PrioritizedRules]); + await ui.detailsloaded(); + expect(ui.qpLink('QP Foo').get()).toBeInTheDocument(); + + // Activate rule in quality profile + expect(ui.prioritizedRuleCell.query()).not.toBeInTheDocument(); + await user.click(ui.activateButton.get()); + + await user.click(ui.prioritizedSwitch.get()); + await user.click(ui.oldSeveritySelect.get()); + await user.click(byRole('option', { name: 'severity.MINOR' }).get()); + await user.click(ui.activateButton.get(ui.activateQPDialog.get())); + expect(ui.qpLink('QP FooBar').get()).toBeInTheDocument(); + expect(ui.prioritizedRuleCell.get()).toBeInTheDocument(); + expect(ui.newSeverityCustomizedCell.query()).not.toBeInTheDocument(); + expect(ui.oldSeverityCustomizedCell.get()).toBeInTheDocument(); + expect(ui.oldSeverityCustomizedCell.get()).toHaveTextContent('severity.MAJORseverity.MINOR'); + + await user.click(ui.changeButton('QP FooBar').get()); + await user.click(ui.oldSeveritySelect.get()); + await user.click( + byRole('option', { name: /coding_rules.custom_severity.severity_with_recommended/ }).get(), + ); + await user.click(ui.saveButton.get(ui.changeQPDialog.get())); + expect(ui.prioritizedRuleCell.get()).toBeInTheDocument(); + expect(ui.oldSeverityCustomizedCell.query()).not.toBeInTheDocument(); + expect(ui.newSeverityCustomizedCell.query()).not.toBeInTheDocument(); + + // Activate last java rule + await user.click(ui.activateButton.get()); + await user.type(ui.paramInput('1').get(), 'paramInput'); + await user.click(ui.activateButton.get(ui.activateQPDialog.get())); + expect(ui.qpLink('QP FooBarBaz').get()).toBeInTheDocument(); + expect(ui.qpLink('QP FooBaz').get()).toBeInTheDocument(); + + // Change rule details in quality profile + await user.click(ui.changeButton('QP FooBaz').get()); + await user.click(ui.oldSeveritySelect.get()); + await user.click(byRole('option', { name: 'severity.BLOCKER' }).get()); + await user.click(ui.saveButton.get(ui.changeQPDialog.get())); + expect(await ui.qualityProfileRow.findAt(5)).toHaveTextContent('QP FooBaz'); + expect(ui.oldSeverityCustomizedCell.get(ui.qualityProfileRow.getAt(5))).toBeInTheDocument(); + expect(ui.oldSeverityCustomizedCell.get(ui.qualityProfileRow.getAt(5))).toHaveTextContent( + 'severity.MAJORseverity.BLOCKER', + ); + + // Revert rule details in quality profile + await user.click(ui.revertToParentDefinitionButton.get()); + await user.click(ui.yesButton.get()); + expect(await ui.qualityProfileRow.findAt(5)).toHaveTextContent('QP FooBaz'); + expect( + ui.oldSeverityCustomizedCell.query(ui.qualityProfileRow.getAt(5)), + ).not.toBeInTheDocument(); + }); + + it('should show multiple customized severities', async () => { + const { ui, user } = getPageObjects(); + rulesHandler.setIsAdmin(); + renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule10', [ + Feature.PrioritizedRules, + ]); + await ui.detailsloaded(); + + expect(ui.newSeverityCustomizedCell.get(ui.qualityProfileRow.getAt(1))).toBeInTheDocument(); + await expect( + ui.newSeverityCustomizedCell.get(ui.qualityProfileRow.getAt(1)), + ).toHaveATooltipWithContent( + 'coding_rules.impact_customized.detailsoftware_quality.RELIABILITYseverity_impact.HIGHseverity_impact.INFO' + + 'coding_rules.impact_customized.detailsoftware_quality.MAINTAINABILITYseverity_impact.LOWseverity_impact.MEDIUM', + ); + + expect(ui.newSeverityCustomizedCell.get(ui.qualityProfileRow.getAt(2))).toBeInTheDocument(); + await expect( + ui.newSeverityCustomizedCell.get(ui.qualityProfileRow.getAt(2)), + ).toHaveATooltipWithContent( + 'coding_rules.impact_customized.detailsoftware_quality.RELIABILITYseverity_impact.HIGHseverity_impact.BLOCKER', + ); + + await user.click(ui.changeButton('QP Bar').get()); + await user.click(ui.mqrSwitch.get()); + await user.click(ui.newSeveritySelect(SoftwareQuality.Reliability).get()); + await user.click( + byRole('option', { name: /coding_rules.custom_severity.severity_with_recommended/ }).get(), + ); + await user.click(ui.newSeveritySelect(SoftwareQuality.Maintainability).get()); + await user.click( + byRole('option', { name: /coding_rules.custom_severity.severity_with_recommended/ }).get(), + ); + await user.click(ui.saveButton.get(ui.changeQPDialog.get())); + expect( + ui.newSeverityCustomizedCell.query(ui.qualityProfileRow.getAt(1)), + ).not.toBeInTheDocument(); + }); + it('can deactivate an inherrited rule', async () => { const { ui, user } = getPageObjects(); rulesHandler.setIsAdmin(); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx index 8b7b29110af..52a91e8babc 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx @@ -19,31 +19,32 @@ */ import styled from '@emotion/styled'; +import { LinkStandalone, Text, Tooltip } from '@sonarsource/echoes-react'; import { ActionCell, CellComponent, ContentCell, DiscreetLink, InheritanceIcon, - Link, Note, SeparatorCircleIcon, SubTitle, Table, TableRow, TableRowInteractive, - TextSubdued, } from 'design-system'; -import { filter } from 'lodash'; +import { filter, isEqual } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Profile } from '../../../api/quality-profiles'; +import { SOFTWARE_QUALITIES } from '../../../helpers/constants'; import { translate } from '../../../helpers/l10n'; import { getQualityProfileUrl } from '../../../helpers/urls'; import { useActivateRuleMutation, useDeactivateRuleMutation, } from '../../../queries/quality-profiles'; +import { useIsLegacyCCTMode } from '../../../queries/settings'; import { Dict, RuleActivation, RuleDetails } from '../../../types/types'; import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge'; import ActivatedRuleActions from './ActivatedRuleActions'; @@ -62,10 +63,15 @@ const MANDATORY_COLUMNS_COUNT = 2; const PROFILES_HEADING_ID = 'rule-details-profiles-heading'; +const softwareQualityOrderMap = new Map( + SOFTWARE_QUALITIES.map((quality, index) => [quality, index]), +); + export default function RuleDetailsProfiles(props: Readonly) { const { activations = [], referencedProfiles, ruleDetails, canDeactivateInherited } = props; const { mutate: activateRule } = useActivateRuleMutation(props.onActivate); const { mutate: deactivateRule } = useDeactivateRuleMutation(props.onDeactivate); + const { data: isLegacy } = useIsLegacyCCTMode(); const canActivate = Object.values(referencedProfiles).some((profile) => Boolean(profile.actions?.edit && profile.language === ruleDetails.lang), @@ -142,19 +148,116 @@ export default function RuleDetailsProfiles(props: Readonly) {
- {profile.name} - + {activation.prioritizedRule && ( <> - {translate('coding_rules.prioritized_rule.title')} + {translate('coding_rules.prioritized_rule.title')} + + )} + {!isLegacy && + activation.impacts && + !isEqual(activation.impacts, ruleDetails.impacts) && ( + <> + + + {[...activation.impacts] + .sort((a, b) => { + const indexA = softwareQualityOrderMap.get(a.softwareQuality) ?? -1; + const indexB = softwareQualityOrderMap.get(b.softwareQuality) ?? -1; + return indexA - indexB; + }) + .map((impact) => { + const ruleImpact = ruleDetails.impacts.find( + (i) => i.softwareQuality === impact.softwareQuality, + ); + if (!ruleImpact || ruleImpact.severity === impact.severity) { + return null; + } + return ( + + + + + ), + recommended: ( + + + + ), + customized: ( + + + + ), + }} + /> + + ); + })} + + } + > + {translate('coding_rules.impact_customized.message')} + + + )} + + {isLegacy && activation.severity && activation.severity !== ruleDetails.severity && ( + <> + + + + + + ), + customized: ( + + + + ), + }} + /> + )} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx index ddfc1ffe0fc..ce8bbefbd61 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx @@ -118,6 +118,11 @@ const selectors = { extendDescriptionButton: byRole('button', { name: 'coding_rules.extend_description' }), extendDescriptionTextbox: byRole('textbox', { name: 'coding_rules.extend_description' }), prioritizedRuleCell: byRole('cell', { name: /coding_rules.prioritized_rule.title/ }), + oldSeverityCustomizedCell: byRole('cell', { name: /coding_rules.severity_customized.message/ }), + newSeverityCustomizedCell: byRole('cell', { + name: /coding_rules.impact_customized.message/, + }).byText('coding_rules.impact_customized.message'), + qualityProfileRow: byRole('table', { name: 'coding_rules.quality_profiles' }).byRole('row'), saveButton: byRole('button', { name: 'save' }), cancelButton: byRole('button', { name: 'cancel' }), removeButton: byRole('button', { name: 'remove' }), diff --git a/server/sonar-web/src/main/js/types/clean-code-taxonomy.ts b/server/sonar-web/src/main/js/types/clean-code-taxonomy.ts index d5a80809f20..d2c2415ded6 100644 --- a/server/sonar-web/src/main/js/types/clean-code-taxonomy.ts +++ b/server/sonar-web/src/main/js/types/clean-code-taxonomy.ts @@ -49,6 +49,7 @@ export enum CleanCodeAttribute { Trustworthy = 'TRUSTWORTHY', } +// The order here is important. Please be mindful about the order when adding new software qualities. export enum SoftwareQuality { Security = 'SECURITY', Reliability = 'RELIABILITY', diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index fc7617ef0e3..a2320cce5d9 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2615,6 +2615,9 @@ 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.impact_customized.message=Rule severity was customized in this profile +coding_rules.impact_customized.detail={softwareQuality} changed from {recommended} to {customized} +coding_rules.severity_customized.message=Rule severity was customized from {recommended} to {customized} 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. -- 2.39.5