* 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';
import ProfileRulesRow from './ProfileRulesRow';
import ProfileRulesSonarWayComparison from './ProfileRulesSonarWayComparison';
-const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT'];
-
interface Props {
profile: Profile;
}
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;
+`;
*/
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) {
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>
);