diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2023-08-18 14:57:33 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-08-22 20:03:05 +0000 |
commit | 46741d608660feeaf3af8aef4980e085b3e2ba5c (patch) | |
tree | 2abcc8cf3bdbf7091daf67f89b711529299751ae /server | |
parent | 8e85496afd806079b9125f351cf5574f65ff8147 (diff) | |
download | sonarqube-46741d608660feeaf3af8aef4980e085b3e2ba5c.tar.gz sonarqube-46741d608660feeaf3af8aef4980e085b3e2ba5c.zip |
SONAR-20197 Rules facet filters use CCT
Diffstat (limited to 'server')
21 files changed, 312 insertions, 147 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts index df68b91786c..d22c7cc83e0 100644 --- a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts @@ -67,12 +67,13 @@ jest.mock('../quality-profiles'); type FacetFilter = Pick< SearchRulesQuery, + | 'impactSeverities' + | 'impactSoftwareQualities' | 'languages' | 'tags' | 'available_since' | 'q' | 'types' - | 'severities' | 'repositories' | 'qprofile' | 'activation' @@ -81,6 +82,7 @@ type FacetFilter = Pick< | 'owaspTop10-2021' | 'cwe' | 'is_template' + | 'cleanCodeAttributeCategories' >; const FACET_RULE_MAP: { [key: string]: keyof Rule } = { @@ -151,10 +153,12 @@ export default class CodingRulesServiceMock { } filterFacet({ + impactSeverities, + impactSoftwareQualities, + cleanCodeAttributeCategories, languages, available_since, q, - severities, types, tags, is_template, @@ -167,15 +171,31 @@ export default class CodingRulesServiceMock { activation, }: FacetFilter) { let filteredRules = this.rules; + if (cleanCodeAttributeCategories) { + filteredRules = filteredRules.filter( + (r) => + r.cleanCodeAttributeCategory && + cleanCodeAttributeCategories.includes(r.cleanCodeAttributeCategory) + ); + } + if (impactSoftwareQualities) { + filteredRules = filteredRules.filter( + (r) => + r.impacts && + r.impacts.some(({ softwareQuality }) => impactSoftwareQualities.includes(softwareQuality)) + ); + } + if (impactSeverities) { + filteredRules = filteredRules.filter( + (r) => r.impacts && r.impacts.some(({ severity }) => impactSeverities.includes(severity)) + ); + } if (types) { filteredRules = filteredRules.filter((r) => types.includes(r.type)); } if (languages) { filteredRules = filteredRules.filter((r) => r.lang && languages.includes(r.lang)); } - if (severities) { - filteredRules = filteredRules.filter((r) => r.severity && severities.includes(r.severity)); - } if (qprofile) { const qProfileLang = this.qualityProfile.find((p) => p.key === qprofile)?.language; filteredRules = filteredRules @@ -365,7 +385,8 @@ export default class CodingRulesServiceMock { p, ps, available_since, - severities, + impactSeverities, + impactSoftwareQualities, repositories, qprofile, sonarsourceSecurity, @@ -377,6 +398,7 @@ export default class CodingRulesServiceMock { rule_key, is_template, activation, + cleanCodeAttributeCategories, }: SearchRulesQuery): Promise<SearchRulesResponse> => { const standards = await getStandards(); const facetCounts: Array<{ property: string; values: { val: string; count: number }[] }> = []; @@ -424,7 +446,9 @@ export default class CodingRulesServiceMock { languages, available_since, q, - severities, + impactSeverities, + impactSoftwareQualities, + cleanCodeAttributeCategories, repositories, types, tags, diff --git a/server/sonar-web/src/main/js/api/mocks/data/rules.ts b/server/sonar-web/src/main/js/api/mocks/data/rules.ts index 785966acdb7..63855d8ac1e 100644 --- a/server/sonar-web/src/main/js/api/mocks/data/rules.ts +++ b/server/sonar-web/src/main/js/api/mocks/data/rules.ts @@ -21,6 +21,11 @@ import { RuleDescriptionSections } from '../../../apps/coding-rules/rule'; import { mockRule, mockRuleActivation, mockRuleDetails } from '../../../helpers/testMocks'; import { + CleanCodeAttributeCategory, + SoftwareImpactSeverity, + SoftwareQuality, +} from '../../../types/clean-code-taxonomy'; +import { ADVANCED_RULE, RULE_1, RULE_10, @@ -113,6 +118,9 @@ export function mockRuleDetailsList() { key: RULE_3, repo: 'repo2', name: 'Unknown rule', + impacts: [ + { softwareQuality: SoftwareQuality.Maintainability, severity: SoftwareImpactSeverity.Low }, + ], lang: 'js', langName: 'JavaScript', }), @@ -128,6 +136,7 @@ export function mockRuleDetailsList() { type: 'VULNERABILITY', lang: 'py', langName: 'Python', + cleanCodeAttributeCategory: CleanCodeAttributeCategory.Consistent, name: 'Awsome Python rule', descriptionSections: [ { key: RuleDescriptionSections.INTRODUCTION, content: introTitle }, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts index 4134304b6c6..a81bc28eee4 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -20,7 +20,7 @@ import { act, fireEvent, screen } from '@testing-library/react'; import selectEvent from 'react-select-event'; import CodingRulesServiceMock, { RULE_TAGS_MOCK } from '../../../api/mocks/CodingRulesServiceMock'; -import { RULE_TYPES } from '../../../helpers/constants'; +import { CLEAN_CODE_CATEGORIES, SOFTWARE_QUALITIES } from '../../../helpers/constants'; import { parseDate } from '../../../helpers/dates'; import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; import { dateInputEvent, renderAppRoutes } from '../../../helpers/testReactTestingUtils'; @@ -57,8 +57,12 @@ describe('Rules app list', () => { 1 ); - // Renders type facets - RULE_TYPES.map((type) => `issue.type.${type}`).forEach((name) => + // Renders clean code categories and software qualities facets + CLEAN_CODE_CATEGORIES.map( + (category) => `issue.clean_code_attribute_category.${category}` + ).forEach((name) => expect(ui.facetItem(name).get()).toBeInTheDocument()); + + SOFTWARE_QUALITIES.map((quality) => `issue.software_quality.${quality}`).forEach((name) => expect(ui.facetItem(name).get()).toBeInTheDocument() ); @@ -77,6 +81,7 @@ describe('Rules app list', () => { ui.availableSinceFacet, ui.templateFacet, ui.qpFacet, + ui.typeFacet, ].forEach((facet) => { expect(facet.get()).toHaveAttribute('aria-expanded', 'false'); }); @@ -159,6 +164,31 @@ describe('Rules app list', () => { await user.click(ui.facetItem('cute').get()); }); expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1); + + // Clear all filters + await act(async () => { + await user.click(ui.clearAllFiltersButton.get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11); + + // Filter by clean code category + await act(async () => { + await user.click(ui.facetItem('issue.clean_code_attribute_category.ADAPTABLE').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + + // Filter by software quality + await act(async () => { + await user.click(ui.facetItem('issue.software_quality.MAINTAINABILITY').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + + // Filter by severity + await act(async () => { + await user.click(ui.severetiesFacet.get()); + await user.click(ui.facetItem('severity.HIGH').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(9); }); it('filter by standards', async () => { @@ -368,7 +398,8 @@ describe('Rule app details', () => { ui.ruleCleanCodeAttributeCategory(CleanCodeAttributeCategory.Adaptable).get() ).toBeInTheDocument(); expect(ui.ruleCleanCodeAttribute(CleanCodeAttribute.Clear).get()).toBeInTheDocument(); - expect(ui.ruleSoftwareQuality(SoftwareQuality.Maintainability).get()).toBeInTheDocument(); + // 1 In Rule details + 1 in facet + expect(ui.ruleSoftwareQuality(SoftwareQuality.Maintainability).getAll()).toHaveLength(2); expect(document.title).toEqual('page_title.template.with_category.coding_rules.page'); expect(screen.getByText('Why')).toBeInTheDocument(); expect(screen.getByText('Because')).toBeInTheDocument(); @@ -683,11 +714,17 @@ describe('redirects', () => { }); it('should handle hash parameters', async () => { - renderCodingRulesApp(mockLoggedInUser(), 'coding_rules#languages=c,js|types=BUG'); - // 2 languages + const { ui } = getPageObjects(); + + renderCodingRulesApp( + mockLoggedInUser(), + 'coding_rules#languages=c,js|types=BUG|cleanCodeAttributeCategories=ADAPTABLE' + ); expect(await screen.findByText('x_selected.2')).toBeInTheDocument(); - expect(screen.getAllByTitle('issue.type.BUG')).toHaveLength(2); - // Only 3 rules shown + expect(screen.getByTitle('issue.type.BUG')).toBeInTheDocument(); + expect(ui.facetItem('issue.clean_code_attribute_category.ADAPTABLE').get()).toBeChecked(); + + // Only 2 rules shown expect(screen.getByText('x_of_y_shown.2.2')).toBeInTheDocument(); }); }); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/AttributeCategoryFacet.tsx index 7754b3efbe3..b0396e8053c 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/AttributeCategoryFacet.tsx @@ -18,32 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import SeverityHelper from '../../../components/shared/SeverityHelper'; -import { SEVERITIES } from '../../../helpers/constants'; +import { CLEAN_CODE_CATEGORIES } from '../../../helpers/constants'; import { translate } from '../../../helpers/l10n'; import Facet, { BasicProps } from './Facet'; -interface Props extends BasicProps { - disabled: boolean; -} - -export default class ActivationSeverityFacet extends React.PureComponent<Props> { - renderName = (severity: string) => <SeverityHelper severity={severity} />; - - renderTextName = (severity: string) => translate('severity', severity); +export default function AttributeCategoryFacet(props: BasicProps) { + const renderName = React.useCallback( + (attribute: string) => translate('issue.clean_code_attribute_category', attribute), + [] + ); - render() { - return ( - <Facet - {...this.props} - disabled={this.props.disabled} - disabledHelper={translate('coding_rules.filters.active_severity.inactive')} - halfWidth - options={SEVERITIES} - property="activationSeverities" - renderName={this.renderName} - renderTextName={this.renderTextName} - /> - ); - } + return ( + <Facet + {...props} + options={CLEAN_CODE_CATEGORIES} + property="cleanCodeAttributeCategories" + renderName={renderName} + renderTextName={renderName} + /> + ); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx index 3cef1985e96..3998d0fc355 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx @@ -58,10 +58,8 @@ import { OpenFacets, Query, areQueriesEqual, - getAppFacet, getOpen, getSelected, - getServerFacet, hasRuleKey, parseQuery, serializeQuery, @@ -114,7 +112,8 @@ export class CodingRulesApp extends React.PureComponent<Props, State> { ), sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query), standards: shouldOpenStandardsFacet({}, query), - types: true, + cleanCodeAttributeCategories: true, + impactSoftwareQualities: true, }, referencedProfiles: {}, referencedRepositories: {}, @@ -199,8 +198,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> { const { openFacets } = this.state; return Object.keys(openFacets) .filter((facet: FacetKey) => openFacets[facet]) - .filter((facet: FacetKey) => shouldRequestFacet(facet)) - .map((facet: FacetKey) => getServerFacet(facet)); + .filter((facet: FacetKey) => shouldRequestFacet(facet)); }; getFieldsToFetch = () => { @@ -214,6 +212,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> { 'sysTags', 'tags', 'templateKey', + 'cleanCodeAttribute', ]; if (parseQuery(this.props.location.query).profile) { fields.push('actives', 'params'); @@ -226,7 +225,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> { facets: this.getFacetsToFetch().join(), ps: PAGE_SIZE, s: 'name', - ...this.props.location.query, + ...serializeQuery(parseQuery(this.props.location.query)), }); stopLoading = () => { @@ -299,7 +298,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> { }; fetchFacet = (facet: FacetKey) => { - this.makeFetchRequest({ ps: 1, facets: getServerFacet(facet) }).then(({ facets }) => { + this.makeFetchRequest({ ps: 1, facets: facet }).then(({ facets }) => { if (this.mounted) { this.setState((state) => ({ facets: { ...state.facets, ...facets }, loading: false })); } @@ -711,7 +710,7 @@ function parseFacets(rawFacets: { property: string; values: { count: number; val for (const rawValue of rawFacet.values) { values[rawValue.val] = rawValue.count; } - facets[getAppFacet(rawFacet.property)] = values; + facets[rawFacet.property as FacetKey] = values; } return facets; } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx index 2d43646bba3..4b8cfe0ca18 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx @@ -21,13 +21,15 @@ import * as React from 'react'; import { Profile } from '../../../api/quality-profiles'; import { Dict } from '../../../types/types'; import { Facets, OpenFacets, Query } from '../query'; -import ActivationSeverityFacet from './ActivationSeverityFacet'; +import AttributeCategoryFacet from './AttributeCategoryFacet'; import AvailableSinceFacet from './AvailableSinceFacet'; -import DefaultSeverityFacet from './DefaultSeverityFacet'; import InheritanceFacet from './InheritanceFacet'; import LanguageFacet from './LanguageFacet'; import ProfileFacet from './ProfileFacet'; +import SoftwareQualityFacet from './SoftwareQualityFacet'; + import RepositoryFacet from './RepositoryFacet'; +import SeverityFacet from './SeverityFacet'; import { StandardFacet } from './StandardFacet'; import StatusFacet from './StatusFacet'; import TagFacet from './TagFacet'; @@ -54,10 +56,6 @@ export default function FacetsList(props: FacetsListProps) { props.selectedProfile === undefined || !props.selectedProfile.isInherited; - const activationSeverityDisabled = - props.query.compareToProfile !== undefined || - props.selectedProfile === undefined || - !props.query.activation; return ( <> <LanguageFacet @@ -68,18 +66,43 @@ export default function FacetsList(props: FacetsListProps) { stats={props.facets && props.facets.languages} values={props.query.languages} /> + + <AttributeCategoryFacet + onChange={props.onFilterChange} + onToggle={props.onFacetToggle} + open={!!props.openFacets.cleanCodeAttributeCategories} + stats={props.facets?.cleanCodeAttributeCategories} + values={props.query.cleanCodeAttributeCategories} + /> + + <SoftwareQualityFacet + onChange={props.onFilterChange} + onToggle={props.onFacetToggle} + open={!!props.openFacets.impactSoftwareQualities} + stats={props.facets?.impactSoftwareQualities} + values={props.query.impactSoftwareQualities} + /> + + <SeverityFacet + onChange={props.onFilterChange} + onToggle={props.onFacetToggle} + open={!!props.openFacets.impactSeverities} + stats={props.facets?.impactSeverities} + values={props.query.impactSeverities} + /> + <TypeFacet onChange={props.onFilterChange} onToggle={props.onFacetToggle} open={!!props.openFacets.types} - stats={props.facets && props.facets.types} + stats={props.facets?.types} values={props.query.types} /> <TagFacet onChange={props.onFilterChange} onToggle={props.onFacetToggle} open={!!props.openFacets.tags} - stats={props.facets && props.facets.tags} + stats={props.facets?.tags} values={props.query.tags} /> <RepositoryFacet @@ -87,27 +110,21 @@ export default function FacetsList(props: FacetsListProps) { onToggle={props.onFacetToggle} open={!!props.openFacets.repositories} referencedRepositories={props.referencedRepositories} - stats={props.facets && props.facets.repositories} + stats={props.facets?.repositories} values={props.query.repositories} /> - <DefaultSeverityFacet - onChange={props.onFilterChange} - onToggle={props.onFacetToggle} - open={!!props.openFacets.severities} - stats={props.facets && props.facets.severities} - values={props.query.severities} - /> + <StatusFacet onChange={props.onFilterChange} onToggle={props.onFacetToggle} open={!!props.openFacets.statuses} - stats={props.facets && props.facets.statuses} + stats={props.facets?.statuses} values={props.query.statuses} /> <StandardFacet cwe={props.query.cwe} cweOpen={!!props.openFacets.cwe} - cweStats={props.facets && props.facets.cwe} + cweStats={props.facets?.cwe} fetchingCwe={false} fetchingOwaspTop10={false} fetchingOwaspTop10-2021={false} @@ -117,14 +134,14 @@ export default function FacetsList(props: FacetsListProps) { open={!!props.openFacets.standards} owaspTop10={props.query.owaspTop10} owaspTop10Open={!!props.openFacets.owaspTop10} - owaspTop10Stats={props.facets && props.facets.owaspTop10} + owaspTop10Stats={props.facets?.owaspTop10} owaspTop10-2021={props.query['owaspTop10-2021']} owaspTop10-2021Open={!!props.openFacets['owaspTop10-2021']} - owaspTop10-2021Stats={props.facets && props.facets['owaspTop10-2021']} + owaspTop10-2021Stats={props.facets?.['owaspTop10-2021']} query={props.query} sonarsourceSecurity={props.query.sonarsourceSecurity} sonarsourceSecurityOpen={!!props.openFacets.sonarsourceSecurity} - sonarsourceSecurityStats={props.facets && props.facets.sonarsourceSecurity} + sonarsourceSecurityStats={props.facets?.sonarsourceSecurity} /> <AvailableSinceFacet onChange={props.onFilterChange} @@ -157,14 +174,6 @@ export default function FacetsList(props: FacetsListProps) { open={!!props.openFacets.inheritance} value={props.query.inheritance} /> - <ActivationSeverityFacet - disabled={activationSeverityDisabled} - onChange={props.onFilterChange} - onToggle={props.onFacetToggle} - open={!!props.openFacets.activationSeverities} - stats={props.facets && props.facets.activationSeverities} - values={props.query.activationSeverities} - /> </> )} </> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx index 1e6fc871fbf..81e9b7b44ba 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx @@ -74,6 +74,7 @@ class LanguageFacet extends React.PureComponent<Props> { getFacetItemText={this.getLanguageName} getSearchResultKey={(language) => language.key} getSearchResultText={(language) => language.name} + maxInitialItems={10} minSearchLength={1} onChange={this.props.onChange} onSearch={this.handleSearch} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx index 8c9c3247362..f791e997634 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx @@ -56,7 +56,6 @@ export default class ProfileFacet extends React.PureComponent<Props> { handleClear = () => this.props.onChange({ activation: undefined, - activationSeverities: [], compareToProfile: undefined, inheritance: undefined, profile: undefined, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/SeverityFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/SeverityFacet.tsx new file mode 100644 index 00000000000..cdb4f11e5b9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/SeverityFacet.tsx @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; +import SoftwareImpactSeverityIcon from '../../../components/icons/SoftwareImpactSeverityIcon'; +import { IMPACT_SEVERITIES } from '../../../helpers/constants'; +import { translate } from '../../../helpers/l10n'; +import Facet, { BasicProps } from './Facet'; + +export default function SeverityFacet(props: BasicProps) { + const renderName = React.useCallback( + (severity: string) => ( + <div className="sw-flex"> + <SoftwareImpactSeverityIcon severity={severity} /> + <span className="sw-ml-1">{translate('severity', severity)}</span> + </div> + ), + [] + ); + + const renderTextName = React.useCallback( + (severity: string) => translate('severity', severity), + [] + ); + + return ( + <Facet + {...props} + options={IMPACT_SEVERITIES} + property="impactSeverities" + renderName={renderName} + renderTextName={renderTextName} + > + <DocumentationTooltip + className="spacer-left" + placement="right" + content={ + <> + <p>{translate('issues.facet.impactSeverities.help.line1')}</p> + <p className="sw-mt-2">{translate('issues.facet.impactSeverities.help.line2')}</p> + </> + } + links={[ + { + href: '/user-guide/clean-code', + label: translate('learn_more'), + }, + ]} + /> + </Facet> + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/DefaultSeverityFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/SoftwareQualityFacet.tsx index da51af94cd6..59d0da4736b 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/DefaultSeverityFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/SoftwareQualityFacet.tsx @@ -17,27 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import * as React from 'react'; -import SeverityHelper from '../../../components/shared/SeverityHelper'; -import { SEVERITIES } from '../../../helpers/constants'; +import { SOFTWARE_QUALITIES } from '../../../helpers/constants'; import { translate } from '../../../helpers/l10n'; import Facet, { BasicProps } from './Facet'; -export default class DefaultSeverityFacet extends React.PureComponent<BasicProps> { - renderName = (severity: string) => <SeverityHelper severity={severity} />; - - renderTextName = (severity: string) => translate('severity', severity); +export default function SoftwareQualityFacet(props: BasicProps) { + const renderName = React.useCallback( + (quality: string) => translate('issue.software_quality', quality), + [] + ); - render() { - return ( - <Facet - {...this.props} - halfWidth - options={SEVERITIES} - property="severities" - renderName={this.renderName} - renderTextName={this.renderTextName} - /> - ); - } + return ( + <Facet + {...props} + options={SOFTWARE_QUALITIES} + property="impactSoftwareQualities" + renderName={renderName} + renderTextName={renderName} + /> + ); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/query.ts b/server/sonar-web/src/main/js/apps/coding-rules/query.ts index 540ebece009..eb31bc35298 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/query.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/query.ts @@ -24,20 +24,28 @@ import { parseAsOptionalBoolean, parseAsOptionalString, parseAsString, + parseImpactSeverityQuery, queriesEqual, serializeDateShort, serializeOptionalBoolean, serializeString, serializeStringArray, } from '../../helpers/query'; +import { + CleanCodeAttributeCategory, + SoftwareImpactSeverity, + SoftwareQuality, +} from '../../types/clean-code-taxonomy'; import { Dict, RawQuery, RuleInheritance } from '../../types/types'; export interface Query { activation: boolean | undefined; - activationSeverities: string[]; availableSince: Date | undefined; + cleanCodeAttributeCategories: CleanCodeAttributeCategory[]; compareToProfile: string | undefined; cwe: string[]; + impactSeverities: SoftwareImpactSeverity[]; + impactSoftwareQualities: SoftwareQuality[]; inheritance: RuleInheritance | undefined; languages: string[]; owaspTop10: string[]; @@ -78,10 +86,18 @@ export interface Actives { export function parseQuery(query: RawQuery): Query { return { activation: parseAsOptionalBoolean(query.activation), - activationSeverities: parseAsArray(query.active_severities, parseAsString), availableSince: parseAsDate(query.available_since), + cleanCodeAttributeCategories: parseAsArray<CleanCodeAttributeCategory>( + query.cleanCodeAttributeCategories, + parseAsString + ), compareToProfile: parseAsOptionalString(query.compareToProfile), cwe: parseAsArray(query.cwe, parseAsString), + impactSeverities: parseImpactSeverityQuery(query.impactSeverities, query.severities), + impactSoftwareQualities: parseAsArray<SoftwareQuality>( + query.impactSoftwareQualities, + parseAsString + ), inheritance: parseAsInheritance(query.inheritance), languages: parseAsArray(query.languages, parseAsString), owaspTop10: parseAsArray(query.owaspTop10, parseAsString), @@ -102,11 +118,13 @@ export function parseQuery(query: RawQuery): Query { export function serializeQuery(query: Query): RawQuery { return cleanQuery({ activation: serializeOptionalBoolean(query.activation), - active_severities: serializeStringArray(query.activationSeverities), available_since: serializeDateShort(query.availableSince), + cleanCodeAttributeCategories: serializeStringArray(query.cleanCodeAttributeCategories), compareToProfile: serializeString(query.compareToProfile), cwe: serializeStringArray(query.cwe), inheritance: serializeInheritance(query.inheritance), + impactSeverities: serializeStringArray(query.impactSeverities), + impactSoftwareQualities: serializeStringArray(query.impactSoftwareQualities), is_template: serializeOptionalBoolean(query.template), languages: serializeStringArray(query.languages), owaspTop10: serializeStringArray(query.owaspTop10), @@ -115,7 +133,7 @@ export function serializeQuery(query: Query): RawQuery { qprofile: serializeString(query.profile), repositories: serializeStringArray(query.repositories), rule_key: serializeString(query.ruleKey), - severities: serializeStringArray(query.severities), + severities: undefined, sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity), statuses: serializeStringArray(query.statuses), tags: serializeStringArray(query.tags), @@ -141,18 +159,13 @@ export function shouldRequestFacet(facet: string): facet is FacetKey { 'statuses', 'tags', 'types', + 'cleanCodeAttributeCategories', + 'impactSoftwareQualities', + 'impactSeverities', ]; return facetsToRequest.includes(facet); } -export function getServerFacet(facet: FacetKey) { - return facet === 'activationSeverities' ? 'active_severities' : facet; -} - -export function getAppFacet(serverFacet: string): FacetKey { - return serverFacet === 'active_severities' ? 'activationSeverities' : (serverFacet as FacetKey); -} - export function getOpen(query: RawQuery) { return query.open; } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx index 8847ba6f056..dd518f5c590 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx @@ -41,11 +41,15 @@ const selectors = { clearAllFiltersButton: byRole('button', { name: 'clear_all_filters' }), // Facets + cleanCodeCategoriesFacet: byRole('button', { + name: 'coding_rules.facet.cleanCodeAttributeCategories', + }), languagesFacet: byRole('button', { name: 'coding_rules.facet.languages' }), typeFacet: byRole('button', { name: 'coding_rules.facet.types' }), tagsFacet: byRole('button', { name: 'coding_rules.facet.tags' }), repositoriesFacet: byRole('button', { name: 'coding_rules.facet.repositories' }), - severetiesFacet: byRole('button', { name: 'coding_rules.facet.severities' }), + softwareQualitiesFacet: byRole('button', { name: 'coding_rules.facet.impactSoftwareQualities' }), + severetiesFacet: byRole('button', { name: 'coding_rules.facet.impactSeverities' }), statusesFacet: byRole('button', { name: 'coding_rules.facet.statuses' }), standardsFacet: byRole('button', { name: 'issues.facet.standards' }), standardsOwasp2017Top10Facet: byRole('button', { name: 'issues.facet.owaspTop10' }), 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 index 28779bd54fb..981f502c3d1 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AttributeCategoryFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AttributeCategoryFacet.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; +import { CLEAN_CODE_CATEGORIES } from '../../../helpers/constants'; import { CleanCodeAttributeCategory } from '../../../types/clean-code-taxonomy'; import { CommonProps, SimpleListStyleFacet } from './SimpleListStyleFacet'; @@ -26,8 +27,6 @@ interface Props extends CommonProps { categories: Array<CleanCodeAttributeCategory>; } -const CATEGORIES = Object.values(CleanCodeAttributeCategory); - export function AttributeCategoryFacet(props: Props) { const { categories = [], ...rest } = props; @@ -35,7 +34,7 @@ export function AttributeCategoryFacet(props: Props) { <SimpleListStyleFacet property="cleanCodeAttributeCategories" itemNamePrefix="issue.clean_code_attribute_category" - listItems={CATEGORIES} + listItems={CLEAN_CODE_CATEGORIES} selectedItems={categories} {...rest} /> 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 4b9fba32883..6247269a42a 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 @@ -21,6 +21,7 @@ import * as React from 'react'; import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; import SoftwareImpactSeverityIcon from '../../../components/icons/SoftwareImpactSeverityIcon'; +import { IMPACT_SEVERITIES } from '../../../helpers/constants'; import { translate } from '../../../helpers/l10n'; import { SoftwareImpactSeverity } from '../../../types/clean-code-taxonomy'; import { CommonProps, SimpleListStyleFacet } from './SimpleListStyleFacet'; @@ -29,8 +30,6 @@ interface Props extends CommonProps { severities: SoftwareImpactSeverity[]; } -const SEVERITIES = Object.values(SoftwareImpactSeverity); - export function SeverityFacet(props: Props) { const { severities = [], ...rest } = props; @@ -38,7 +37,7 @@ export function SeverityFacet(props: Props) { <SimpleListStyleFacet property="impactSeverities" itemNamePrefix="severity" - listItems={SEVERITIES} + listItems={IMPACT_SEVERITIES} selectedItems={severities} renderIcon={(severity: string) => <SoftwareImpactSeverityIcon severity={severity} />} help={ 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 index 2888116505e..0c94d42c751 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/SoftwareQualityFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SoftwareQualityFacet.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; +import { SOFTWARE_QUALITIES } from '../../../helpers/constants'; import { SoftwareQuality } from '../../../types/clean-code-taxonomy'; import { CommonProps, SimpleListStyleFacet } from './SimpleListStyleFacet'; @@ -26,8 +27,6 @@ interface Props extends CommonProps { qualities: Array<SoftwareQuality>; } -const QUALITIES = Object.values(SoftwareQuality); - export function SoftwareQualityFacet(props: Props) { const { qualities = [], ...rest } = props; @@ -35,7 +34,7 @@ export function SoftwareQualityFacet(props: Props) { <SimpleListStyleFacet property="impactSoftwareQualities" itemNamePrefix="issue.software_quality" - listItems={QUALITIES} + listItems={SOFTWARE_QUALITIES} selectedItems={qualities} {...rest} /> 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 31220895075..f3f9c40661b 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/utils.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { compact, isArray, uniq } from 'lodash'; +import { isArray } from 'lodash'; import { getUsers } from '../../api/users'; import { formatMeasure } from '../../helpers/measures'; import { @@ -26,6 +26,7 @@ import { parseAsBoolean, parseAsDate, parseAsString, + parseImpactSeverityQuery, queriesEqual, serializeDateShort, serializeString, @@ -134,29 +135,6 @@ export function parseQuery(query: RawQuery): Query { }; } -function parseImpactSeverityQuery( - newSeverities: string, - oldSeverities?: string -): SoftwareImpactSeverity[] { - const OLD_TO_NEW_MAPPER = { - BLOCKER: SoftwareImpactSeverity.High, - CRITICAL: SoftwareImpactSeverity.High, - MAJOR: SoftwareImpactSeverity.Medium, - MINOR: SoftwareImpactSeverity.Low, - INFO: SoftwareImpactSeverity.Low, - }; - - // Merging new and old severities includes mapping for old to new - return compact( - uniq([ - ...parseAsArray<SoftwareImpactSeverity>(newSeverities, parseAsString), - ...parseAsArray(oldSeverities, parseAsString).map( - (oldSeverity: string) => OLD_TO_NEW_MAPPER[oldSeverity as keyof typeof OLD_TO_NEW_MAPPER] - ), - ]) - ); -} - export function getOpen(query: RawQuery): string | undefined { return query.open; } diff --git a/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx b/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx index e741e3084ee..35f16da7730 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx +++ b/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx @@ -91,7 +91,7 @@ export default class FacetHeader extends React.PureComponent<Props> { {this.props.onClick ? ( <span className="search-navigator-facet-header display-flex-center"> <button - className="button-link" + className="button-link display-flex-center" type="button" onClick={this.handleClick} aria-expanded={open} diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts index 828f442b408..17615740c75 100644 --- a/server/sonar-web/src/main/js/helpers/constants.ts +++ b/server/sonar-web/src/main/js/helpers/constants.ts @@ -19,12 +19,23 @@ */ import { colors } from '../app/theme'; import { AlmKeys } from '../types/alm-settings'; +import { + CleanCodeAttributeCategory, + SoftwareImpactSeverity, + SoftwareQuality, +} from '../types/clean-code-taxonomy'; import { ComponentQualifier } from '../types/component'; import { IssueResolution, IssueScope, IssueSeverity, IssueType } from '../types/issues'; import { RuleType } from '../types/types'; export const SEVERITIES = Object.values(IssueSeverity); +export const IMPACT_SEVERITIES = Object.values(SoftwareImpactSeverity); + +export const CLEAN_CODE_CATEGORIES = Object.values(CleanCodeAttributeCategory); + +export const SOFTWARE_QUALITIES = Object.values(SoftwareQuality); + export const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED']; export const ISSUE_TYPES: IssueType[] = [ diff --git a/server/sonar-web/src/main/js/helpers/query.ts b/server/sonar-web/src/main/js/helpers/query.ts index 37ced126f60..97238fc4ab8 100644 --- a/server/sonar-web/src/main/js/helpers/query.ts +++ b/server/sonar-web/src/main/js/helpers/query.ts @@ -17,7 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { isEqual, isNil, omitBy } from 'lodash'; +import { compact, isEqual, isNil, omitBy, uniq } from 'lodash'; +import { SoftwareImpactSeverity } from '../types/clean-code-taxonomy'; import { RawQuery } from '../types/types'; import { isValidDate, parseDate, toISO8601WithOffsetString, toShortISO8601String } from './dates'; @@ -125,3 +126,26 @@ export function serializeOptionalBoolean(value: boolean | undefined): string | u } return undefined; } + +export function parseImpactSeverityQuery( + newSeverities: string, + oldSeverities?: string +): SoftwareImpactSeverity[] { + const OLD_TO_NEW_MAPPER = { + BLOCKER: SoftwareImpactSeverity.High, + CRITICAL: SoftwareImpactSeverity.High, + MAJOR: SoftwareImpactSeverity.Medium, + MINOR: SoftwareImpactSeverity.Low, + INFO: SoftwareImpactSeverity.Low, + }; + + // Merging new and old severities includes mapping for old to new + return compact( + uniq([ + ...parseAsArray<SoftwareImpactSeverity>(newSeverities, parseAsString), + ...parseAsArray(oldSeverities, parseAsString).map( + (oldSeverity: string) => OLD_TO_NEW_MAPPER[oldSeverity as keyof typeof OLD_TO_NEW_MAPPER] + ), + ]) + ); +} diff --git a/server/sonar-web/src/main/js/types/rules.ts b/server/sonar-web/src/main/js/types/rules.ts index 4e85cb1e1a7..11128fdcf36 100644 --- a/server/sonar-web/src/main/js/types/rules.ts +++ b/server/sonar-web/src/main/js/types/rules.ts @@ -29,6 +29,7 @@ export interface SearchRulesQuery { active_severities?: string; asc?: boolean | string; available_since?: string; + cleanCodeAttributeCategories?: string; cwe?: string; f?: string; facets?: string; @@ -45,7 +46,8 @@ export interface SearchRulesQuery { repositories?: string; rule_key?: string; s?: string; - severities?: string; + impactSoftwareQualities?: string; + impactSeverities?: string; sonarsourceSecurity?: string; statuses?: string; tags?: string; diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index bab27408f09..aa26d7d3192 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -533,9 +533,9 @@ export interface QualityGate { export type RawQuery = Dict<any>; export interface Rule { - cleanCodeAttributeCategory: CleanCodeAttributeCategory; - cleanCodeAttribute: CleanCodeAttribute; - impacts: Array<{ + cleanCodeAttributeCategory?: CleanCodeAttributeCategory; + cleanCodeAttribute?: CleanCodeAttribute; + impacts?: Array<{ softwareQuality: SoftwareQuality; severity: SoftwareImpactSeverity; }>; |