]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20716 Fix missing projects in Rule Details
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 1 Nov 2023 17:27:42 +0000 (18:27 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 2 Nov 2023 20:02:42 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 84d1a4fa5fad248c54631813426176a9698ceef0..d7f9b6020bd7219b32556fe736d0bc7db0a955f4 100644 (file)
@@ -28,11 +28,13 @@ import {
   mockRuleRepository,
 } from '../../helpers/testMocks';
 import { RuleRepository, SearchRulesResponse } from '../../types/coding-rules';
+import { ComponentQualifier, Visibility } from '../../types/component';
 import { RawIssuesResponse } from '../../types/issues';
 import { SearchRulesQuery } from '../../types/rules';
 import { SecurityStandard } from '../../types/security';
 import { Dict, Rule, RuleActivation, RuleDetails, RulesUpdateRequest } from '../../types/types';
 import { NoticeType } from '../../types/users';
+import { getComponentData } from '../components';
 import { getFacet } from '../issues';
 import {
   Profile,
@@ -64,6 +66,7 @@ jest.mock('../rules');
 jest.mock('../issues');
 jest.mock('../users');
 jest.mock('../quality-profiles');
+jest.mock('../components');
 
 type FacetFilter = Pick<
   SearchRulesQuery,
@@ -125,10 +128,11 @@ export default class CodingRulesServiceMock {
     jest.mocked(bulkDeactivateRules).mockImplementation(this.handleBulkDeactivateRules);
     jest.mocked(activateRule).mockImplementation(this.handleActivateRule);
     jest.mocked(deactivateRule).mockImplementation(this.handleDeactivateRule);
-    jest.mocked(getFacet).mockImplementation(this.handleGetGacet);
+    jest.mocked(getFacet).mockImplementation(this.handleGetFacet);
     jest.mocked(getRuleTags).mockImplementation(this.handleGetRuleTags);
     jest.mocked(getCurrentUser).mockImplementation(this.handleGetCurrentUser);
     jest.mocked(dismissNotice).mockImplementation(this.handleDismissNotification);
+    jest.mocked(getComponentData).mockImplementation(this.handleGetComponentData);
   }
 
   getRulesWithoutDetails(rules: RuleDetails[]) {
@@ -274,19 +278,23 @@ export default class CodingRulesServiceMock {
     return this.qualityProfile.filter((qp) => qp.language === language);
   }
 
-  handleGetGacet = (): Promise<{
+  handleGetFacet = (): Promise<{
     facet: { count: number; val: string }[];
     response: RawIssuesResponse;
   }> => {
     return this.reply({
-      facet: [],
+      facet: [
+        { count: 135, val: 'project-1' },
+        { count: 65, val: 'project-2' },
+        { count: 13, val: 'project-3' },
+      ],
       response: {
         components: [],
         effortTotal: 0,
         facets: [],
         issues: [],
         languages: [],
-        paging: { total: 0, pageIndex: 1, pageSize: 1 },
+        paging: { total: 213, pageIndex: 1, pageSize: 1 },
       },
     });
   };
@@ -610,6 +618,18 @@ export default class CodingRulesServiceMock {
     return Promise.reject();
   };
 
+  handleGetComponentData = (data: { component: string }) => {
+    return Promise.resolve({
+      ancestors: [],
+      component: {
+        key: data.component,
+        name: data.component.toUpperCase().split(/[ -.]/g).join(' '),
+        qualifier: ComponentQualifier.Project,
+        visibility: Visibility.Public,
+      },
+    });
+  };
+
   reply<T>(response: T): Promise<T> {
     return Promise.resolve(cloneDeep(response));
   }
index 057c9242af3ffe7ca0a643410f38dac78320ee16..f6f1431dd5ffe615fbdea96a4ed3e75a487c3f16 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { ContentCell, Link, Spinner, SubHeadingHighlight, Table, TableRow } from 'design-system';
+import { keyBy } from 'lodash';
 import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { getComponentData } from '../../../api/components';
 import { getFacet } from '../../../api/issues';
 import withAvailableFeatures, {
   WithAvailableFeaturesProps,
@@ -30,7 +33,7 @@ import { getIssuesUrl } from '../../../helpers/urls';
 import { Feature } from '../../../types/features';
 import { FacetName } from '../../../types/issues';
 import { MetricType } from '../../../types/metrics';
-import { RuleDetails } from '../../../types/types';
+import { Dict, RuleDetails } from '../../../types/types';
 
 interface Props extends WithAvailableFeaturesProps {
   ruleDetails: Pick<RuleDetails, 'key' | 'type'>;
@@ -45,9 +48,12 @@ interface Project {
 interface State {
   loading: boolean;
   projects?: Project[];
-  total?: number;
+  totalIssues?: number;
+  totalProjects?: number;
 }
 
+const MAX_VIOLATING_PROJECTS = 10;
+
 export class RuleDetailsIssues extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = { loading: false };
@@ -80,17 +86,16 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> {
       },
       FacetName.Projects,
     ).then(
-      ({ facet, response }) => {
+      async ({ facet, response }) => {
         if (this.mounted) {
-          const { components = [], paging } = response;
-          const projects = [];
-          for (const item of facet) {
-            const project = components.find((component) => component.key === item.val);
-            if (project) {
-              projects.push({ count: item.count, key: project.key, name: project.name });
-            }
-          }
-          this.setState({ projects, loading: false, total: paging.total });
+          const { paging } = response;
+
+          this.setState({
+            projects: await this.getProjects(facet.slice(0, MAX_VIOLATING_PROJECTS)),
+            loading: false,
+            totalIssues: paging.total,
+            totalProjects: facet.length,
+          });
         }
       },
       () => {
@@ -101,12 +106,36 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> {
     );
   };
 
+  /**
+   * Retrieve the names of the projects, to display nicely
+   * (The facet only contains key & count)
+   */
+  getProjects = async (facet: { count: number; val: string }[]) => {
+    const projects: Dict<{ key: string; name: string }> = keyBy(
+      await Promise.all(
+        facet.map((item) =>
+          getComponentData({ component: item.val })
+            .then((response) => ({
+              key: item.val,
+              name: response.component.name,
+            }))
+            .catch(() => ({ key: item.val, name: item.val })),
+        ),
+      ),
+      'key',
+    );
+
+    return facet.map((item) => {
+      return { count: item.count, key: item.val, name: projects[item.val].name };
+    });
+  };
+
   renderTotal = () => {
     const {
       ruleDetails: { key },
     } = this.props;
 
-    const { total } = this.state;
+    const { totalIssues: total } = this.state;
     if (total === undefined) {
       return null;
     }
@@ -146,7 +175,8 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { loading, projects = [] } = this.state;
+    const { ruleDetails } = this.props;
+    const { loading, projects = [], totalProjects } = this.state;
 
     return (
       <div className="sw-mb-8">
@@ -157,19 +187,36 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> {
           </SubHeadingHighlight>
 
           {projects.length > 0 ? (
-            <Table
-              className="sw-mt-6"
-              columnCount={2}
-              header={
-                <TableRow>
-                  <ContentCell colSpan={2}>
-                    {translate('coding_rules.most_violating_projects')}
-                  </ContentCell>
-                </TableRow>
-              }
-            >
-              {projects.map(this.renderProject)}
-            </Table>
+            <>
+              <Table
+                className="sw-mt-6"
+                columnCount={2}
+                header={
+                  <TableRow>
+                    <ContentCell colSpan={2}>
+                      {translate('coding_rules.most_violating_projects')}
+                    </ContentCell>
+                  </TableRow>
+                }
+              >
+                {projects.map(this.renderProject)}
+              </Table>
+              {totalProjects !== undefined && totalProjects > projects.length && (
+                <div className="sw-text-center sw-mt-4">
+                  <FormattedMessage
+                    id="coding_rules.most_violating_projects.more_x"
+                    values={{
+                      count: totalProjects - projects.length,
+                      link: (
+                        <Link to={getIssuesUrl({ resolved: 'false', rules: ruleDetails.key })}>
+                          <FormattedMessage id="coding_rules.most_violating_projects.link" />
+                        </Link>
+                      ),
+                    }}
+                  />
+                </div>
+              )}
+            </>
           ) : (
             <div className="sw-mb-6">
               {translate('coding_rules.no_issue_detected_for_projects')}
index 580d9e76bcde3dd122dd34c49ec527d49cf9d485..9f17c79b7f7e456e7e0e7045d31aab65034932ef 100644 (file)
@@ -2307,6 +2307,8 @@ coding_rules.inherits="{0}" inherits from "{1}"
 coding_rules.issues=Issues
 coding_rules.issues.only_main_branches=Only issues from the main project branches are included.
 coding_rules.most_violating_projects=Most Violating Projects
+coding_rules.most_violating_projects.more_x={count} more projects contain issues raised from this rule. {link}
+coding_rules.most_violating_projects.link=See full list of issues
 coding_rules.need_extend_or_copy=Rules in built-in Quality Profiles can't be changed. You can create a customizable Quality Profile based on a built-in one by Copying or Extending it in the Quality Profiles list.
 coding_rules.no_results=No Coding Rules
 coding_rules.no_issue_detected_for_projects=No issues were detected for this rule in the main project branches.