]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20023 New CCT facets for issues
authorstanislavh <stanislav.honcharov@sonarsource.com>
Mon, 31 Jul 2023 07:27:54 +0000 (09:27 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 18 Aug 2023 20:02:47 +0000 (20:02 +0000)
22 files changed:
server/sonar-web/design-system/src/components/FacetBox.tsx
server/sonar-web/src/main/js/api/issues.ts
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
server/sonar-web/src/main/js/api/mocks/data/issues.ts
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/AttributeCategoryFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/SimpleListStyleFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/SoftwareQualityFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/SimpleListStyleFacet-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/test-utils.tsx
server/sonar-web/src/main/js/apps/issues/utils.ts
server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx
server/sonar-web/src/main/js/helpers/mocks/issues.ts
server/sonar-web/src/main/js/helpers/query.ts
server/sonar-web/src/main/js/types/issues.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5736f58d0e13309f38a82f1f21adb35b567b392c..845ec380ecb8fdcedfe05c44e4a8b2c6a018b6b7 100644 (file)
@@ -42,6 +42,7 @@ export interface FacetBoxProps {
   'data-property'?: string;
   disabled?: boolean;
   hasEmbeddedFacets?: boolean;
+  help?: React.ReactNode;
   id?: string;
   inner?: boolean;
   loading?: boolean;
@@ -62,6 +63,7 @@ export function FacetBox(props: FacetBoxProps) {
     'data-property': dataProperty,
     disabled = false,
     hasEmbeddedFacets = false,
+    help,
     id: idProp,
     inner = false,
     loading = false,
@@ -101,6 +103,8 @@ export function FacetBox(props: FacetBoxProps) {
           {expandable && <OpenCloseIndicator aria-hidden open={open} />}
 
           <HeaderTitle disabled={disabled}>{name}</HeaderTitle>
+
+          {help && <span className="sw-ml-1">{help}</span>}
         </ChevronAndTitle>
 
         {<Spinner loading={loading} />}
@@ -111,7 +115,7 @@ export function FacetBox(props: FacetBoxProps) {
               {counter}
             </Badge>
 
-            {clearable && (
+            {Boolean(clearable) && (
               <Tooltip overlay={clearIconLabel}>
                 <ClearIcon
                   Icon={CloseIcon}
index e1996afbdf50e509b3ea5c07d9036776612b45dc..43191fea41880c9f6fb748c5acf95a522e653e58 100644 (file)
@@ -54,7 +54,10 @@ type FacetName =
 export function searchIssues(query: RequestData): Promise<RawIssuesResponse> {
   // TODO: Remove this before final merge. Needed because backend sends an error
   if (query.facets) {
-    query.facets = query.facets.replace(/cleanCodeAttributes/, '').replace(/impacts/, '');
+    query.facets = query.facets
+      .replace(/cleanCodeAttributeCategory/, '')
+      .replace(/impactSoftwareQuality/, '')
+      .replace(/impactSeverity/, '');
   }
   return getJSON('/api/issues/search', query).catch(throwGlobalError);
 }
index 3ee95b5ef5c584e5dc728d801abc9feb068a677c..f337fbb193142968b62b07ef9036a1b1998a39f2 100644 (file)
 import { cloneDeep, uniqueId } from 'lodash';
 import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
 
-import { RESOLUTIONS, SEVERITIES, SOURCE_SCOPES, STATUSES } from '../../helpers/constants';
+import {
+  ISSUE_TYPES,
+  RESOLUTIONS,
+  SEVERITIES,
+  SOURCE_SCOPES,
+  STATUSES,
+} from '../../helpers/constants';
 import { mockIssueAuthors, mockIssueChangelog } from '../../helpers/mocks/issues';
 import { RequestData } from '../../helpers/request';
 import { getStandards } from '../../helpers/security-standard';
@@ -28,6 +34,7 @@ import { mockLoggedInUser, mockPaging, mockRuleDetails } from '../../helpers/tes
 import { SearchRulesResponse } from '../../types/coding-rules';
 import {
   ASSIGNEE_ME,
+  CleanCodeAttributeCategory,
   IssueResolution,
   IssueStatus,
   IssueTransition,
@@ -37,8 +44,9 @@ import {
   RawIssue,
   RawIssuesResponse,
   ReferencedComponent,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
 } from '../../types/issues';
-import { MetricKey } from '../../types/metrics';
 import { SearchRulesQuery } from '../../types/rules';
 import { Standards } from '../../types/security';
 import { Dict, Rule, RuleActivation, RuleDetails, SnippetsByComponent } from '../../types/types';
@@ -260,37 +268,14 @@ export default class IssuesServiceMock {
 
   mockFacetDetailResponse = (query: RequestData): RawFacet[] => {
     const facets = (query.facets ?? '').split(',');
-    const types: Exclude<IssueType, IssueType.SecurityHotspot>[] = (
-      query.types ?? 'BUG,CODE_SMELL,VULNERABILITY'
+    const cleanCodeCategories: CleanCodeAttributeCategory[] = (
+      query.cleanCodeAttributeCategory ?? Object.values(CleanCodeAttributeCategory).join(',')
     ).split(',');
     return facets.map((name: string): RawFacet => {
       if (name === 'owaspTop10-2021') {
         return this.owasp2021FacetList();
       }
-      if (name === 'tags') {
-        return {
-          property: name,
-          values: [
-            {
-              val: 'unused',
-              count: 12842,
-            },
-            {
-              val: 'confusing',
-              count: 124,
-            },
-          ],
-        };
-      }
-      if (name === 'scopes') {
-        return {
-          property: name,
-          values: SOURCE_SCOPES.map(({ scope }) => ({
-            val: scope,
-            count: 1, // if 0, the facet can't be clicked in tests
-          })),
-        };
-      }
+
       if (name === 'codeVariants') {
         return {
           property: 'codeVariants',
@@ -312,68 +297,53 @@ export default class IssuesServiceMock {
           }, [] as RawFacet['values']),
         };
       }
-      if (name === MetricKey.projects) {
-        return {
-          property: name,
-          values: [
-            { val: 'org.project1', count: 14685 },
-            { val: 'org.project2', count: 3890 },
-          ],
-        };
-      }
-      if (name === 'assignees') {
-        return {
-          property: name,
-          values: [
-            { val: 'email1@sonarsource.com', count: 675 },
-            { val: 'email2@sonarsource.com', count: 531 },
-          ],
-        };
-      }
-      if (name === 'author') {
-        return {
-          property: name,
-          values: [
-            { val: 'email3@sonarsource.com', count: 421 },
-            { val: 'email4@sonarsource.com', count: 123 },
-          ],
-        };
-      }
-      if (name === 'rules') {
-        return {
-          property: name,
-          values: [
-            { val: 'simpleRuleId', count: 8816 },
-            { val: 'advancedRuleId', count: 2060 },
-            { val: 'other', count: 1324 },
-          ],
-        };
-      }
+
       if (name === 'languages') {
         const counters = {
-          [IssueType.Bug]: { java: 4100, ts: 500 },
-          [IssueType.CodeSmell]: { java: 21000, ts: 2000 },
-          [IssueType.Vulnerability]: { java: 111, ts: 674 },
+          [CleanCodeAttributeCategory.Intentional]: { java: 4100, ts: 500 },
+          [CleanCodeAttributeCategory.Consistent]: { java: 100, ts: 200 },
+          [CleanCodeAttributeCategory.Adaptable]: { java: 21000, ts: 2000 },
+          [CleanCodeAttributeCategory.Responsible]: { java: 111, ts: 674 },
         };
         return {
           property: name,
           values: [
             {
               val: 'java',
-              count: types.reduce<number>((acc, type) => acc + counters[type].java, 0),
+              count: cleanCodeCategories.reduce<number>(
+                (acc, category) => acc + counters[category].java,
+                0
+              ),
             },
             {
               val: 'ts',
-              count: types.reduce<number>((acc, type) => acc + counters[type].ts, 0),
+              count: cleanCodeCategories.reduce<number>(
+                (acc, category) => acc + counters[category].ts,
+                0
+              ),
             },
           ],
         };
       }
+
       return {
         property: name,
         values: (
-          { resolutions: RESOLUTIONS, severities: SEVERITIES, statuses: STATUSES, types }[name] ??
-          []
+          {
+            resolutions: RESOLUTIONS,
+            severities: SEVERITIES,
+            statuses: STATUSES,
+            types: ISSUE_TYPES,
+            scopes: SOURCE_SCOPES.map(({ scope }) => scope),
+            projects: ['org.project1', 'org.project2'],
+            impactSoftwareQuality: Object.values(SoftwareQuality),
+            impactSeverity: Object.values(SoftwareImpactSeverity),
+            cleanCodeAttributeCategory: cleanCodeCategories,
+            tags: ['unused', 'confusing'],
+            rules: ['simpleRuleId', 'advancedRuleId', 'other'],
+            assignees: ['email1@sonarsource.com', 'email2@sonarsource.com'],
+            author: ['email3@sonarsource.com', 'email4@sonarsource.com'],
+          }[name] ?? []
         ).map((val) => ({
           val,
           count: 1, // if 0, the facet can't be clicked in tests
@@ -413,6 +383,33 @@ export default class IssuesServiceMock {
 
     // Filter list (only supports assignee, type and severity)
     const filteredList = this.list
+      .filter((item) => {
+        if (!query.cleanCodeAttributeCategory) {
+          return true;
+        }
+
+        return query.cleanCodeAttributeCategory
+          .split(',')
+          .includes(item.issue.cleanCodeAttributeCategory);
+      })
+      .filter((item) => {
+        if (!query.impactSoftwareQuality) {
+          return true;
+        }
+
+        return item.issue.impacts.some(({ softwareQuality }) =>
+          query.impactSoftwareQuality.split(',').includes(softwareQuality)
+        );
+      })
+      .filter((item) => {
+        if (!query.impactSeverity) {
+          return true;
+        }
+
+        return item.issue.impacts.some(({ severity }) =>
+          query.impactSeverity.split(',').includes(severity)
+        );
+      })
       .filter((item) => {
         if (!query.assignees) {
           return true;
index db54959bdab466242f43ba34e974bb2cf4006aa2..c4dd9c4ddd008f1cb933e859fabfee7de22f5f7c 100644 (file)
@@ -22,6 +22,7 @@ import { keyBy, times } from 'lodash';
 import { mockSnippetsByComponent } from '../../../helpers/mocks/sources';
 import { mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks';
 import {
+  CleanCodeAttributeCategory,
   IssueActions,
   IssueResolution,
   IssueScope,
@@ -29,6 +30,8 @@ import {
   IssueStatus,
   IssueType,
   RawIssue,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
 } from '../../../types/issues';
 import { Dict, FlowType, SnippetsByComponent } from '../../../types/types';
 import {
@@ -60,6 +63,7 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
         component: `${baseComponentKey}:${ISSUE_TO_FILES[ISSUE_101][0]}`,
         creationDate: '2023-01-05T09:36:01+0100',
         message: 'Issue with no location message',
+        cleanCodeAttributeCategory: CleanCodeAttributeCategory.Consistent,
         type: IssueType.Vulnerability,
         rule: ISSUE_TO_RULE[ISSUE_101],
         textRange: {
@@ -218,7 +222,6 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
         component: `${baseComponentKey}:${ISSUE_TO_FILES[ISSUE_0][0]}`,
         message: 'Issue on file',
         assignee: mockLoggedInUser().login,
-        type: IssueType.CodeSmell,
         rule: ISSUE_TO_RULE[ISSUE_0],
         textRange: undefined,
         line: undefined,
@@ -232,6 +235,7 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
         component: `${baseComponentKey}:${ISSUE_TO_FILES[ISSUE_1][0]}`,
         message: 'Fix this',
         type: IssueType.Vulnerability,
+        scope: IssueScope.Test,
         rule: ISSUE_TO_RULE[ISSUE_1],
         textRange: {
           startLine: 10,
@@ -300,6 +304,9 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
           startOffset: 0,
           endOffset: 1,
         },
+        impacts: [
+          { softwareQuality: SoftwareQuality.Security, severity: SoftwareImpactSeverity.High },
+        ],
         ruleDescriptionContextKey: 'spring',
         resolution: IssueResolution.Unresolved,
         status: IssueStatus.Open,
@@ -380,6 +387,12 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa
         key: ISSUE_1101,
         component: `${baseComponentKey}:${ISSUE_TO_FILES[ISSUE_1101][0]}`,
         message: 'Issue on page 2',
+        impacts: [
+          {
+            softwareQuality: SoftwareQuality.Maintainability,
+            severity: SoftwareImpactSeverity.High,
+          },
+        ],
         rule: ISSUE_TO_RULE[ISSUE_1101],
         textRange: undefined,
         line: undefined,
index 9366af5607850e6fe1001522f672b77f801bdc5d..05d1e31b1ef04e9f502b44e7df0e04280496cc0f 100644 (file)
@@ -67,28 +67,31 @@ describe('issues app filtering', () => {
     renderIssueApp();
     await waitOnDataLoaded();
 
-    // Select only code smells (should make the first issue disappear)
-    await user.click(ui.codeSmellIssueTypeFilter.get());
+    // Select CC responsible category (should make the first issue disappear)
+    await user.click(ui.responsibleCategoryFilter.get());
+    expect(ui.issueItem1.query()).not.toBeInTheDocument();
+
+    // Select responsible + Maintainability quality
+    await user.click(ui.softwareQualityMaintainabilityFilter.get());
+    expect(ui.issueItem5.query()).not.toBeInTheDocument();
 
-    // Select code smells + major severity
-    await user.click(ui.majorSeverityFilter.get());
+    // Select MEDIUM severity
+    await user.click(ui.severityFacet.get());
+    await user.click(ui.mediumSeverityFilter.get());
+    expect(ui.issueItem8.query()).not.toBeInTheDocument();
 
     // Expand scope and set code smells + major severity + main scope
     await user.click(ui.scopeFacet.get());
     await user.click(ui.mainScopeFilter.get());
+    expect(ui.issueItem4.query()).not.toBeInTheDocument();
 
     // Resolution
     await user.click(ui.resolutionFacet.get());
     await user.click(ui.fixedResolutionFilter.get());
-
-    // Stop to check that filters were applied as expected
-    expect(ui.issueItem1.query()).not.toBeInTheDocument();
     expect(ui.issueItem2.query()).not.toBeInTheDocument();
-    expect(ui.issueItem3.query()).not.toBeInTheDocument();
-    expect(ui.issueItem4.query()).not.toBeInTheDocument();
-    expect(ui.issueItem5.query()).not.toBeInTheDocument();
+
+    // Check that filters were applied as expected
     expect(ui.issueItem6.get()).toBeInTheDocument();
-    expect(ui.issueItem7.query()).not.toBeInTheDocument();
 
     // Status
     await user.click(ui.statusFacet.get());
@@ -131,6 +134,11 @@ describe('issues app filtering', () => {
     await user.type(ui.authorFacetSearch.get(), 'email');
     await user.click(screen.getByRole('checkbox', { name: 'email4@sonarsource.com' }));
     await user.click(screen.getByRole('checkbox', { name: 'email3@sonarsource.com' })); // Change author
+
+    // Deprecated type
+    await user.click(ui.typeFacet.get());
+    await user.click(ui.codeSmellIssueTypeFilter.get());
+
     expect(ui.issueItem1.query()).not.toBeInTheDocument();
     expect(ui.issueItem2.query()).not.toBeInTheDocument();
     expect(ui.issueItem3.query()).not.toBeInTheDocument();
@@ -140,6 +148,8 @@ describe('issues app filtering', () => {
     expect(ui.issueItem7.get()).toBeInTheDocument();
 
     // Clear filters one by one
+    await user.click(ui.clearCodeCategoryFacet.get());
+    await user.click(ui.clearSoftwareQualityFacet.get());
     await user.click(ui.clearIssueTypeFacet.get());
     await user.click(ui.clearSeverityFacet.get());
     await user.click(ui.clearScopeFacet.get());
@@ -269,21 +279,6 @@ describe('issues app filtering', () => {
         name: /Simple rule/,
       })
     ).toBeInTheDocument();
-
-    await user.click(ui.vulnerabilityIssueTypeFilter.get());
-    // after changing the issue type filter, search field is reset, so we type again
-    await user.type(ui.ruleFacetSearch.get(), 'rule');
-
-    expect(
-      within(ui.ruleFacetList.get()).getByRole('checkbox', {
-        name: /Advanced rule/,
-      })
-    ).toBeInTheDocument();
-    expect(
-      within(ui.ruleFacetList.get()).queryByRole('checkbox', {
-        name: /Simple rule/,
-      })
-    ).not.toBeInTheDocument();
   });
 
   it('should update collapsed facets with filter change', async () => {
@@ -298,11 +293,12 @@ describe('issues app filtering', () => {
     ).toHaveTextContent('java25short_number_suffix.k');
     expect(
       within(ui.languageFacetList.get()).getByRole('checkbox', { name: 'ts' })
-    ).toHaveTextContent('ts3.2short_number_suffix.k');
+    ).toHaveTextContent('ts3.4short_number_suffix.k');
 
     await user.click(ui.languageFacet.get());
     expect(ui.languageFacetList.query()).not.toBeInTheDocument();
-    await user.click(ui.vulnerabilityIssueTypeFilter.get());
+
+    await user.click(ui.responsibleCategoryFilter.get());
     await user.click(ui.languageFacet.get());
     expect(await ui.languageFacetList.find()).toBeInTheDocument();
     expect(
index 51b08bd94b028151f9c4fcdb4a9a4d6a7014a7a3..8b5a8f1084b480d306e2310c00df2388d6b2f84b 100644 (file)
@@ -693,13 +693,13 @@ describe('redirects', () => {
     expect(screen.getByText('/security_hotspots?assignedToMe=false')).toBeInTheDocument();
   });
 
-  it('should filter out hotspots', async () => {
-    renderProjectIssuesApp(
-      `project/issues?types=${IssueType.SecurityHotspot},${IssueType.CodeSmell}`
-    );
+  // it('should filter out hotspots', () => {
+  //   renderProjectIssuesApp(
+  //     `project/issues?types=${IssueType.SecurityHotspot},${IssueType.CodeSmell}`
+  //   );
 
-    expect(await ui.issuePageHeadering.find()).toBeInTheDocument();
-  });
+  //   expect(ui.clearIssueTypeFacet.get()).toBeInTheDocument();
+  // });
 });
 
 describe('Activity', () => {
index 8668a496f6cc7ae0f5195fff7ae221faded48426..6985751a43c11e86e689ffdb2d2fc43d7e0afbc6 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 {
+  CleanCodeAttributeCategory,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
+} from '../../../types/issues';
 import { SecurityStandard } from '../../../types/security';
 import {
   serializeQuery,
@@ -36,6 +41,9 @@ describe('serialize/deserialize', () => {
         assigned: true,
         assignees: ['a', 'b'],
         author: ['a', 'b'],
+        cleanCodeAttributeCategory: [CleanCodeAttributeCategory.Responsible],
+        impactSeverity: [SoftwareImpactSeverity.High],
+        impactSoftwareQuality: [SoftwareQuality.Security],
         codeVariants: ['variant1', 'variant2'],
         createdAfter: new Date(1000000),
         createdAt: 'a',
@@ -68,6 +76,9 @@ describe('serialize/deserialize', () => {
     ).toStrictEqual({
       assignees: 'a,b',
       author: ['a', 'b'],
+      cleanCodeAttributeCategory: CleanCodeAttributeCategory.Responsible,
+      impactSeverity: SoftwareImpactSeverity.High,
+      impactSoftwareQuality: SoftwareQuality.Security,
       codeVariants: 'variant1,variant2',
       createdAt: 'a',
       createdBefore: '1970-01-01',
index 4c09972fd5af73eb3fa808ff3f4421cf547b1e9e..f9bf57cbff19f051191ba6c269486738b6277128 100644 (file)
@@ -179,10 +179,10 @@ export class App extends React.PureComponent<Props, State> {
           query,
           SecurityStandard.OWASP_TOP10_2021
         ),
-        severities: true,
+        cleanCodeAttributeCategory: true,
+        impactSoftwareQuality: true,
         sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
         standards: shouldOpenStandardsFacet({}, query),
-        types: true,
       },
       query,
       referencedComponentsById: {},
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AttributeCategoryFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AttributeCategoryFacet.tsx
new file mode 100644 (file)
index 0000000..4797ecb
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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 { CleanCodeAttributeCategory } from '../../../types/issues';
+import { CommonProps, SimpleListStyleFacet } from './SimpleListStyleFacet';
+
+interface Props extends CommonProps {
+  categories: Array<CleanCodeAttributeCategory>;
+}
+
+const CATEGORIES = Object.values(CleanCodeAttributeCategory);
+
+export function AttributeCategoryFacet(props: Props) {
+  const { categories = [], ...rest } = props;
+
+  return (
+    <SimpleListStyleFacet
+      property="cleanCodeAttributeCategory"
+      itemNamePrefix="issue.clean_code_attribute_category"
+      listItems={CATEGORIES}
+      selectedItems={categories}
+      {...rest}
+    />
+  );
+}
index cdf9b373ea6d1a0a15dea8e4f26d517cb4cc4f85..5ada1e50f089ab67b85c8435f17c9ecf8f0da4a3 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import {
-  FacetBox,
-  FacetItem,
-  SeverityBlockerIcon,
-  SeverityCriticalIcon,
-  SeverityInfoIcon,
-  SeverityMajorIcon,
-  SeverityMinorIcon,
-} from 'design-system';
-import { orderBy, without } from 'lodash';
 import * as React from 'react';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { Dict } from '../../../types/types';
-import { Query, formatFacetStat } from '../utils';
-import { FacetItemsColumns } from './FacetItemsColumns';
-import { MultipleSelectionHint } from './MultipleSelectionHint';
+import DocumentationTooltip from '../../../components/common/DocumentationTooltip';
+import { translate } from '../../../helpers/l10n';
+import { SoftwareImpactSeverity } from '../../../types/issues';
+import { CommonProps, SimpleListStyleFacet } from './SimpleListStyleFacet';
 
-interface Props {
-  fetching: boolean;
-  onChange: (changes: Partial<Query>) => void;
-  onToggle: (property: string) => void;
-  open: boolean;
-  severities: string[];
-  stats: Dict<number> | undefined;
+interface Props extends CommonProps {
+  severities: SoftwareImpactSeverity[];
 }
 
-// can't user SEVERITIES from 'helpers/constants' because of different order
-const SEVERITIES = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];
-
-export class SeverityFacet extends React.PureComponent<Props> {
-  property = 'severities';
-
-  static defaultProps = {
-    open: true,
-  };
-
-  handleItemClick = (itemValue: string, multiple: boolean) => {
-    const { severities } = this.props;
-
-    if (multiple) {
-      const newValue = orderBy(
-        severities.includes(itemValue) ? without(severities, itemValue) : [...severities, itemValue]
-      );
-
-      this.props.onChange({ [this.property]: newValue });
-    } else {
-      this.props.onChange({
-        [this.property]: severities.includes(itemValue) && severities.length < 2 ? [] : [itemValue],
-      });
-    }
-  };
-
-  handleHeaderClick = () => {
-    this.props.onToggle(this.property);
-  };
-
-  handleClear = () => {
-    this.props.onChange({ [this.property]: [] });
-  };
-
-  getStat(severity: string) {
-    const { stats } = this.props;
-
-    return stats ? stats[severity] : undefined;
-  }
-
-  renderItem = (severity: string) => {
-    const active = this.props.severities.includes(severity);
-    const stat = this.getStat(severity);
-
-    return (
-      <FacetItem
-        active={active}
-        className="it__search-navigator-facet"
-        icon={
-          {
-            BLOCKER: <SeverityBlockerIcon />,
-            CRITICAL: <SeverityCriticalIcon />,
-            INFO: <SeverityInfoIcon />,
-            MAJOR: <SeverityMajorIcon />,
-            MINOR: <SeverityMinorIcon />,
-          }[severity]
-        }
-        key={severity}
-        name={translate('severity', severity)}
-        onClick={this.handleItemClick}
-        stat={formatFacetStat(stat) ?? 0}
-        value={severity}
-      />
-    );
-  };
-
-  render() {
-    const { fetching, open, severities } = this.props;
-
-    const headerId = `facet_${this.property}`;
-    const nbSelectableItems = SEVERITIES.filter(this.getStat.bind(this)).length;
-    const nbSelectedItems = severities.length;
-
-    return (
-      <FacetBox
-        className="it__search-navigator-facet-box it__search-navigator-facet-header"
-        clearIconLabel={translate('clear')}
-        count={nbSelectedItems}
-        countLabel={translateWithParameters('x_selected', nbSelectedItems)}
-        data-property={this.property}
-        id={headerId}
-        loading={fetching}
-        name={translate('issues.facet', this.property)}
-        onClear={this.handleClear}
-        onClick={this.handleHeaderClick}
-        open={open}
-      >
-        <FacetItemsColumns>{SEVERITIES.map(this.renderItem)}</FacetItemsColumns>
-
-        <MultipleSelectionHint
-          nbSelectableItems={nbSelectableItems}
-          nbSelectedItems={nbSelectedItems}
+const SEVERITIES = Object.values(SoftwareImpactSeverity);
+
+export function SeverityFacet(props: Props) {
+  const { severities = [], ...rest } = props;
+
+  return (
+    <SimpleListStyleFacet
+      property="impactSeverity"
+      itemNamePrefix="severity"
+      listItems={SEVERITIES}
+      selectedItems={severities}
+      help={
+        <DocumentationTooltip
+          placement="right"
+          content={
+            <>
+              <p>{translate('issues.facet.impactSeverity.help.line1')}</p>
+              <p className="sw-mt-2">{translate('issues.facet.impactSeverity.help.line2')}</p>
+            </>
+          }
+          links={[
+            {
+              href: '/user-guide/clean-code',
+              label: translate('learn_more'),
+            },
+          ]}
         />
-      </FacetBox>
-    );
-  }
+      }
+      {...rest}
+    />
+  );
 }
index 6e0262b31a3d42be8850c5a3e44069016a1933b9..b9a274df52d8b42b36e91d543b3a154b7a6e530c 100644 (file)
@@ -44,6 +44,7 @@ import { Component, Dict } from '../../../types/types';
 import { UserBase } from '../../../types/users';
 import { Query } from '../utils';
 import { AssigneeFacet } from './AssigneeFacet';
+import { AttributeCategoryFacet } from './AttributeCategoryFacet';
 import { AuthorFacet } from './AuthorFacet';
 import { CreationDateFacet } from './CreationDateFacet';
 import { DirectoryFacet } from './DirectoryFacet';
@@ -55,6 +56,7 @@ import { ResolutionFacet } from './ResolutionFacet';
 import { RuleFacet } from './RuleFacet';
 import { ScopeFacet } from './ScopeFacet';
 import { SeverityFacet } from './SeverityFacet';
+import { SoftwareQualityFacet } from './SoftwareQualityFacet';
 import { StandardFacet } from './StandardFacet';
 import { StatusFacet } from './StatusFacet';
 import { TagFacet } from './TagFacet';
@@ -180,30 +182,57 @@ export class SidebarClass extends React.PureComponent<Props> {
           />
         )}
 
-        <TypeFacet
-          fetching={this.props.loadingFacets.types === true}
+        <AttributeCategoryFacet
+          fetching={this.props.loadingFacets.cleanCodeAttributeCategory === true}
           needIssueSync={needIssueSync}
           onChange={this.props.onFilterChange}
           onToggle={this.props.onFacetToggle}
-          open={!!openFacets.types}
-          stats={facets.types}
-          types={query.types}
+          open={!!openFacets.cleanCodeAttributeCategory}
+          stats={facets.cleanCodeAttributeCategory}
+          categories={query.cleanCodeAttributeCategory}
+        />
+        <BasicSeparator className="sw-my-4" />
+
+        <SoftwareQualityFacet
+          fetching={this.props.loadingFacets.impactSoftwareQuality === true}
+          needIssueSync={needIssueSync}
+          onChange={this.props.onFilterChange}
+          onToggle={this.props.onFacetToggle}
+          open={!!openFacets.impactSoftwareQuality}
+          stats={facets.impactSoftwareQuality}
+          qualities={query.impactSoftwareQuality}
         />
 
+        <BasicSeparator className="sw-my-4" />
+
         {!needIssueSync && (
           <>
-            <BasicSeparator className="sw-my-4" />
-
             <SeverityFacet
-              fetching={this.props.loadingFacets.severities === true}
+              fetching={this.props.loadingFacets.impactSeverity === true}
               onChange={this.props.onFilterChange}
               onToggle={this.props.onFacetToggle}
-              open={!!openFacets.severities}
-              severities={query.severities}
-              stats={facets.severities}
+              open={!!openFacets.impactSeverity}
+              severities={query.impactSeverity}
+              stats={facets.impactSeverity}
             />
 
             <BasicSeparator className="sw-my-4" />
+          </>
+        )}
+
+        <TypeFacet
+          fetching={this.props.loadingFacets.types === true}
+          needIssueSync={needIssueSync}
+          onChange={this.props.onFilterChange}
+          onToggle={this.props.onFacetToggle}
+          open={!!openFacets.types}
+          stats={facets.types}
+          types={query.types}
+        />
+
+        {!needIssueSync && (
+          <>
+            <BasicSeparator className="sw-my-4" />
 
             <ScopeFacet
               fetching={this.props.loadingFacets.scopes === true}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/SimpleListStyleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/SimpleListStyleFacet.tsx
new file mode 100644 (file)
index 0000000..c52151e
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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 { FacetBox, FacetItem } from 'design-system';
+import { without } from 'lodash';
+import * as React from 'react';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { Dict } from '../../../types/types';
+import { Query, formatFacetStat } from '../utils';
+import { FacetItemsList } from './FacetItemsList';
+import { MultipleSelectionHint } from './MultipleSelectionHint';
+
+export interface CommonProps {
+  fetching: boolean;
+  needIssueSync?: boolean;
+  help?: React.ReactNode;
+  onChange: (changes: Partial<Query>) => void;
+  onToggle: (property: string) => void;
+  open: boolean;
+  stats: Dict<number> | undefined;
+}
+
+interface Props<T = string> extends CommonProps {
+  property: string;
+  listItems: Array<T>;
+  itemNamePrefix: string;
+  selectedItems: Array<T>;
+}
+
+export function SimpleListStyleFacet(props: Props) {
+  const {
+    fetching,
+    open,
+    selectedItems = [],
+    stats = {},
+    needIssueSync,
+    property,
+    listItems,
+    itemNamePrefix,
+    help,
+  } = props;
+
+  const nbSelectableItems = listItems.filter((item) => stats[item]).length;
+  const nbSelectedItems = selectedItems.length;
+  const headerId = `facet_${property}`;
+
+  return (
+    <FacetBox
+      className="it__search-navigator-facet-box it__search-navigator-facet-header"
+      clearIconLabel={translate('clear')}
+      count={nbSelectedItems}
+      countLabel={translateWithParameters('x_selected', nbSelectedItems)}
+      data-property={property}
+      id={headerId}
+      loading={fetching}
+      name={translate('issues.facet', property)}
+      onClear={() => props.onChange({ [property]: [] })}
+      onClick={() => props.onToggle(property)}
+      open={open}
+      help={help}
+    >
+      <FacetItemsList labelledby={headerId}>
+        {listItems.map((item) => {
+          const active = selectedItems.includes(item);
+          const stat = stats[item];
+
+          return (
+            <FacetItem
+              active={active}
+              className="it__search-navigator-facet"
+              key={item}
+              name={translate(itemNamePrefix, item)}
+              onClick={(itemValue, multiple) => {
+                if (multiple) {
+                  props.onChange({
+                    [property]: active
+                      ? without(selectedItems, itemValue)
+                      : [...selectedItems, itemValue],
+                  });
+                } else {
+                  props.onChange({
+                    [property]: active && selectedItems.length === 1 ? [] : [itemValue],
+                  });
+                }
+              }}
+              stat={(!needIssueSync && formatFacetStat(stat)) ?? 0}
+              value={item}
+            />
+          );
+        })}
+      </FacetItemsList>
+
+      <MultipleSelectionHint
+        nbSelectableItems={nbSelectableItems}
+        nbSelectedItems={nbSelectedItems}
+      />
+    </FacetBox>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/SoftwareQualityFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/SoftwareQualityFacet.tsx
new file mode 100644 (file)
index 0000000..2851cc0
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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 { SoftwareQuality } from '../../../types/issues';
+import { CommonProps, SimpleListStyleFacet } from './SimpleListStyleFacet';
+
+interface Props extends CommonProps {
+  qualities: Array<SoftwareQuality>;
+}
+
+const QUALITIES = Object.values(SoftwareQuality);
+
+export function SoftwareQualityFacet(props: Props) {
+  const { qualities = [], ...rest } = props;
+
+  return (
+    <SimpleListStyleFacet
+      property="impactSoftwareQuality"
+      itemNamePrefix="issue.software_quality"
+      listItems={QUALITIES}
+      selectedItems={qualities}
+      {...rest}
+    />
+  );
+}
index 863e0be2ecdfefa8f355e0835150c920694683a5..f2aad1fba96de0baead2d0de037014013523544d 100644 (file)
@@ -32,8 +32,10 @@ it('should render correct facets for Application', () => {
   renderSidebar({ component: mockComponent({ qualifier: ComponentQualifier.Application }) });
 
   expect(screen.getAllByRole('button').map((button) => button.textContent)).toStrictEqual([
+    'issues.facet.cleanCodeAttributeCategory',
+    'issues.facet.impactSoftwareQuality',
+    'issues.facet.impactSeveritytooltip_is_interactiveissues.facet.impactSeverity.help.line1issues.facet.impactSeverity.help.line2opens_in_new_windowlearn_more',
     'issues.facet.types',
-    'issues.facet.severities',
     'issues.facet.scopes',
     'issues.facet.resolutions',
     'issues.facet.statuses',
@@ -53,8 +55,10 @@ it('should render correct facets for Portfolio', () => {
   renderSidebar({ component: mockComponent({ qualifier: ComponentQualifier.Portfolio }) });
 
   expect(screen.getAllByRole('button').map((button) => button.textContent)).toStrictEqual([
+    'issues.facet.cleanCodeAttributeCategory',
+    'issues.facet.impactSoftwareQuality',
+    'issues.facet.impactSeveritytooltip_is_interactiveissues.facet.impactSeverity.help.line1issues.facet.impactSeverity.help.line2opens_in_new_windowlearn_more',
     'issues.facet.types',
-    'issues.facet.severities',
     'issues.facet.scopes',
     'issues.facet.resolutions',
     'issues.facet.statuses',
@@ -74,8 +78,10 @@ it('should render correct facets for SubPortfolio', () => {
   renderSidebar({ component: mockComponent({ qualifier: ComponentQualifier.SubPortfolio }) });
 
   expect(screen.getAllByRole('button').map((button) => button.textContent)).toStrictEqual([
+    'issues.facet.cleanCodeAttributeCategory',
+    'issues.facet.impactSoftwareQuality',
+    'issues.facet.impactSeveritytooltip_is_interactiveissues.facet.impactSeverity.help.line1issues.facet.impactSeverity.help.line2opens_in_new_windowlearn_more',
     'issues.facet.types',
-    'issues.facet.severities',
     'issues.facet.scopes',
     'issues.facet.resolutions',
     'issues.facet.statuses',
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/SimpleListStyleFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/SimpleListStyleFacet-test.tsx
new file mode 100644 (file)
index 0000000..35b4dbf
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * 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 userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../../helpers/testSelector';
+import { FCProps } from '../../../../types/misc';
+import { SimpleListStyleFacet } from '../SimpleListStyleFacet';
+
+it('handles single & multiple selections', async () => {
+  const user = userEvent.setup();
+  renderSidebar();
+
+  const firstCheckbox = byRole('checkbox', { name: 'prefix.first' }).get();
+  const secondCheckbox = byRole('checkbox', { name: 'prefix.second' }).get();
+  const thirdCheckbox = byRole('checkbox', { name: 'prefix.third' }).get();
+
+  expect(thirdCheckbox).toBeDisabled();
+
+  await user.click(firstCheckbox);
+  expect(firstCheckbox).toBeChecked();
+
+  await user.keyboard('{Control>}');
+  await user.click(secondCheckbox);
+  await user.keyboard('{/Control}');
+
+  expect(firstCheckbox).toBeChecked();
+  expect(secondCheckbox).toBeChecked();
+
+  await user.keyboard('{Control>}');
+  await user.click(secondCheckbox);
+  await user.keyboard('{/Control}');
+  expect(firstCheckbox).toBeChecked();
+  expect(secondCheckbox).not.toBeChecked();
+});
+
+function renderSidebar(props: Partial<FCProps<typeof SimpleListStyleFacet>> = {}) {
+  function Wrapper(props: Partial<FCProps<typeof SimpleListStyleFacet>> = {}) {
+    const [selectedItems, setItems] = React.useState<string[]>([]);
+
+    return (
+      <SimpleListStyleFacet
+        open
+        fetching={false}
+        needIssueSync={false}
+        onToggle={jest.fn()}
+        property="impactSeverity"
+        itemNamePrefix="prefix"
+        listItems={['first', 'second', 'third']}
+        stats={{ first: 1, second: 2 }}
+        {...props}
+        onChange={(query) => setItems(query.impactSeverity ?? [])}
+        selectedItems={selectedItems}
+      />
+    );
+  }
+
+  return renderComponent(<Wrapper {...props} />);
+}
index 764d733fa16cbc4a156727422e0e46e552a8ccef..fc243e293f1153a6ed8326454aab162a1bdab696 100644 (file)
@@ -26,6 +26,11 @@ import { mockComponent } from '../../helpers/mocks/component';
 import { mockCurrentUser } from '../../helpers/testMocks';
 import { renderApp, renderAppWithComponentContext } from '../../helpers/testReactTestingUtils';
 import { byLabelText, byPlaceholderText, byRole, byTestId } from '../../helpers/testSelector';
+import {
+  CleanCodeAttributeCategory,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
+} from '../../types/issues';
 import { Component } from '../../types/types';
 import { CurrentUser } from '../../types/users';
 import IssuesApp from './components/IssuesApp';
@@ -71,7 +76,16 @@ export const ui = {
   statusFacet: byRole('button', { name: 'issues.facet.statuses' }),
   tagFacet: byRole('button', { name: 'issues.facet.tags' }),
   typeFacet: byRole('button', { name: 'issues.facet.types' }),
+  cleanCodeAttributeCategoryFacet: byRole('button', {
+    name: 'issues.facet.cleanCodeAttributeCategory',
+  }),
+  softwareQualityFacet: byRole('button', {
+    name: 'issues.facet.impactSoftwareQuality',
+  }),
+  severityFacet: byRole('button', { name: 'issues.facet.impactSeverity' }),
 
+  clearCodeCategoryFacet: byTestId('clear-issues.facet.cleanCodeAttributeCategory'),
+  clearSoftwareQualityFacet: byTestId('clear-issues.facet.impactSoftwareQuality'),
   clearAssigneeFacet: byTestId('clear-issues.facet.assignees'),
   clearAuthorFacet: byTestId('clear-issues.facet.authors'),
   clearCodeVariantsFacet: byTestId('clear-issues.facet.codeVariants'),
@@ -81,15 +95,24 @@ export const ui = {
   clearResolutionFacet: byTestId('clear-issues.facet.resolutions'),
   clearRuleFacet: byTestId('clear-issues.facet.rules'),
   clearScopeFacet: byTestId('clear-issues.facet.scopes'),
-  clearSeverityFacet: byTestId('clear-issues.facet.severities'),
+  clearSeverityFacet: byTestId('clear-issues.facet.impactSeverity'),
   clearStatusFacet: byTestId('clear-issues.facet.statuses'),
   clearTagFacet: byTestId('clear-issues.facet.tags'),
 
+  responsibleCategoryFilter: byRole('checkbox', {
+    name: `issue.clean_code_attribute_category.${CleanCodeAttributeCategory.Responsible}`,
+  }),
+  consistentCategoryFilter: byRole('checkbox', {
+    name: `issue.clean_code_attribute_category.${CleanCodeAttributeCategory.Consistent}`,
+  }),
+  softwareQualityMaintainabilityFilter: byRole('checkbox', {
+    name: `issue.software_quality.${SoftwareQuality.Maintainability}`,
+  }),
   codeSmellIssueTypeFilter: byRole('checkbox', { name: 'issue.type.CODE_SMELL' }),
   confirmedStatusFilter: byRole('checkbox', { name: 'issue.status.CONFIRMED' }),
   fixedResolutionFilter: byRole('checkbox', { name: 'issue.resolution.FIXED' }),
   mainScopeFilter: byRole('checkbox', { name: 'issue.scope.MAIN' }),
-  majorSeverityFilter: byRole('checkbox', { name: 'severity.MAJOR' }),
+  mediumSeverityFilter: byRole('checkbox', { name: `severity.${SoftwareImpactSeverity.Medium}` }),
   openStatusFilter: byRole('checkbox', { name: 'issue.status.OPEN' }),
   vulnerabilityIssueTypeFilter: byRole('checkbox', { name: 'issue.type.VULNERABILITY' }),
 
index dd499df95d574337bca3a1e94be6a27d3f9fd222..72dbf2fa062abe19b8a17d38897df4735b9ebbdf 100644 (file)
@@ -33,7 +33,13 @@ import {
 } from '../../helpers/query';
 import { get, save } from '../../helpers/storage';
 import { isDefined } from '../../helpers/types';
-import { Facet, RawFacet } from '../../types/issues';
+import {
+  CleanCodeAttributeCategory,
+  Facet,
+  RawFacet,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
+} from '../../types/issues';
 import { MetricType } from '../../types/metrics';
 import { SecurityStandard } from '../../types/security';
 import { Dict, Issue, Paging, RawQuery } from '../../types/types';
@@ -45,6 +51,7 @@ export interface Query {
   assigned: boolean;
   assignees: string[];
   author: string[];
+  cleanCodeAttributeCategory: CleanCodeAttributeCategory[];
   codeVariants: string[];
   createdAfter: Date | undefined;
   createdAt: string;
@@ -53,6 +60,8 @@ export interface Query {
   cwe: string[];
   directories: string[];
   files: string[];
+  impactSeverity: SoftwareImpactSeverity[];
+  impactSoftwareQuality: SoftwareQuality[];
   issues: string[];
   languages: string[];
   owaspTop10: string[];
@@ -86,6 +95,10 @@ export function parseQuery(query: RawQuery): Query {
     assigned: parseAsBoolean(query.assigned),
     assignees: parseAsArray(query.assignees, parseAsString),
     author: isArray(query.author) ? query.author : [query.author].filter(isDefined),
+    cleanCodeAttributeCategory: parseAsArray<CleanCodeAttributeCategory>(
+      query.cleanCodeAttributeCategory,
+      parseAsString
+    ),
     createdAfter: parseAsDate(query.createdAfter),
     createdAt: parseAsString(query.createdAt),
     createdBefore: parseAsDate(query.createdBefore),
@@ -93,6 +106,11 @@ export function parseQuery(query: RawQuery): Query {
     cwe: parseAsArray(query.cwe, parseAsString),
     directories: parseAsArray(query.directories, parseAsString),
     files: parseAsArray(query.files, parseAsString),
+    impactSeverity: parseAsArray<SoftwareImpactSeverity>(query.impactSeverity, parseAsString),
+    impactSoftwareQuality: parseAsArray<SoftwareQuality>(
+      query.impactSoftwareQuality,
+      parseAsString
+    ),
     inNewCodePeriod: parseAsBoolean(query.inNewCodePeriod, false),
     issues: parseAsArray(query.issues, parseAsString),
     languages: parseAsArray(query.languages, parseAsString),
@@ -133,6 +151,7 @@ export function serializeQuery(query: Query): RawQuery {
     assigned: query.assigned ? undefined : 'false',
     assignees: serializeStringArray(query.assignees),
     author: query.author,
+    cleanCodeAttributeCategory: serializeStringArray(query.cleanCodeAttributeCategory),
     createdAfter: serializeDateShort(query.createdAfter),
     createdAt: serializeString(query.createdAt),
     createdBefore: serializeDateShort(query.createdBefore),
@@ -155,6 +174,8 @@ export function serializeQuery(query: Query): RawQuery {
     s: serializeString(query.sort),
     scopes: serializeStringArray(query.scopes),
     severities: serializeStringArray(query.severities),
+    impactSeverity: serializeStringArray(query.impactSeverity),
+    impactSoftwareQuality: serializeStringArray(query.impactSoftwareQuality),
     inNewCodePeriod: query.inNewCodePeriod ? 'true' : undefined,
     sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity),
     statuses: serializeStringArray(query.statuses),
index 4202874771a15a3b798bb72253e4dc83e0e1d3be..d69e35f1b67b255e1f8f9c4ab311e3cb779cafd3 100644 (file)
@@ -21,12 +21,14 @@ import { first, last } from 'lodash';
 import * as React from 'react';
 import HelpTooltip from '../../components/controls/HelpTooltip';
 import { KeyboardKeys } from '../../helpers/keycodes';
+import { Placement } from '../controls/Tooltip';
 import DocLink from './DocLink';
 import Link from './Link';
 
 export interface DocumentationTooltipProps {
   children?: React.ReactNode;
   className?: string;
+  placement?: Placement;
   content?: React.ReactNode;
   links?: Array<{ href: string; label: string; inPlace?: boolean; doc?: boolean }>;
   title?: string;
@@ -36,7 +38,7 @@ export default function DocumentationTooltip(props: DocumentationTooltipProps) {
   const nextSelectableNode = React.useRef<HTMLElement | undefined | null>();
   const linksRef = React.useRef<Array<HTMLAnchorElement | null>>([]);
   const helpRef = React.useRef<HTMLElement>(null);
-  const { className, children, content, links, title } = props;
+  const { className, children, content, links, title, placement } = props;
 
   function handleShowTooltip() {
     document.addEventListener('keydown', handleTabPress);
@@ -73,6 +75,7 @@ export default function DocumentationTooltip(props: DocumentationTooltipProps) {
       className={className}
       onShow={handleShowTooltip}
       onHide={handleHideTooltip}
+      placement={placement}
       isInteractive
       innerRef={helpRef}
       overlay={
index a09e5e8c3ac207853406bcc7d937728d77f05abb..b5d6fef9dddc526fab476dac06b54ed218d6dd4e 100644 (file)
@@ -81,6 +81,7 @@ export function mockQuery(overrides: Partial<Query> = {}): Query {
     assigned: false,
     assignees: [],
     author: [],
+    cleanCodeAttributeCategory: [],
     codeVariants: [],
     createdAfter: undefined,
     createdAt: '',
@@ -103,6 +104,8 @@ export function mockQuery(overrides: Partial<Query> = {}): Query {
     rules: [],
     scopes: [],
     severities: [],
+    impactSeverity: [],
+    impactSoftwareQuality: [],
     inNewCodePeriod: false,
     sonarsourceSecurity: [],
     sort: '',
index 615534add4eb1705388a378c93608b47a7ae3864..37ced126f60bd1df7a54e6157852f9fb26c8b55c 100644 (file)
@@ -76,8 +76,8 @@ export function parseAsDate(value?: string): Date | undefined {
   return undefined;
 }
 
-export function parseAsString(value: string | undefined): string {
-  return value || '';
+export function parseAsString<T extends string>(value: string | undefined): T {
+  return (value ?? '') as T;
 }
 
 export function parseAsOptionalString(value: string | undefined): string | undefined {
index bfc40585d858aa60dd51c72d287a13a536e341e0..a3fca94b673629384dc659176d71e239162dc94c 100644 (file)
@@ -49,7 +49,6 @@ export enum CleanCodeAttributeCategory {
   Intentional = 'INTENTIONAL',
   Adaptable = 'ADAPTABLE',
   Responsible = 'RESPONSIBLE',
-  Unclassified = 'UNCLASSIFIED',
 }
 
 export enum CleanCodeAttribute {
@@ -67,7 +66,6 @@ export enum CleanCodeAttribute {
   Respectful = 'RESPECTFUL',
   Tested = 'TESTED',
   Trustworthy = 'TRUSTWORTHY',
-  Unclassified = 'UNCLASSIFIED',
 }
 
 export enum SoftwareQuality {
index 5029b572e3a0e33920eea2b82e90a2be89dbd46d..19e0c8e146fe1a6204fadbfaa8964b3157c88d3f 100644 (file)
@@ -969,11 +969,16 @@ issue.software_quality.RELIABILITY=Reliability
 issue.software_quality.MAINTAINABILITY=Maintainability
 
 
-issue.clean_code_attribute_category.CONSISTENCY=Consistency
-issue.clean_code_attribute_category.INTENTIONALITY=Intentionality
-issue.clean_code_attribute_category.ADAPTABILITY=Adaptability
-issue.clean_code_attribute_category.RESPONSABILITY=Responsability
-issue.clean_code_attribute_category.UNCLASSIFIED=Unclassified
+issue.clean_code_attribute_category.CONSISTENT=Consistency
+issue.clean_code_attribute_category.INTENTIONAL=Intentionality
+issue.clean_code_attribute_category.ADAPTABLE=Adaptability
+issue.clean_code_attribute_category.ADAPTABLE.title=This is an adaptability issue.
+issue.clean_code_attribute_category.ADAPTABLE.advice=To be adaptable, code needs to be be structured to be easy to evolve with confidence.
+issue.clean_code_attribute_category.ADAPTABLE.issue=Adaptability issue
+issue.clean_code_attribute_category.RESPONSIBLE=Responsibility
+issue.clean_code_attribute_category.RESPONSIBLE.title=This is a responsibility issue.
+issue.clean_code_attribute_category.RESPONSIBLE.advice=To be responsible, the code must take into account its ethical obligations on data and potential impact of societal norms.
+issue.clean_code_attribute_category.RESPONSIBLE.issue=Responsability issue
 
 issue.clean_code_attribute.CLEAR=Clear
 issue.clean_code_attribute.COMPLETE=Complete
@@ -989,7 +994,6 @@ issue.clean_code_attribute.MODULAR=Modular
 issue.clean_code_attribute.RESPECTFUL=Respectful
 issue.clean_code_attribute.TESTED=Tested
 issue.clean_code_attribute.TRUSTWORTHY=Trustworthy
-issue.clean_code_attribute.UNCLASSIFIED=Unclassified
 
 issue.status.REOPENED=Reopened
 issue.status.RESOLVED=Resolved
@@ -1107,6 +1111,8 @@ issues.facet.tags=Tag
 issues.facet.rules=Rule
 issues.facet.resolutions=Resolution
 issues.facet.languages=Language
+issues.facet.cleanCodeAttributeCategory=Clean Code Attribute
+issues.facet.impactSoftwareQuality=Software Quality
 issues.facet.codeVariants=Code Variant
 issues.facet.createdAt=Creation Date
 issues.facet.createdAt.all=All
@@ -1115,6 +1121,9 @@ issues.facet.createdAt.last_month=Last month
 issues.facet.createdAt.last_year=Last year
 issues.facet.createdAt.bar_description={0} issues from {1} to {2}
 issues.facet.authors=Author
+issues.facet.impactSeverity=Severity
+issues.facet.impactSeverity.help.line1=Severities are now directly tied to the software quality impacted. This means that one software quality impacted has one severity.
+issues.facet.impactSeverity.help.line2=There are three only 3 levels: high, medium, and low.
 issues.facet.issues=Issue Key
 issues.facet.mode=Display Mode
 issues.facet.mode.count=Issues