aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2023-07-31 09:27:54 +0200
committersonartech <sonartech@sonarsource.com>2023-08-18 20:02:47 +0000
commit0b0f4049a9e8f42b9a7800109904cdbffe2eb348 (patch)
tree6ae88fb0332837cfe0e34def142fce62e6d650ad /server
parent4c74112ad0b65d9f7d48354579fce0b5dd74869f (diff)
downloadsonarqube-0b0f4049a9e8f42b9a7800109904cdbffe2eb348.tar.gz
sonarqube-0b0f4049a9e8f42b9a7800109904cdbffe2eb348.zip
SONAR-20023 New CCT facets for issues
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/design-system/src/components/FacetBox.tsx6
-rw-r--r--server/sonar-web/src/main/js/api/issues.ts5
-rw-r--r--server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts141
-rw-r--r--server/sonar-web/src/main/js/api/mocks/data/issues.ts15
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx52
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AttributeCategoryFacet.tsx43
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx153
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/SimpleListStyleFacet.tsx116
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/SoftwareQualityFacet.tsx43
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/SimpleListStyleFacet-test.tsx77
-rw-r--r--server/sonar-web/src/main/js/apps/issues/test-utils.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/issues/utils.ts23
-rw-r--r--server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx5
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/issues.ts3
-rw-r--r--server/sonar-web/src/main/js/helpers/query.ts4
-rw-r--r--server/sonar-web/src/main/js/types/issues.ts2
21 files changed, 555 insertions, 250 deletions
diff --git a/server/sonar-web/design-system/src/components/FacetBox.tsx b/server/sonar-web/design-system/src/components/FacetBox.tsx
index 5736f58d0e1..845ec380ecb 100644
--- a/server/sonar-web/design-system/src/components/FacetBox.tsx
+++ b/server/sonar-web/design-system/src/components/FacetBox.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/api/issues.ts b/server/sonar-web/src/main/js/api/issues.ts
index e1996afbdf5..43191fea418 100644
--- a/server/sonar-web/src/main/js/api/issues.ts
+++ b/server/sonar-web/src/main/js/api/issues.ts
@@ -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);
}
diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
index 3ee95b5ef5c..f337fbb1931 100644
--- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
@@ -20,7 +20,13 @@
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
@@ -414,6 +384,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;
}
diff --git a/server/sonar-web/src/main/js/api/mocks/data/issues.ts b/server/sonar-web/src/main/js/api/mocks/data/issues.ts
index db54959bdab..c4dd9c4ddd0 100644
--- a/server/sonar-web/src/main/js/api/mocks/data/issues.ts
+++ b/server/sonar-web/src/main/js/api/mocks/data/issues.ts
@@ -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,
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx
index 9366af56078..05d1e31b1ef 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx
@@ -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(
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
index 51b08bd94b0..8b5a8f1084b 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
@@ -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', () => {
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
index 8668a496f6c..6985751a43c 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
@@ -17,6 +17,11 @@
* 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',
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
index 4c09972fd5a..f9bf57cbff1 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
@@ -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
index 00000000000..4797ecb36ad
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AttributeCategoryFacet.tsx
@@ -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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
index cdf9b373ea6..5ada1e50f08 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
@@ -18,126 +18,45 @@
* 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}
+ />
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
index 6e0262b31a3..b9a274df52d 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
@@ -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
index 00000000000..c52151e6722
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SimpleListStyleFacet.tsx
@@ -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
index 00000000000..2851cc06a60
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SoftwareQualityFacet.tsx
@@ -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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx
index 863e0be2ecd..f2aad1fba96 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx
@@ -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
index 00000000000..35b4dbf5c16
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/SimpleListStyleFacet-test.tsx
@@ -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} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx
index 764d733fa16..fc243e293f1 100644
--- a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx
@@ -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' }),
diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts
index dd499df95d5..72dbf2fa062 100644
--- a/server/sonar-web/src/main/js/apps/issues/utils.ts
+++ b/server/sonar-web/src/main/js/apps/issues/utils.ts
@@ -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),
diff --git a/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx b/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx
index 4202874771a..d69e35f1b67 100644
--- a/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx
+++ b/server/sonar-web/src/main/js/components/common/DocumentationTooltip.tsx
@@ -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={
diff --git a/server/sonar-web/src/main/js/helpers/mocks/issues.ts b/server/sonar-web/src/main/js/helpers/mocks/issues.ts
index a09e5e8c3ac..b5d6fef9ddd 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/issues.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/issues.ts
@@ -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: '',
diff --git a/server/sonar-web/src/main/js/helpers/query.ts b/server/sonar-web/src/main/js/helpers/query.ts
index 615534add4e..37ced126f60 100644
--- a/server/sonar-web/src/main/js/helpers/query.ts
+++ b/server/sonar-web/src/main/js/helpers/query.ts
@@ -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 {
diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts
index bfc40585d85..a3fca94b673 100644
--- a/server/sonar-web/src/main/js/types/issues.ts
+++ b/server/sonar-web/src/main/js/types/issues.ts
@@ -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 {