]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20197 Rules facet filters use CCT
authorstanislavh <stanislav.honcharov@sonarsource.com>
Fri, 18 Aug 2023 12:57:33 +0000 (14:57 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 22 Aug 2023 20:03:05 +0000 (20:03 +0000)
24 files changed:
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/apps/coding-rules/__tests__/CodingRules-it.ts
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/components/AttributeCategoryFacet.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/DefaultSeverityFacet.tsx [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/SeverityFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/SoftwareQualityFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/query.ts
server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/AttributeCategoryFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/SoftwareQualityFacet.tsx
server/sonar-web/src/main/js/apps/issues/utils.ts
server/sonar-web/src/main/js/components/facet/FacetHeader.tsx
server/sonar-web/src/main/js/helpers/constants.ts
server/sonar-web/src/main/js/helpers/query.ts
server/sonar-web/src/main/js/types/rules.ts
server/sonar-web/src/main/js/types/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index df68b91786c7c9a70e083be7db2c985b5d152c20..d22c7cc83e0751ff58010eed225b8348424806e6 100644 (file)
@@ -67,12 +67,13 @@ jest.mock('../quality-profiles');
 
 type FacetFilter = Pick<
   SearchRulesQuery,
+  | 'impactSeverities'
+  | 'impactSoftwareQualities'
   | 'languages'
   | 'tags'
   | 'available_since'
   | 'q'
   | 'types'
-  | 'severities'
   | 'repositories'
   | 'qprofile'
   | 'activation'
@@ -81,6 +82,7 @@ type FacetFilter = Pick<
   | 'owaspTop10-2021'
   | 'cwe'
   | 'is_template'
+  | 'cleanCodeAttributeCategories'
 >;
 
 const FACET_RULE_MAP: { [key: string]: keyof Rule } = {
@@ -151,10 +153,12 @@ export default class CodingRulesServiceMock {
   }
 
   filterFacet({
+    impactSeverities,
+    impactSoftwareQualities,
+    cleanCodeAttributeCategories,
     languages,
     available_since,
     q,
-    severities,
     types,
     tags,
     is_template,
@@ -167,15 +171,31 @@ export default class CodingRulesServiceMock {
     activation,
   }: FacetFilter) {
     let filteredRules = this.rules;
+    if (cleanCodeAttributeCategories) {
+      filteredRules = filteredRules.filter(
+        (r) =>
+          r.cleanCodeAttributeCategory &&
+          cleanCodeAttributeCategories.includes(r.cleanCodeAttributeCategory)
+      );
+    }
+    if (impactSoftwareQualities) {
+      filteredRules = filteredRules.filter(
+        (r) =>
+          r.impacts &&
+          r.impacts.some(({ softwareQuality }) => impactSoftwareQualities.includes(softwareQuality))
+      );
+    }
+    if (impactSeverities) {
+      filteredRules = filteredRules.filter(
+        (r) => r.impacts && r.impacts.some(({ severity }) => impactSeverities.includes(severity))
+      );
+    }
     if (types) {
       filteredRules = filteredRules.filter((r) => types.includes(r.type));
     }
     if (languages) {
       filteredRules = filteredRules.filter((r) => r.lang && languages.includes(r.lang));
     }
-    if (severities) {
-      filteredRules = filteredRules.filter((r) => r.severity && severities.includes(r.severity));
-    }
     if (qprofile) {
       const qProfileLang = this.qualityProfile.find((p) => p.key === qprofile)?.language;
       filteredRules = filteredRules
@@ -365,7 +385,8 @@ export default class CodingRulesServiceMock {
     p,
     ps,
     available_since,
-    severities,
+    impactSeverities,
+    impactSoftwareQualities,
     repositories,
     qprofile,
     sonarsourceSecurity,
@@ -377,6 +398,7 @@ export default class CodingRulesServiceMock {
     rule_key,
     is_template,
     activation,
+    cleanCodeAttributeCategories,
   }: SearchRulesQuery): Promise<SearchRulesResponse> => {
     const standards = await getStandards();
     const facetCounts: Array<{ property: string; values: { val: string; count: number }[] }> = [];
@@ -424,7 +446,9 @@ export default class CodingRulesServiceMock {
         languages,
         available_since,
         q,
-        severities,
+        impactSeverities,
+        impactSoftwareQualities,
+        cleanCodeAttributeCategories,
         repositories,
         types,
         tags,
index 785966acdb78db0ca59656e210bf3add9dc9af72..63855d8ac1e293e554c33ceabf0be5e28c2832e4 100644 (file)
 
 import { RuleDescriptionSections } from '../../../apps/coding-rules/rule';
 import { mockRule, mockRuleActivation, mockRuleDetails } from '../../../helpers/testMocks';
+import {
+  CleanCodeAttributeCategory,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
+} from '../../../types/clean-code-taxonomy';
 import {
   ADVANCED_RULE,
   RULE_1,
@@ -113,6 +118,9 @@ export function mockRuleDetailsList() {
       key: RULE_3,
       repo: 'repo2',
       name: 'Unknown rule',
+      impacts: [
+        { softwareQuality: SoftwareQuality.Maintainability, severity: SoftwareImpactSeverity.Low },
+      ],
       lang: 'js',
       langName: 'JavaScript',
     }),
@@ -128,6 +136,7 @@ export function mockRuleDetailsList() {
       type: 'VULNERABILITY',
       lang: 'py',
       langName: 'Python',
+      cleanCodeAttributeCategory: CleanCodeAttributeCategory.Consistent,
       name: 'Awsome Python rule',
       descriptionSections: [
         { key: RuleDescriptionSections.INTRODUCTION, content: introTitle },
index 4134304b6c6d5dfc0036c99a2af3af9228ae96ca..a81bc28eee463a7df053dd7a8207d62ba385f46b 100644 (file)
@@ -20,7 +20,7 @@
 import { act, fireEvent, screen } from '@testing-library/react';
 import selectEvent from 'react-select-event';
 import CodingRulesServiceMock, { RULE_TAGS_MOCK } from '../../../api/mocks/CodingRulesServiceMock';
-import { RULE_TYPES } from '../../../helpers/constants';
+import { CLEAN_CODE_CATEGORIES, SOFTWARE_QUALITIES } from '../../../helpers/constants';
 import { parseDate } from '../../../helpers/dates';
 import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
 import { dateInputEvent, renderAppRoutes } from '../../../helpers/testReactTestingUtils';
@@ -57,8 +57,12 @@ describe('Rules app list', () => {
       1
     );
 
-    // Renders type facets
-    RULE_TYPES.map((type) => `issue.type.${type}`).forEach((name) =>
+    // Renders clean code categories and software qualities facets
+    CLEAN_CODE_CATEGORIES.map(
+      (category) => `issue.clean_code_attribute_category.${category}`
+    ).forEach((name) => expect(ui.facetItem(name).get()).toBeInTheDocument());
+
+    SOFTWARE_QUALITIES.map((quality) => `issue.software_quality.${quality}`).forEach((name) =>
       expect(ui.facetItem(name).get()).toBeInTheDocument()
     );
 
@@ -77,6 +81,7 @@ describe('Rules app list', () => {
       ui.availableSinceFacet,
       ui.templateFacet,
       ui.qpFacet,
+      ui.typeFacet,
     ].forEach((facet) => {
       expect(facet.get()).toHaveAttribute('aria-expanded', 'false');
     });
@@ -159,6 +164,31 @@ describe('Rules app list', () => {
         await user.click(ui.facetItem('cute').get());
       });
       expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
+
+      // Clear all filters
+      await act(async () => {
+        await user.click(ui.clearAllFiltersButton.get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11);
+
+      // Filter by clean code category
+      await act(async () => {
+        await user.click(ui.facetItem('issue.clean_code_attribute_category.ADAPTABLE').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+
+      // Filter by software quality
+      await act(async () => {
+        await user.click(ui.facetItem('issue.software_quality.MAINTAINABILITY').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+
+      // Filter by severity
+      await act(async () => {
+        await user.click(ui.severetiesFacet.get());
+        await user.click(ui.facetItem('severity.HIGH').get());
+      });
+      expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(9);
     });
 
     it('filter by standards', async () => {
@@ -368,7 +398,8 @@ describe('Rule app details', () => {
         ui.ruleCleanCodeAttributeCategory(CleanCodeAttributeCategory.Adaptable).get()
       ).toBeInTheDocument();
       expect(ui.ruleCleanCodeAttribute(CleanCodeAttribute.Clear).get()).toBeInTheDocument();
-      expect(ui.ruleSoftwareQuality(SoftwareQuality.Maintainability).get()).toBeInTheDocument();
+      // 1 In Rule details + 1 in facet
+      expect(ui.ruleSoftwareQuality(SoftwareQuality.Maintainability).getAll()).toHaveLength(2);
       expect(document.title).toEqual('page_title.template.with_category.coding_rules.page');
       expect(screen.getByText('Why')).toBeInTheDocument();
       expect(screen.getByText('Because')).toBeInTheDocument();
@@ -683,11 +714,17 @@ describe('redirects', () => {
   });
 
   it('should handle hash parameters', async () => {
-    renderCodingRulesApp(mockLoggedInUser(), 'coding_rules#languages=c,js|types=BUG');
-    // 2 languages
+    const { ui } = getPageObjects();
+
+    renderCodingRulesApp(
+      mockLoggedInUser(),
+      'coding_rules#languages=c,js|types=BUG|cleanCodeAttributeCategories=ADAPTABLE'
+    );
     expect(await screen.findByText('x_selected.2')).toBeInTheDocument();
-    expect(screen.getAllByTitle('issue.type.BUG')).toHaveLength(2);
-    // Only 3 rules shown
+    expect(screen.getByTitle('issue.type.BUG')).toBeInTheDocument();
+    expect(ui.facetItem('issue.clean_code_attribute_category.ADAPTABLE').get()).toBeChecked();
+
+    // Only 2 rules shown
     expect(screen.getByText('x_of_y_shown.2.2')).toBeInTheDocument();
   });
 });
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx
deleted file mode 100644 (file)
index 7754b3e..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 SeverityHelper from '../../../components/shared/SeverityHelper';
-import { SEVERITIES } from '../../../helpers/constants';
-import { translate } from '../../../helpers/l10n';
-import Facet, { BasicProps } from './Facet';
-
-interface Props extends BasicProps {
-  disabled: boolean;
-}
-
-export default class ActivationSeverityFacet extends React.PureComponent<Props> {
-  renderName = (severity: string) => <SeverityHelper severity={severity} />;
-
-  renderTextName = (severity: string) => translate('severity', severity);
-
-  render() {
-    return (
-      <Facet
-        {...this.props}
-        disabled={this.props.disabled}
-        disabledHelper={translate('coding_rules.filters.active_severity.inactive')}
-        halfWidth
-        options={SEVERITIES}
-        property="activationSeverities"
-        renderName={this.renderName}
-        renderTextName={this.renderTextName}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/AttributeCategoryFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/AttributeCategoryFacet.tsx
new file mode 100644 (file)
index 0000000..b0396e8
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { CLEAN_CODE_CATEGORIES } from '../../../helpers/constants';
+import { translate } from '../../../helpers/l10n';
+import Facet, { BasicProps } from './Facet';
+
+export default function AttributeCategoryFacet(props: BasicProps) {
+  const renderName = React.useCallback(
+    (attribute: string) => translate('issue.clean_code_attribute_category', attribute),
+    []
+  );
+
+  return (
+    <Facet
+      {...props}
+      options={CLEAN_CODE_CATEGORIES}
+      property="cleanCodeAttributeCategories"
+      renderName={renderName}
+      renderTextName={renderName}
+    />
+  );
+}
index 3cef1985e9691d90db9a9ce52b01ccee9cf54a9d..3998d0fc355faa99b2fc7e8cb173e236946bfa14 100644 (file)
@@ -58,10 +58,8 @@ import {
   OpenFacets,
   Query,
   areQueriesEqual,
-  getAppFacet,
   getOpen,
   getSelected,
-  getServerFacet,
   hasRuleKey,
   parseQuery,
   serializeQuery,
@@ -114,7 +112,8 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {
         ),
         sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
         standards: shouldOpenStandardsFacet({}, query),
-        types: true,
+        cleanCodeAttributeCategories: true,
+        impactSoftwareQualities: true,
       },
       referencedProfiles: {},
       referencedRepositories: {},
@@ -199,8 +198,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {
     const { openFacets } = this.state;
     return Object.keys(openFacets)
       .filter((facet: FacetKey) => openFacets[facet])
-      .filter((facet: FacetKey) => shouldRequestFacet(facet))
-      .map((facet: FacetKey) => getServerFacet(facet));
+      .filter((facet: FacetKey) => shouldRequestFacet(facet));
   };
 
   getFieldsToFetch = () => {
@@ -214,6 +212,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {
       'sysTags',
       'tags',
       'templateKey',
+      'cleanCodeAttribute',
     ];
     if (parseQuery(this.props.location.query).profile) {
       fields.push('actives', 'params');
@@ -226,7 +225,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {
     facets: this.getFacetsToFetch().join(),
     ps: PAGE_SIZE,
     s: 'name',
-    ...this.props.location.query,
+    ...serializeQuery(parseQuery(this.props.location.query)),
   });
 
   stopLoading = () => {
@@ -299,7 +298,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {
   };
 
   fetchFacet = (facet: FacetKey) => {
-    this.makeFetchRequest({ ps: 1, facets: getServerFacet(facet) }).then(({ facets }) => {
+    this.makeFetchRequest({ ps: 1, facets: facet }).then(({ facets }) => {
       if (this.mounted) {
         this.setState((state) => ({ facets: { ...state.facets, ...facets }, loading: false }));
       }
@@ -711,7 +710,7 @@ function parseFacets(rawFacets: { property: string; values: { count: number; val
     for (const rawValue of rawFacet.values) {
       values[rawValue.val] = rawValue.count;
     }
-    facets[getAppFacet(rawFacet.property)] = values;
+    facets[rawFacet.property as FacetKey] = values;
   }
   return facets;
 }
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/DefaultSeverityFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/DefaultSeverityFacet.tsx
deleted file mode 100644 (file)
index da51af9..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 SeverityHelper from '../../../components/shared/SeverityHelper';
-import { SEVERITIES } from '../../../helpers/constants';
-import { translate } from '../../../helpers/l10n';
-import Facet, { BasicProps } from './Facet';
-
-export default class DefaultSeverityFacet extends React.PureComponent<BasicProps> {
-  renderName = (severity: string) => <SeverityHelper severity={severity} />;
-
-  renderTextName = (severity: string) => translate('severity', severity);
-
-  render() {
-    return (
-      <Facet
-        {...this.props}
-        halfWidth
-        options={SEVERITIES}
-        property="severities"
-        renderName={this.renderName}
-        renderTextName={this.renderTextName}
-      />
-    );
-  }
-}
index 2d43646bba3d5017a4d3a260f42da39a9694945f..4b8cfe0ca1826c192e2eb8ef65c5b02b99e3e267 100644 (file)
@@ -21,13 +21,15 @@ import * as React from 'react';
 import { Profile } from '../../../api/quality-profiles';
 import { Dict } from '../../../types/types';
 import { Facets, OpenFacets, Query } from '../query';
-import ActivationSeverityFacet from './ActivationSeverityFacet';
+import AttributeCategoryFacet from './AttributeCategoryFacet';
 import AvailableSinceFacet from './AvailableSinceFacet';
-import DefaultSeverityFacet from './DefaultSeverityFacet';
 import InheritanceFacet from './InheritanceFacet';
 import LanguageFacet from './LanguageFacet';
 import ProfileFacet from './ProfileFacet';
+import SoftwareQualityFacet from './SoftwareQualityFacet';
+
 import RepositoryFacet from './RepositoryFacet';
+import SeverityFacet from './SeverityFacet';
 import { StandardFacet } from './StandardFacet';
 import StatusFacet from './StatusFacet';
 import TagFacet from './TagFacet';
@@ -54,10 +56,6 @@ export default function FacetsList(props: FacetsListProps) {
     props.selectedProfile === undefined ||
     !props.selectedProfile.isInherited;
 
-  const activationSeverityDisabled =
-    props.query.compareToProfile !== undefined ||
-    props.selectedProfile === undefined ||
-    !props.query.activation;
   return (
     <>
       <LanguageFacet
@@ -68,18 +66,43 @@ export default function FacetsList(props: FacetsListProps) {
         stats={props.facets && props.facets.languages}
         values={props.query.languages}
       />
+
+      <AttributeCategoryFacet
+        onChange={props.onFilterChange}
+        onToggle={props.onFacetToggle}
+        open={!!props.openFacets.cleanCodeAttributeCategories}
+        stats={props.facets?.cleanCodeAttributeCategories}
+        values={props.query.cleanCodeAttributeCategories}
+      />
+
+      <SoftwareQualityFacet
+        onChange={props.onFilterChange}
+        onToggle={props.onFacetToggle}
+        open={!!props.openFacets.impactSoftwareQualities}
+        stats={props.facets?.impactSoftwareQualities}
+        values={props.query.impactSoftwareQualities}
+      />
+
+      <SeverityFacet
+        onChange={props.onFilterChange}
+        onToggle={props.onFacetToggle}
+        open={!!props.openFacets.impactSeverities}
+        stats={props.facets?.impactSeverities}
+        values={props.query.impactSeverities}
+      />
+
       <TypeFacet
         onChange={props.onFilterChange}
         onToggle={props.onFacetToggle}
         open={!!props.openFacets.types}
-        stats={props.facets && props.facets.types}
+        stats={props.facets?.types}
         values={props.query.types}
       />
       <TagFacet
         onChange={props.onFilterChange}
         onToggle={props.onFacetToggle}
         open={!!props.openFacets.tags}
-        stats={props.facets && props.facets.tags}
+        stats={props.facets?.tags}
         values={props.query.tags}
       />
       <RepositoryFacet
@@ -87,27 +110,21 @@ export default function FacetsList(props: FacetsListProps) {
         onToggle={props.onFacetToggle}
         open={!!props.openFacets.repositories}
         referencedRepositories={props.referencedRepositories}
-        stats={props.facets && props.facets.repositories}
+        stats={props.facets?.repositories}
         values={props.query.repositories}
       />
-      <DefaultSeverityFacet
-        onChange={props.onFilterChange}
-        onToggle={props.onFacetToggle}
-        open={!!props.openFacets.severities}
-        stats={props.facets && props.facets.severities}
-        values={props.query.severities}
-      />
+
       <StatusFacet
         onChange={props.onFilterChange}
         onToggle={props.onFacetToggle}
         open={!!props.openFacets.statuses}
-        stats={props.facets && props.facets.statuses}
+        stats={props.facets?.statuses}
         values={props.query.statuses}
       />
       <StandardFacet
         cwe={props.query.cwe}
         cweOpen={!!props.openFacets.cwe}
-        cweStats={props.facets && props.facets.cwe}
+        cweStats={props.facets?.cwe}
         fetchingCwe={false}
         fetchingOwaspTop10={false}
         fetchingOwaspTop10-2021={false}
@@ -117,14 +134,14 @@ export default function FacetsList(props: FacetsListProps) {
         open={!!props.openFacets.standards}
         owaspTop10={props.query.owaspTop10}
         owaspTop10Open={!!props.openFacets.owaspTop10}
-        owaspTop10Stats={props.facets && props.facets.owaspTop10}
+        owaspTop10Stats={props.facets?.owaspTop10}
         owaspTop10-2021={props.query['owaspTop10-2021']}
         owaspTop10-2021Open={!!props.openFacets['owaspTop10-2021']}
-        owaspTop10-2021Stats={props.facets && props.facets['owaspTop10-2021']}
+        owaspTop10-2021Stats={props.facets?.['owaspTop10-2021']}
         query={props.query}
         sonarsourceSecurity={props.query.sonarsourceSecurity}
         sonarsourceSecurityOpen={!!props.openFacets.sonarsourceSecurity}
-        sonarsourceSecurityStats={props.facets && props.facets.sonarsourceSecurity}
+        sonarsourceSecurityStats={props.facets?.sonarsourceSecurity}
       />
       <AvailableSinceFacet
         onChange={props.onFilterChange}
@@ -157,14 +174,6 @@ export default function FacetsList(props: FacetsListProps) {
             open={!!props.openFacets.inheritance}
             value={props.query.inheritance}
           />
-          <ActivationSeverityFacet
-            disabled={activationSeverityDisabled}
-            onChange={props.onFilterChange}
-            onToggle={props.onFacetToggle}
-            open={!!props.openFacets.activationSeverities}
-            stats={props.facets && props.facets.activationSeverities}
-            values={props.query.activationSeverities}
-          />
         </>
       )}
     </>
index 1e6fc871fbfe35f8c6ef946565fb0b5fef3d4d6d..81e9b7b44badaa8376bebe74fc31a0e253a9e2dd 100644 (file)
@@ -74,6 +74,7 @@ class LanguageFacet extends React.PureComponent<Props> {
         getFacetItemText={this.getLanguageName}
         getSearchResultKey={(language) => language.key}
         getSearchResultText={(language) => language.name}
+        maxInitialItems={10}
         minSearchLength={1}
         onChange={this.props.onChange}
         onSearch={this.handleSearch}
index 8c9c32473623a4ff68db68321847f29a7df25e7b..f791e997634610295c346376bfdc6e889d70369d 100644 (file)
@@ -56,7 +56,6 @@ export default class ProfileFacet extends React.PureComponent<Props> {
   handleClear = () =>
     this.props.onChange({
       activation: undefined,
-      activationSeverities: [],
       compareToProfile: undefined,
       inheritance: undefined,
       profile: undefined,
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/SeverityFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/SeverityFacet.tsx
new file mode 100644 (file)
index 0000000..cdb4f11
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 DocumentationTooltip from '../../../components/common/DocumentationTooltip';
+import SoftwareImpactSeverityIcon from '../../../components/icons/SoftwareImpactSeverityIcon';
+import { IMPACT_SEVERITIES } from '../../../helpers/constants';
+import { translate } from '../../../helpers/l10n';
+import Facet, { BasicProps } from './Facet';
+
+export default function SeverityFacet(props: BasicProps) {
+  const renderName = React.useCallback(
+    (severity: string) => (
+      <div className="sw-flex">
+        <SoftwareImpactSeverityIcon severity={severity} />
+        <span className="sw-ml-1">{translate('severity', severity)}</span>
+      </div>
+    ),
+    []
+  );
+
+  const renderTextName = React.useCallback(
+    (severity: string) => translate('severity', severity),
+    []
+  );
+
+  return (
+    <Facet
+      {...props}
+      options={IMPACT_SEVERITIES}
+      property="impactSeverities"
+      renderName={renderName}
+      renderTextName={renderTextName}
+    >
+      <DocumentationTooltip
+        className="spacer-left"
+        placement="right"
+        content={
+          <>
+            <p>{translate('issues.facet.impactSeverities.help.line1')}</p>
+            <p className="sw-mt-2">{translate('issues.facet.impactSeverities.help.line2')}</p>
+          </>
+        }
+        links={[
+          {
+            href: '/user-guide/clean-code',
+            label: translate('learn_more'),
+          },
+        ]}
+      />
+    </Facet>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/SoftwareQualityFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/SoftwareQualityFacet.tsx
new file mode 100644 (file)
index 0000000..59d0da4
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { SOFTWARE_QUALITIES } from '../../../helpers/constants';
+import { translate } from '../../../helpers/l10n';
+import Facet, { BasicProps } from './Facet';
+
+export default function SoftwareQualityFacet(props: BasicProps) {
+  const renderName = React.useCallback(
+    (quality: string) => translate('issue.software_quality', quality),
+    []
+  );
+
+  return (
+    <Facet
+      {...props}
+      options={SOFTWARE_QUALITIES}
+      property="impactSoftwareQualities"
+      renderName={renderName}
+      renderTextName={renderName}
+    />
+  );
+}
index 540ebece00916f79066217177e81fab09c572d47..eb31bc35298c637eed59a775b76c8cddd645b08c 100644 (file)
@@ -24,20 +24,28 @@ import {
   parseAsOptionalBoolean,
   parseAsOptionalString,
   parseAsString,
+  parseImpactSeverityQuery,
   queriesEqual,
   serializeDateShort,
   serializeOptionalBoolean,
   serializeString,
   serializeStringArray,
 } from '../../helpers/query';
+import {
+  CleanCodeAttributeCategory,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
+} from '../../types/clean-code-taxonomy';
 import { Dict, RawQuery, RuleInheritance } from '../../types/types';
 
 export interface Query {
   activation: boolean | undefined;
-  activationSeverities: string[];
   availableSince: Date | undefined;
+  cleanCodeAttributeCategories: CleanCodeAttributeCategory[];
   compareToProfile: string | undefined;
   cwe: string[];
+  impactSeverities: SoftwareImpactSeverity[];
+  impactSoftwareQualities: SoftwareQuality[];
   inheritance: RuleInheritance | undefined;
   languages: string[];
   owaspTop10: string[];
@@ -78,10 +86,18 @@ export interface Actives {
 export function parseQuery(query: RawQuery): Query {
   return {
     activation: parseAsOptionalBoolean(query.activation),
-    activationSeverities: parseAsArray(query.active_severities, parseAsString),
     availableSince: parseAsDate(query.available_since),
+    cleanCodeAttributeCategories: parseAsArray<CleanCodeAttributeCategory>(
+      query.cleanCodeAttributeCategories,
+      parseAsString
+    ),
     compareToProfile: parseAsOptionalString(query.compareToProfile),
     cwe: parseAsArray(query.cwe, parseAsString),
+    impactSeverities: parseImpactSeverityQuery(query.impactSeverities, query.severities),
+    impactSoftwareQualities: parseAsArray<SoftwareQuality>(
+      query.impactSoftwareQualities,
+      parseAsString
+    ),
     inheritance: parseAsInheritance(query.inheritance),
     languages: parseAsArray(query.languages, parseAsString),
     owaspTop10: parseAsArray(query.owaspTop10, parseAsString),
@@ -102,11 +118,13 @@ export function parseQuery(query: RawQuery): Query {
 export function serializeQuery(query: Query): RawQuery {
   return cleanQuery({
     activation: serializeOptionalBoolean(query.activation),
-    active_severities: serializeStringArray(query.activationSeverities),
     available_since: serializeDateShort(query.availableSince),
+    cleanCodeAttributeCategories: serializeStringArray(query.cleanCodeAttributeCategories),
     compareToProfile: serializeString(query.compareToProfile),
     cwe: serializeStringArray(query.cwe),
     inheritance: serializeInheritance(query.inheritance),
+    impactSeverities: serializeStringArray(query.impactSeverities),
+    impactSoftwareQualities: serializeStringArray(query.impactSoftwareQualities),
     is_template: serializeOptionalBoolean(query.template),
     languages: serializeStringArray(query.languages),
     owaspTop10: serializeStringArray(query.owaspTop10),
@@ -115,7 +133,7 @@ export function serializeQuery(query: Query): RawQuery {
     qprofile: serializeString(query.profile),
     repositories: serializeStringArray(query.repositories),
     rule_key: serializeString(query.ruleKey),
-    severities: serializeStringArray(query.severities),
+    severities: undefined,
     sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity),
     statuses: serializeStringArray(query.statuses),
     tags: serializeStringArray(query.tags),
@@ -141,18 +159,13 @@ export function shouldRequestFacet(facet: string): facet is FacetKey {
     'statuses',
     'tags',
     'types',
+    'cleanCodeAttributeCategories',
+    'impactSoftwareQualities',
+    'impactSeverities',
   ];
   return facetsToRequest.includes(facet);
 }
 
-export function getServerFacet(facet: FacetKey) {
-  return facet === 'activationSeverities' ? 'active_severities' : facet;
-}
-
-export function getAppFacet(serverFacet: string): FacetKey {
-  return serverFacet === 'active_severities' ? 'activationSeverities' : (serverFacet as FacetKey);
-}
-
 export function getOpen(query: RawQuery) {
   return query.open;
 }
index 8847ba6f0561ade79b0ae78031af630d1007249a..dd518f5c59015c0c400f92086ab3adc3629bb75b 100644 (file)
@@ -41,11 +41,15 @@ const selectors = {
   clearAllFiltersButton: byRole('button', { name: 'clear_all_filters' }),
 
   // Facets
+  cleanCodeCategoriesFacet: byRole('button', {
+    name: 'coding_rules.facet.cleanCodeAttributeCategories',
+  }),
   languagesFacet: byRole('button', { name: 'coding_rules.facet.languages' }),
   typeFacet: byRole('button', { name: 'coding_rules.facet.types' }),
   tagsFacet: byRole('button', { name: 'coding_rules.facet.tags' }),
   repositoriesFacet: byRole('button', { name: 'coding_rules.facet.repositories' }),
-  severetiesFacet: byRole('button', { name: 'coding_rules.facet.severities' }),
+  softwareQualitiesFacet: byRole('button', { name: 'coding_rules.facet.impactSoftwareQualities' }),
+  severetiesFacet: byRole('button', { name: 'coding_rules.facet.impactSeverities' }),
   statusesFacet: byRole('button', { name: 'coding_rules.facet.statuses' }),
   standardsFacet: byRole('button', { name: 'issues.facet.standards' }),
   standardsOwasp2017Top10Facet: byRole('button', { name: 'issues.facet.owaspTop10' }),
index 28779bd54fbfefded83492165fc1e47798a1f60e..981f502c3d191649e62dd2af65aa1e6b7c88a2fa 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 import * as React from 'react';
+import { CLEAN_CODE_CATEGORIES } from '../../../helpers/constants';
 import { CleanCodeAttributeCategory } from '../../../types/clean-code-taxonomy';
 import { CommonProps, SimpleListStyleFacet } from './SimpleListStyleFacet';
 
@@ -26,8 +27,6 @@ interface Props extends CommonProps {
   categories: Array<CleanCodeAttributeCategory>;
 }
 
-const CATEGORIES = Object.values(CleanCodeAttributeCategory);
-
 export function AttributeCategoryFacet(props: Props) {
   const { categories = [], ...rest } = props;
 
@@ -35,7 +34,7 @@ export function AttributeCategoryFacet(props: Props) {
     <SimpleListStyleFacet
       property="cleanCodeAttributeCategories"
       itemNamePrefix="issue.clean_code_attribute_category"
-      listItems={CATEGORIES}
+      listItems={CLEAN_CODE_CATEGORIES}
       selectedItems={categories}
       {...rest}
     />
index 4b9fba328839b31525a875a62059182d7ad085d7..6247269a42a42d979b05badf82757b770645dde0 100644 (file)
@@ -21,6 +21,7 @@
 import * as React from 'react';
 import DocumentationTooltip from '../../../components/common/DocumentationTooltip';
 import SoftwareImpactSeverityIcon from '../../../components/icons/SoftwareImpactSeverityIcon';
+import { IMPACT_SEVERITIES } from '../../../helpers/constants';
 import { translate } from '../../../helpers/l10n';
 import { SoftwareImpactSeverity } from '../../../types/clean-code-taxonomy';
 import { CommonProps, SimpleListStyleFacet } from './SimpleListStyleFacet';
@@ -29,8 +30,6 @@ interface Props extends CommonProps {
   severities: SoftwareImpactSeverity[];
 }
 
-const SEVERITIES = Object.values(SoftwareImpactSeverity);
-
 export function SeverityFacet(props: Props) {
   const { severities = [], ...rest } = props;
 
@@ -38,7 +37,7 @@ export function SeverityFacet(props: Props) {
     <SimpleListStyleFacet
       property="impactSeverities"
       itemNamePrefix="severity"
-      listItems={SEVERITIES}
+      listItems={IMPACT_SEVERITIES}
       selectedItems={severities}
       renderIcon={(severity: string) => <SoftwareImpactSeverityIcon severity={severity} />}
       help={
index 2888116505eb2783c6076c64da5a833088447ea0..0c94d42c751d979a3b08e94697b8951a9145a68a 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 import * as React from 'react';
+import { SOFTWARE_QUALITIES } from '../../../helpers/constants';
 import { SoftwareQuality } from '../../../types/clean-code-taxonomy';
 import { CommonProps, SimpleListStyleFacet } from './SimpleListStyleFacet';
 
@@ -26,8 +27,6 @@ interface Props extends CommonProps {
   qualities: Array<SoftwareQuality>;
 }
 
-const QUALITIES = Object.values(SoftwareQuality);
-
 export function SoftwareQualityFacet(props: Props) {
   const { qualities = [], ...rest } = props;
 
@@ -35,7 +34,7 @@ export function SoftwareQualityFacet(props: Props) {
     <SimpleListStyleFacet
       property="impactSoftwareQualities"
       itemNamePrefix="issue.software_quality"
-      listItems={QUALITIES}
+      listItems={SOFTWARE_QUALITIES}
       selectedItems={qualities}
       {...rest}
     />
index 31220895075367f48914ef33939f89fc08e4af4e..f3f9c40661b779b7141d120cc08109b424879053 100644 (file)
@@ -17,7 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { compact, isArray, uniq } from 'lodash';
+import { isArray } from 'lodash';
 import { getUsers } from '../../api/users';
 import { formatMeasure } from '../../helpers/measures';
 import {
@@ -26,6 +26,7 @@ import {
   parseAsBoolean,
   parseAsDate,
   parseAsString,
+  parseImpactSeverityQuery,
   queriesEqual,
   serializeDateShort,
   serializeString,
@@ -134,29 +135,6 @@ export function parseQuery(query: RawQuery): Query {
   };
 }
 
-function parseImpactSeverityQuery(
-  newSeverities: string,
-  oldSeverities?: string
-): SoftwareImpactSeverity[] {
-  const OLD_TO_NEW_MAPPER = {
-    BLOCKER: SoftwareImpactSeverity.High,
-    CRITICAL: SoftwareImpactSeverity.High,
-    MAJOR: SoftwareImpactSeverity.Medium,
-    MINOR: SoftwareImpactSeverity.Low,
-    INFO: SoftwareImpactSeverity.Low,
-  };
-
-  // Merging new and old severities includes mapping for old to new
-  return compact(
-    uniq([
-      ...parseAsArray<SoftwareImpactSeverity>(newSeverities, parseAsString),
-      ...parseAsArray(oldSeverities, parseAsString).map(
-        (oldSeverity: string) => OLD_TO_NEW_MAPPER[oldSeverity as keyof typeof OLD_TO_NEW_MAPPER]
-      ),
-    ])
-  );
-}
-
 export function getOpen(query: RawQuery): string | undefined {
   return query.open;
 }
index e741e3084ee728e5f3eb88bf90e70dcab675cb68..35f16da7730456c1c2eb6ec909095d53d57b09a1 100644 (file)
@@ -91,7 +91,7 @@ export default class FacetHeader extends React.PureComponent<Props> {
         {this.props.onClick ? (
           <span className="search-navigator-facet-header display-flex-center">
             <button
-              className="button-link"
+              className="button-link display-flex-center"
               type="button"
               onClick={this.handleClick}
               aria-expanded={open}
index 828f442b4088690086eb4e71ee244ca7eb75fd4f..17615740c754542981877fd7036cc998d9a6698b 100644 (file)
  */
 import { colors } from '../app/theme';
 import { AlmKeys } from '../types/alm-settings';
+import {
+  CleanCodeAttributeCategory,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
+} from '../types/clean-code-taxonomy';
 import { ComponentQualifier } from '../types/component';
 import { IssueResolution, IssueScope, IssueSeverity, IssueType } from '../types/issues';
 import { RuleType } from '../types/types';
 
 export const SEVERITIES = Object.values(IssueSeverity);
 
+export const IMPACT_SEVERITIES = Object.values(SoftwareImpactSeverity);
+
+export const CLEAN_CODE_CATEGORIES = Object.values(CleanCodeAttributeCategory);
+
+export const SOFTWARE_QUALITIES = Object.values(SoftwareQuality);
+
 export const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED'];
 
 export const ISSUE_TYPES: IssueType[] = [
index 37ced126f60bd1df7a54e6157852f9fb26c8b55c..97238fc4ab807fc4b78565bad6e421c8c6169e76 100644 (file)
@@ -17,7 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { isEqual, isNil, omitBy } from 'lodash';
+import { compact, isEqual, isNil, omitBy, uniq } from 'lodash';
+import { SoftwareImpactSeverity } from '../types/clean-code-taxonomy';
 import { RawQuery } from '../types/types';
 import { isValidDate, parseDate, toISO8601WithOffsetString, toShortISO8601String } from './dates';
 
@@ -125,3 +126,26 @@ export function serializeOptionalBoolean(value: boolean | undefined): string | u
   }
   return undefined;
 }
+
+export function parseImpactSeverityQuery(
+  newSeverities: string,
+  oldSeverities?: string
+): SoftwareImpactSeverity[] {
+  const OLD_TO_NEW_MAPPER = {
+    BLOCKER: SoftwareImpactSeverity.High,
+    CRITICAL: SoftwareImpactSeverity.High,
+    MAJOR: SoftwareImpactSeverity.Medium,
+    MINOR: SoftwareImpactSeverity.Low,
+    INFO: SoftwareImpactSeverity.Low,
+  };
+
+  // Merging new and old severities includes mapping for old to new
+  return compact(
+    uniq([
+      ...parseAsArray<SoftwareImpactSeverity>(newSeverities, parseAsString),
+      ...parseAsArray(oldSeverities, parseAsString).map(
+        (oldSeverity: string) => OLD_TO_NEW_MAPPER[oldSeverity as keyof typeof OLD_TO_NEW_MAPPER]
+      ),
+    ])
+  );
+}
index 4e85cb1e1a7bfe9566b2507db4239914205174c0..11128fdcf368228a12f5f38eaa2b94264eb1c563 100644 (file)
@@ -29,6 +29,7 @@ export interface SearchRulesQuery {
   active_severities?: string;
   asc?: boolean | string;
   available_since?: string;
+  cleanCodeAttributeCategories?: string;
   cwe?: string;
   f?: string;
   facets?: string;
@@ -45,7 +46,8 @@ export interface SearchRulesQuery {
   repositories?: string;
   rule_key?: string;
   s?: string;
-  severities?: string;
+  impactSoftwareQualities?: string;
+  impactSeverities?: string;
   sonarsourceSecurity?: string;
   statuses?: string;
   tags?: string;
index bab27408f09144000f1de17988052fe21c619c7d..aa26d7d3192f6a52dac6bfa4bb2c911a52674df0 100644 (file)
@@ -533,9 +533,9 @@ export interface QualityGate {
 export type RawQuery = Dict<any>;
 
 export interface Rule {
-  cleanCodeAttributeCategory: CleanCodeAttributeCategory;
-  cleanCodeAttribute: CleanCodeAttribute;
-  impacts: Array<{
+  cleanCodeAttributeCategory?: CleanCodeAttributeCategory;
+  cleanCodeAttribute?: CleanCodeAttribute;
+  impacts?: Array<{
     softwareQuality: SoftwareQuality;
     severity: SoftwareImpactSeverity;
   }>;
index 50d43f925e08dc5af7e7370116cfa9dbb74b55e4..113a731445c73437121286b0b0f5b19759a56a9d 100644 (file)
@@ -2311,6 +2311,9 @@ coding_rules.filters.template.is_not_template=Hide Templates
 
 coding_rules.facet.languages=Language
 coding_rules.facet.repositories=Repository
+coding_rules.facet.impactSeverities=Severity
+coding_rules.facet.cleanCodeAttributeCategories=Clean Code Attribute
+coding_rules.facet.impactSoftwareQualities=Software Quality
 coding_rules.facet.tags=Tag
 coding_rules.facet.qprofile=Quality Profile
 coding_rules.facet.qprofile.help=Quality Profiles are collections of Rules to apply during an analysis.