diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2023-07-31 09:27:54 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-08-18 20:02:47 +0000 |
commit | 0b0f4049a9e8f42b9a7800109904cdbffe2eb348 (patch) | |
tree | 6ae88fb0332837cfe0e34def142fce62e6d650ad /server | |
parent | 4c74112ad0b65d9f7d48354579fce0b5dd74869f (diff) | |
download | sonarqube-0b0f4049a9e8f42b9a7800109904cdbffe2eb348.tar.gz sonarqube-0b0f4049a9e8f42b9a7800109904cdbffe2eb348.zip |
SONAR-20023 New CCT facets for issues
Diffstat (limited to 'server')
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 { |