]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20545 Show new rule breakdown using Clean Code Taxonomy in quality profile
author7PH <benjamin.raymond@sonarsource.com>
Thu, 28 Sep 2023 00:20:31 +0000 (02:20 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 10 Oct 2023 20:02:43 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 7c147187de0c18589994128ee7213862eab02089..5ec2b36d62bb78311bd832adcdd93090004beb8a 100644 (file)
  * 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;
+`;
index db7fed442414d80ea82abafd8048b639069f238c..566ccaf06599ef05eff343be80d4d278ddde9fbd 100644 (file)
  */
 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>
   );
index 279a43620a725b534805419207003d11d5273cb6..2d852db5c48a595c5c5a294e02da73ea4fca6de0 100644 (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
index 83ccb43d34c8dbb697b6fb7e62852312b2d249cd..a65e2e937c9f256b592fb0c2c2a94b0539894699 100644 (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