From 1d629e962ed89540dbe683610f6a001a283e8d61 Mon Sep 17 00:00:00 2001 From: Viktor Vorona Date: Fri, 31 May 2024 14:09:10 +0200 Subject: [PATCH] SONAR-22224 Rules list filter by prioritized flag --- .../js/api/mocks/CodingRulesServiceMock.ts | 13 ++++ .../coding-rules/__tests__/CodingRules-it.ts | 32 +++++++++- .../coding-rules/components/FacetsList.tsx | 18 ++++++ .../components/PrioritizedRulesFacet.tsx | 59 +++++++++++++++++++ .../src/main/js/apps/coding-rules/query.ts | 3 + .../main/js/apps/coding-rules/utils-tests.tsx | 1 + server/sonar-web/src/main/js/types/rules.ts | 1 + .../resources/org/sonar/l10n/core.properties | 5 +- 8 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/PrioritizedRulesFacet.tsx 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 c180408a466..62967a73ab9 100644 --- a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts @@ -95,6 +95,7 @@ type FacetFilter = Pick< | 'cwe' | 'is_template' | 'cleanCodeAttributeCategories' + | 'prioritizedRule' >; const FACET_RULE_MAP: { [key: string]: keyof Rule } = { @@ -182,6 +183,7 @@ export default class CodingRulesServiceMock { 'owaspTop10-2021': owasp2021Top10, cwe, activation, + prioritizedRule, }: FacetFilter) { let filteredRules = this.getRulesFilteredByRemovedStatus(); if (cleanCodeAttributeCategories) { @@ -256,6 +258,15 @@ export default class CodingRulesServiceMock { if (tags) { filteredRules = filteredRules.filter((r) => r.tags && r.tags.some((t) => tags.includes(t))); } + if (qprofile && prioritizedRule !== undefined) { + filteredRules = filteredRules.filter((r) => { + const qProfilesInRule = this.rulesActivations[r.key] ?? []; + const ruleHasQueriedProfile = qProfilesInRule.find((q) => q.qProfile === qprofile); + return prioritizedRule === 'true' + ? ruleHasQueriedProfile?.prioritizedRule + : !ruleHasQueriedProfile?.prioritizedRule; + }); + } return this.getRulesWithoutDetails(filteredRules); } @@ -434,6 +445,7 @@ export default class CodingRulesServiceMock { is_template, activation, cleanCodeAttributeCategories, + prioritizedRule, }: SearchRulesQuery): Promise => { const standards = await getStandards(); const facetCounts: Array<{ property: string; values: { val: string; count: number }[] }> = []; @@ -493,6 +505,7 @@ export default class CodingRulesServiceMock { 'owaspTop10-2021': owasp2021Top10, cwe, activation, + prioritizedRule, }); } 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 aff33129022..27b251d703c 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 @@ -143,7 +143,7 @@ describe('Rules app list', () => { expect(ui.getAllRuleListItems()).toHaveLength(1); }); - it('filter by quality profile, tag and search by tag', async () => { + it('filter by quality profile, tag and search by tag, does not show prioritized rule', async () => { const { ui, user } = getPageObjects(); renderCodingRulesApp(mockCurrentUser()); await ui.appLoaded(); @@ -155,6 +155,8 @@ describe('Rules app list', () => { await user.click(ui.facetItem('QP Foo Java').get()); expect(ui.getAllRuleListItems()).toHaveLength(1); + expect(ui.prioritizedRuleFacet.query()).not.toBeInTheDocument(); + // Filter by tag await user.click(ui.facetClear('clear-coding_rules.facet.qprofile').get()); // Clear quality profile facet await user.click(ui.tagsFacet.get()); @@ -232,6 +234,34 @@ describe('Rules app list', () => { await user.type(ui.searchInput.get(), 'Hot hotspot'); expect(ui.getAllRuleListItems()).toHaveLength(1); }); + + it('filter by quality profileand prioritizedRule', async () => { + const { ui, user } = getPageObjects(); + renderCodingRulesApp(mockCurrentUser(), undefined, [Feature.PrioritizedRules]); + await ui.appLoaded(); + + expect(ui.getAllRuleListItems()).toHaveLength(11); + + expect(ui.prioritizedRuleFacet.get()).toHaveAttribute('aria-disabled', 'true'); + + // Filter by quality profile + await user.click(ui.qpFacet.get()); + await user.click(ui.facetItem('QP Bar Python').get()); + expect(ui.getAllRuleListItems()).toHaveLength(4); + + // Filter by prioritized rule + expect(ui.prioritizedRuleFacet.get()).not.toHaveAttribute('aria-disabled', 'true'); + await user.click(ui.prioritizedRuleFacet.get()); + await user.click(ui.facetItem('coding_rules.filters.prioritizedRule.true').get()); + expect(ui.getAllRuleListItems()).toHaveLength(1); + + // Filter by non-prioritized rule + await user.click(ui.facetItem('coding_rules.filters.prioritizedRule.false').get()); + expect(ui.getAllRuleListItems()).toHaveLength(3); + + await user.click(ui.facetClear('clear-coding_rules.facet.prioritizedRule').get()); + expect(ui.getAllRuleListItems()).toHaveLength(4); + }); }); describe('bulk change', () => { diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx index 4d9f8f343e3..e937feb4806 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx @@ -20,7 +20,9 @@ import { BasicSeparator } from 'design-system'; import * as React from 'react'; import { Profile } from '../../../api/quality-profiles'; +import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures'; import { translate } from '../../../helpers/l10n'; +import { Feature } from '../../../types/features'; import { Dict } from '../../../types/types'; import { LanguageFacet } from '../../issues/sidebar/LanguageFacet'; import { StandardFacet } from '../../issues/sidebar/StandardFacet'; @@ -28,6 +30,7 @@ import { Facets, OpenFacets, Query } from '../query'; import AttributeCategoryFacet from './AttributeCategoryFacet'; import AvailableSinceFacet from './AvailableSinceFacet'; import InheritanceFacet from './InheritanceFacet'; +import PrioritizedRulesFacet from './PrioritizedRulesFacet'; import ProfileFacet from './ProfileFacet'; import RepositoryFacet from './RepositoryFacet'; import SeverityFacet from './SeverityFacet'; @@ -52,6 +55,7 @@ export interface FacetsListProps { const MAX_INITIAL_LANGUAGES = 5; export default function FacetsList(props: FacetsListProps) { + const { hasFeature } = useAvailableFeatures(); const languageDisabled = !props.hideProfileFacet && props.query.profile !== undefined; const inheritanceDisabled = @@ -59,6 +63,8 @@ export default function FacetsList(props: FacetsListProps) { props.selectedProfile === undefined || !props.selectedProfile.isInherited; + const showPrioritizedRuleFacet = hasFeature(Feature.PrioritizedRules); + return ( <> + {showPrioritizedRuleFacet && ( + <> + + + + )} )} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/PrioritizedRulesFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/PrioritizedRulesFacet.tsx new file mode 100644 index 00000000000..24cfa5bf962 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/PrioritizedRulesFacet.tsx @@ -0,0 +1,59 @@ +/* + * 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 * as React from 'react'; +import { translate } from '../../../helpers/l10n'; +import Facet, { BasicProps } from './Facet'; + +interface Props extends Omit { + onChange: (changes: { prioritizedRule: boolean | undefined }) => void; + disabled: boolean; + value: boolean | undefined; +} + +export default function PrioritizedRulesFacet(props: Readonly) { + const { value, disabled, ...rest } = props; + + const handleChange = (changes: { prioritizedRule: string | any[] }) => { + const prioritizedRule = + // empty array is returned when a user cleared the facet + // otherwise `"true"`, `"false"` or `undefined` can be returned + Array.isArray(changes.prioritizedRule) || changes.prioritizedRule === undefined + ? undefined + : changes.prioritizedRule === 'true'; + props.onChange({ ...changes, prioritizedRule }); + }; + + const renderName = (value: string) => translate('coding_rules.filters.prioritizedRule', value); + + return ( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/query.ts b/server/sonar-web/src/main/js/apps/coding-rules/query.ts index 4950906bd5a..181d59f2d75 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/query.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/query.ts @@ -60,6 +60,7 @@ export interface Query { tags: string[]; template: boolean | undefined; types: string[]; + prioritizedRule: boolean | undefined; } export type FacetKey = keyof Query; @@ -107,6 +108,7 @@ export function parseQuery(query: RawQuery): Query { tags: parseAsArray(query.tags, parseAsString), template: parseAsOptionalBoolean(query.is_template), types: parseAsArray(query.types, parseAsString), + prioritizedRule: parseAsOptionalBoolean(query.prioritizedRule), }; } @@ -133,6 +135,7 @@ export function serializeQuery(query: Query): RawQuery { statuses: serializeStringArray(query.statuses), tags: serializeStringArray(query.tags), types: serializeStringArray(query.types), + prioritizedRule: serializeOptionalBoolean(query.prioritizedRule), }); } 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 c9cd21e8609..89a63e1ae82 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 @@ -72,6 +72,7 @@ const selectors = { availableSinceFacet: byRole('button', { name: 'coding_rules.facet.available_since' }), templateFacet: byRole('button', { name: 'coding_rules.facet.template' }), qpFacet: byRole('button', { name: 'coding_rules.facet.qprofile' }), + prioritizedRuleFacet: byRole('button', { name: 'coding_rules.facet.prioritizedRule' }), facetClear: (name: string) => byTestId(name), facetSearchInput: (name: string) => byRole('searchbox', { name }), facetItem: (name: string | RegExp) => byRole('checkbox', { name }), diff --git a/server/sonar-web/src/main/js/types/rules.ts b/server/sonar-web/src/main/js/types/rules.ts index 94e4f4e0248..761548feb9f 100644 --- a/server/sonar-web/src/main/js/types/rules.ts +++ b/server/sonar-web/src/main/js/types/rules.ts @@ -53,6 +53,7 @@ export interface SearchRulesQuery { tags?: string; template_key?: string; types?: string; + prioritizedRule?: boolean | string; } export enum RulesFacetName { 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 28ff53ba35c..a532247bbd2 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2478,7 +2478,9 @@ coding_rules.filters.tag=Tag coding_rules.filters.template=Templates coding_rules.filters.template.is_template=Show Templates Only coding_rules.filters.template.is_not_template=Hide Templates - +coding_rules.filters.prioritizedRule.disabled=Prioritized Rules criterion is available when Quality Profile is selected +coding_rules.filters.prioritizedRule.true=Show Prioritized Only +coding_rules.filters.prioritizedRule.false=Hide Prioritized coding_rules.facet.languages=Language coding_rules.facet.repositories=Repository coding_rules.facet.impactSeverities=Severity @@ -2497,6 +2499,7 @@ coding_rules.facet.activationSeverities=Activation Severity coding_rules.facet.template=Template coding_rules.facet.rule_key=Rule coding_rules.facet.types=Type +coding_rules.facet.prioritizedRule=Prioritized Rules coding_rules.facet.language.show_more=Show more languages coding_rules.facet.language.show_less=Show less languages -- 2.39.5