From 49608896520f49d93eb9af62b7c756861dffec04 Mon Sep 17 00:00:00 2001 From: 7PH Date: Thu, 28 Sep 2023 02:20:31 +0200 Subject: [PATCH] SONAR-20545 Show new rule breakdown using Clean Code Taxonomy in quality profile --- .../quality-profiles/details/ProfileRules.tsx | 297 +++++++++--------- .../details/ProfileRulesRow.tsx | 61 ++-- .../ProfileRulesSonarWayComparison.tsx | 14 +- .../resources/org/sonar/l10n/core.properties | 6 + 4 files changed, 207 insertions(+), 171 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx index 7c147187de0..5ec2b36d62b 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx @@ -17,22 +17,26 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import styled from '@emotion/styled'; import { ButtonPrimary, ContentCell, NumericalCell, + Spinner, SubTitle, Table, TableRow, -} from 'design-system/lib'; +} from 'design-system'; import { keyBy } from 'lodash'; import * as React from 'react'; +import { useEffect, useState } from 'react'; import { getQualityProfile } from '../../../api/quality-profiles'; import { searchRules } from '../../../api/rules'; import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; import { translate } from '../../../helpers/l10n'; import { isDefined } from '../../../helpers/types'; import { getRulesUrl } from '../../../helpers/urls'; +import { CleanCodeAttributeCategory, SoftwareQuality } from '../../../types/clean-code-taxonomy'; import { SearchRulesResponse } from '../../../types/coding-rules'; import { Dict } from '../../../types/types'; import { Profile } from '../types'; @@ -40,8 +44,6 @@ import ProfileRulesDeprecatedWarning from './ProfileRulesDeprecatedWarning'; import ProfileRulesRow from './ProfileRulesRow'; import ProfileRulesSonarWayComparison from './ProfileRulesSonarWayComparison'; -const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT']; - interface Props { profile: Profile; } @@ -51,172 +53,181 @@ interface ByType { count: number | null; } -interface State { - activatedTotal: number | null; - activatedByType: Dict; - allByType: Dict; - compareToSonarWay: { profile: string; profileName: string; missingRuleCount: number } | null; - total: number | null; -} - -export default class ProfileRules extends React.PureComponent, State> { - mounted = false; - - state: State = { - activatedTotal: null, - activatedByType: keyBy( - TYPES.map((t) => ({ val: t, count: null })), - 'val', - ), - allByType: keyBy( - TYPES.map((t) => ({ val: t, count: null })), - 'val', - ), - compareToSonarWay: null, - total: null, - }; - - componentDidMount() { - this.mounted = true; - this.loadRules(); - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.profile.key !== this.props.profile.key) { - this.loadRules(); +export default function ProfileRules({ profile }: Readonly) { + const activateMoreUrl = getRulesUrl({ qprofile: profile.key, activation: 'false' }); + const { actions = {} } = profile; + + const [loading, setLoading] = useState(false); + const [countsByCctCategory, setCountsByCctCategory] = useState>({}); + const [totalByCctCategory, setTotalByCctCategory] = useState>({}); + const [countsBySoftwareImpact, setCountsBySoftwareImpact] = useState>({}); + const [totalBySoftwareQuality, setTotalBySoftwareQuality] = useState>({}); + const [sonarWayDiff, setSonarWayDiff] = useState<{ + profile: string; + profileName: string; + missingRuleCount: number; + } | null>(null); + + const loadRules = React.useCallback(async () => { + function findFacet(response: SearchRulesResponse, property: string) { + const facet = response.facets?.find((f) => f.property === property); + return facet ? facet.values : []; } - } - - componentWillUnmount() { - this.mounted = false; - } - loadProfile() { - if (this.props.profile.isBuiltIn) { - return Promise.resolve(null); + try { + setLoading(true); + return await Promise.all([ + searchRules({ + languages: profile.language, + facets: 'cleanCodeAttributeCategories,impactSoftwareQualities', + }), + searchRules({ + activation: 'true', + facets: 'cleanCodeAttributeCategories,impactSoftwareQualities', + qprofile: profile.key, + }), + !profile.isBuiltIn && + getQualityProfile({ + compareToSonarWay: true, + profile, + }), + ]).then((responses) => { + const [allRules, activatedRules, showProfile] = responses; + setTotalByCctCategory( + keyBy(findFacet(allRules, 'cleanCodeAttributeCategories'), 'val'), + ); + setCountsByCctCategory( + keyBy(findFacet(activatedRules, 'cleanCodeAttributeCategories'), 'val'), + ); + setTotalBySoftwareQuality( + keyBy(findFacet(allRules, 'impactSoftwareQualities'), 'val'), + ); + setCountsBySoftwareImpact( + keyBy(findFacet(activatedRules, 'impactSoftwareQualities'), 'val'), + ); + setSonarWayDiff(showProfile?.compareToSonarWay); + }); + } finally { + setLoading(false); } - return getQualityProfile({ - compareToSonarWay: true, - profile: this.props.profile, - }); - } + }, [profile]); - loadAllRules() { - return searchRules({ - languages: this.props.profile.language, - facets: 'types', - ps: 1, - }); - } - - loadActivatedRules() { - return searchRules({ - activation: 'true', - facets: 'types', - ps: 1, - qprofile: this.props.profile.key, - }); - } - - loadRules() { - return Promise.all([this.loadAllRules(), this.loadActivatedRules(), this.loadProfile()]).then( - (responses) => { - if (this.mounted) { - const [allRules, activatedRules, showProfile] = responses; - this.setState({ - activatedTotal: activatedRules.paging.total, - allByType: keyBy(this.takeFacet(allRules, 'types'), 'val'), - activatedByType: keyBy(this.takeFacet(activatedRules, 'types'), 'val'), - compareToSonarWay: showProfile?.compareToSonarWay, - total: allRules.paging.total, - }); - } - }, - ); - } + useEffect(() => { + loadRules(); + }, [profile.key, loadRules]); - takeFacet(response: SearchRulesResponse, property: string) { - const facet = response.facets?.find((f) => f.property === property); - return facet ? facet.values : []; + if (loading) { + return ; } - render() { - const { profile } = this.props; - const { compareToSonarWay } = this.state; - const activateMoreUrl = getRulesUrl({ qprofile: profile.key, activation: 'false' }); - const { actions = {} } = profile; + return ( +
+ {translate('quality_profile.rules.breakdown')} - return ( -
+ - - {translate('rules')} + + + {translate('quality_profile.rules.cct_categories_title')} {translate('active')} - {translate('inactive')} - + {translate('inactive')} + } noHeaderTopBorder noSidePadding > - - {TYPES.map((type) => ( + {Object.values(CleanCodeAttributeCategory).map((category) => ( ))}
+
-
- {profile.activeDeprecatedRuleCount > 0 && ( - - )} - - {isDefined(compareToSonarWay) && compareToSonarWay.missingRuleCount > 0 && ( - + + + {translate('quality_profile.rules.software_qualities_title')} + + {translate('active')} + {translate('inactive')} + + } + noHeaderTopBorder + noSidePadding + > + {Object.values(SoftwareQuality).map((quality) => ( + - )} + ))} +
+ - {actions.edit && !profile.isBuiltIn && ( - +
+ {profile.activeDeprecatedRuleCount > 0 && ( + + )} + + {isDefined(sonarWayDiff) && sonarWayDiff.missingRuleCount > 0 && ( + + )} + + {actions.edit && !profile.isBuiltIn && ( + + {translate('quality_profiles.activate_more')} + + )} + + {/* if a user is allowed to `copy` a profile if they are a global admin */} + {/* this user could potentially activate more rules if the profile was not built-in */} + {/* in such cases it's better to show the button but disable it with a tooltip */} + {actions.copy && profile.isBuiltIn && ( + + {translate('quality_profiles.activate_more')} - )} - - {/* if a user is allowed to `copy` a profile if they are a global admin */} - {/* this user could potentially activate more rules if the profile was not built-in */} - {/* in such cases it's better to show the button but disable it with a tooltip */} - {actions.copy && profile.isBuiltIn && ( - - - {translate('quality_profiles.activate_more')} - - - )} -
-
- ); - } + + )} + +
+ ); } + +const StyledTableContainer = styled.div` + border-radius: 4px; + border: 1px solid #dddddd; +`; + +const StyledTableRowHeader = styled(TableRow)` + border-radius: 3px; + background-color: #eff2f9; +`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx index db7fed44241..566ccaf0659 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx @@ -19,31 +19,32 @@ */ import { ContentCell, Link, Note, NumericalCell, TableRow } from 'design-system'; import * as React from 'react'; -import IssueTypeIcon from '../../../components/icons/IssueTypeIcon'; -import { translate } from '../../../helpers/l10n'; +import { translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; import { isDefined } from '../../../helpers/types'; import { getRulesUrl } from '../../../helpers/urls'; import { MetricType } from '../../../types/metrics'; interface Props { + title: string; className?: string; count: number | null; qprofile: string; total: number | null; - type?: string; + propertyName: 'cleanCodeAttributeCategories' | 'impactSoftwareQualities'; + propertyValue: string; } export default function ProfileRulesRowOfType(props: Readonly) { const activeRulesUrl = getRulesUrl({ qprofile: props.qprofile, activation: 'true', - types: props.type, + [props.propertyName]: props.propertyValue, }); const inactiveRulesUrl = getRulesUrl({ qprofile: props.qprofile, activation: 'false', - types: props.type, + [props.propertyName]: props.propertyValue, }); let inactiveCount = null; if (props.count != null && props.total != null) { @@ -52,30 +53,38 @@ export default function ProfileRulesRowOfType(props: Readonly) { return ( - - {props.type ? ( - <> - - {translate('issue.type', props.type, 'plural')} - - ) : ( - translate('total') - )} - + {props.title} - {isDefined(props.count) && ( - {formatMeasure(props.count, MetricType.ShortInteger)} + {isDefined(props.count) && props.count > 0 ? ( + + {formatMeasure(props.count, MetricType.ShortInteger)} + + ) : ( + 0 )} - - {isDefined(inactiveCount) && - (inactiveCount > 0 ? ( - - {formatMeasure(inactiveCount, MetricType.ShortInteger)} - - ) : ( - 0 - ))} + + {isDefined(inactiveCount) && inactiveCount > 0 ? ( + + {formatMeasure(inactiveCount, MetricType.ShortInteger)} + + ) : ( + 0 + )} ); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx index 279a43620a7..2d852db5c48 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx @@ -21,7 +21,7 @@ import { FlagMessage, Link } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import HelpTooltip from '../../../components/controls/HelpTooltip'; -import { translate } from '../../../helpers/l10n'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getRulesUrl } from '../../../helpers/urls'; interface Props { @@ -47,7 +47,17 @@ export default function ProfileRulesSonarWayComparison(props: Props) { id="quality_profiles.x_sonarway_missing_rules" values={{ count: props.sonarWayMissingRules, - linkCount: {props.sonarWayMissingRules}, + linkCount: ( + + {props.sonarWayMissingRules} + + ), }} />