Browse Source

SONAR-20545 Show new rule breakdown using Clean Code Taxonomy in quality profile

tags/10.3.0.82913
7PH 8 months ago
parent
commit
4960889652

+ 154
- 143
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx View File

@@ -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<ByType>;
allByType: Dict<ByType>;
compareToSonarWay: { profile: string; profileName: string; missingRuleCount: number } | null;
total: number | null;
}

export default class ProfileRules extends React.PureComponent<Readonly<Props>, 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<Props>) {
const activateMoreUrl = getRulesUrl({ qprofile: profile.key, activation: 'false' });
const { actions = {} } = profile;

const [loading, setLoading] = useState(false);
const [countsByCctCategory, setCountsByCctCategory] = useState<Dict<ByType>>({});
const [totalByCctCategory, setTotalByCctCategory] = useState<Dict<ByType>>({});
const [countsBySoftwareImpact, setCountsBySoftwareImpact] = useState<Dict<ByType>>({});
const [totalBySoftwareQuality, setTotalBySoftwareQuality] = useState<Dict<ByType>>({});
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<ByType>(findFacet(allRules, 'cleanCodeAttributeCategories'), 'val'),
);
setCountsByCctCategory(
keyBy<ByType>(findFacet(activatedRules, 'cleanCodeAttributeCategories'), 'val'),
);
setTotalBySoftwareQuality(
keyBy<ByType>(findFacet(allRules, 'impactSoftwareQualities'), 'val'),
);
setCountsBySoftwareImpact(
keyBy<ByType>(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<ByType>(this.takeFacet(allRules, 'types'), 'val'),
activatedByType: keyBy<ByType>(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 <Spinner />;
}

render() {
const { profile } = this.props;
const { compareToSonarWay } = this.state;
const activateMoreUrl = getRulesUrl({ qprofile: profile.key, activation: 'false' });
const { actions = {} } = profile;
return (
<section aria-label={translate('rules')} className="it__quality-profiles__rules">
<SubTitle>{translate('quality_profile.rules.breakdown')}</SubTitle>

return (
<section aria-label={translate('rules')} className="it__quality-profiles__rules">
<StyledTableContainer>
<Table
columnCount={3}
columnWidths={['50%', '25%', '25%']}
header={
<TableRow>
<ContentCell>
<SubTitle className="sw-mb-0">{translate('rules')}</SubTitle>
<StyledTableRowHeader>
<ContentCell className="sw-font-semibold sw-pl-4">
{translate('quality_profile.rules.cct_categories_title')}
</ContentCell>
<NumericalCell>{translate('active')}</NumericalCell>
<NumericalCell>{translate('inactive')}</NumericalCell>
</TableRow>
<NumericalCell className="sw-pr-4">{translate('inactive')}</NumericalCell>
</StyledTableRowHeader>
}
noHeaderTopBorder
noSidePadding
>
<ProfileRulesRow
className="it__quality-profiles__rules__total"
count={this.state.activatedTotal}
qprofile={profile.key}
total={this.state.total}
/>
{TYPES.map((type) => (
{Object.values(CleanCodeAttributeCategory).map((category) => (
<ProfileRulesRow
count={this.state.activatedByType[type]?.count}
key={type}
title={translate('issue.clean_code_attribute_category', category)}
total={totalByCctCategory[category]?.count}
count={countsByCctCategory[category]?.count}
key={category}
qprofile={profile.key}
total={this.state.allByType[type]?.count}
type={type}
propertyName="cleanCodeAttributeCategories"
propertyValue={category}
/>
))}
</Table>
</StyledTableContainer>

<div className="sw-mt-6 sw-flex sw-flex-col sw-gap-4 sw-items-start">
{profile.activeDeprecatedRuleCount > 0 && (
<ProfileRulesDeprecatedWarning
activeDeprecatedRules={profile.activeDeprecatedRuleCount}
profile={profile.key}
/>
)}

{isDefined(compareToSonarWay) && compareToSonarWay.missingRuleCount > 0 && (
<ProfileRulesSonarWayComparison
language={profile.language}
profile={profile.key}
sonarWayMissingRules={compareToSonarWay.missingRuleCount}
sonarway={compareToSonarWay.profile}
<StyledTableContainer className="sw-mt-4">
<Table
columnCount={3}
columnWidths={['50%', '25%', '25%']}
header={
<StyledTableRowHeader>
<ContentCell className="sw-font-semibold sw-pl-4">
{translate('quality_profile.rules.software_qualities_title')}
</ContentCell>
<NumericalCell>{translate('active')}</NumericalCell>
<NumericalCell className="sw-pr-4">{translate('inactive')}</NumericalCell>
</StyledTableRowHeader>
}
noHeaderTopBorder
noSidePadding
>
{Object.values(SoftwareQuality).map((quality) => (
<ProfileRulesRow
title={translate('issue.software_quality', quality)}
total={totalBySoftwareQuality[quality]?.count}
count={countsBySoftwareImpact[quality]?.count}
key={quality}
qprofile={profile.key}
propertyName="impactSoftwareQualities"
propertyValue={quality}
/>
)}
))}
</Table>
</StyledTableContainer>

{actions.edit && !profile.isBuiltIn && (
<ButtonPrimary className="it__quality-profiles__activate-rules" to={activateMoreUrl}>
<div className="sw-mt-6 sw-flex sw-flex-col sw-gap-4 sw-items-start">
{profile.activeDeprecatedRuleCount > 0 && (
<ProfileRulesDeprecatedWarning
activeDeprecatedRules={profile.activeDeprecatedRuleCount}
profile={profile.key}
/>
)}

{isDefined(sonarWayDiff) && sonarWayDiff.missingRuleCount > 0 && (
<ProfileRulesSonarWayComparison
language={profile.language}
profile={profile.key}
sonarWayMissingRules={sonarWayDiff.missingRuleCount}
sonarway={sonarWayDiff.profile}
/>
)}

{actions.edit && !profile.isBuiltIn && (
<ButtonPrimary className="it__quality-profiles__activate-rules" to={activateMoreUrl}>
{translate('quality_profiles.activate_more')}
</ButtonPrimary>
)}

{/* 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 && (
<DocumentationTooltip content={translate('quality_profiles.activate_more.help.built_in')}>
<ButtonPrimary className="it__quality-profiles__activate-rules" disabled>
{translate('quality_profiles.activate_more')}
</ButtonPrimary>
)}

{/* 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 && (
<DocumentationTooltip
content={translate('quality_profiles.activate_more.help.built_in')}
>
<ButtonPrimary className="it__quality-profiles__activate-rules" disabled>
{translate('quality_profiles.activate_more')}
</ButtonPrimary>
</DocumentationTooltip>
)}
</div>
</section>
);
}
</DocumentationTooltip>
)}
</div>
</section>
);
}

const StyledTableContainer = styled.div`
border-radius: 4px;
border: 1px solid #dddddd;
`;

const StyledTableRowHeader = styled(TableRow)`
border-radius: 3px;
background-color: #eff2f9;
`;

+ 35
- 26
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx View File

@@ -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<Props>) {
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<Props>) {

return (
<TableRow className={props.className}>
<ContentCell>
{props.type ? (
<>
<IssueTypeIcon className="sw-mr-1" query={props.type} />
{translate('issue.type', props.type, 'plural')}
</>
) : (
translate('total')
)}
</ContentCell>
<ContentCell className="sw-pl-4">{props.title}</ContentCell>
<NumericalCell>
{isDefined(props.count) && (
<Link to={activeRulesUrl}>{formatMeasure(props.count, MetricType.ShortInteger)}</Link>
{isDefined(props.count) && props.count > 0 ? (
<Link
aria-label={translateWithParameters(
'quality_profile.rules.see_x_active_x_rules',
props.count,
props.title,
)}
to={activeRulesUrl}
>
{formatMeasure(props.count, MetricType.ShortInteger)}
</Link>
) : (
<Note>0</Note>
)}
</NumericalCell>
<NumericalCell>
{isDefined(inactiveCount) &&
(inactiveCount > 0 ? (
<Link to={inactiveRulesUrl}>
{formatMeasure(inactiveCount, MetricType.ShortInteger)}
</Link>
) : (
<Note>0</Note>
))}
<NumericalCell className="sw-pr-4">
{isDefined(inactiveCount) && inactiveCount > 0 ? (
<Link
aria-label={translateWithParameters(
'quality_profile.rules.see_x_inactive_x_rules',
inactiveCount,
props.title,
)}
to={inactiveRulesUrl}
>
{formatMeasure(inactiveCount, MetricType.ShortInteger)}
</Link>
) : (
<Note>0</Note>
)}
</NumericalCell>
</TableRow>
);

+ 12
- 2
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx View File

@@ -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: <Link to={url}>{props.sonarWayMissingRules}</Link>,
linkCount: (
<Link
aria-label={translateWithParameters(
'quality_profiles.sonarway_see_x_missing_rules',
props.sonarWayMissingRules,
)}
to={url}
>
{props.sonarWayMissingRules}
</Link>
),
}}
/>
<HelpTooltip

+ 6
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -2021,6 +2021,11 @@ quality_profiles.projects_for_default=Every project not specifically associated
quality_profile.x_rules={count} rule(s)
quality_profile.lang_deprecated_x_rules={name}, {count} deprecated rule(s)
quality_profile.x_active_rules={0} active rules
quality_profile.rules.breakdown=Rule breakdown
quality_profile.rules.cct_categories_title=Clean Code Categories
quality_profile.rules.software_qualities_title=Software Qualities
quality_profile.rules.see_x_active_x_rules=See {0} active {1} rules
quality_profile.rules.see_x_inactive_x_rules=See {0} inactive {1} rules
quality_profiles.x_overridden_rules={0} overridden rules
quality_profiles.change_parent=Change Parent
quality_profiles.change_parent_warning=By changing the parent of this profile, any information on inherited rules that were manually disabled will be lost. This means some previously disabled rules might be re-enabled.
@@ -2055,6 +2060,7 @@ quality_profiles.deprecated_rules_are_still_activated=Deprecated rules are still
quality_profiles.sonarway_missing_rules=Sonar way rules not included
quality_profiles.sonarway_missing_rules_description=Recommended rules are missing from your profile
quality_profiles.x_sonarway_missing_rules={linkCount} Sonar way {count, plural, one {rule} other {rules}} not included
quality_profiles.sonarway_see_x_missing_rules=See {0} missing Sonar way rules
quality_profiles.stagnant_profiles=Stagnant Profiles
quality_profiles.not_updated_more_than_year=The following profiles haven't been updated for more than 1 year:
quality_profiles.exporters=Exporters

Loading…
Cancel
Save