]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17816 Improve QG display for Apps
authorJeremy Davis <jeremy.davis@sonarsource.com>
Thu, 12 Jan 2023 10:42:43 +0000 (11:42 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 12 Jan 2023 20:02:52 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 827630106c230bc06136df980e6d5e835ae3fd41..e7953550f34b4bda80a76510232acdddd7edb043 100644 (file)
@@ -185,20 +185,22 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
     ).then(
       (results) => {
         if (this.mounted) {
-          const qgStatuses = results.map(({ measures = [], project, projectBranchLike }) => {
-            const { key, name, status, isCaycCompliant } = project;
-            const conditions = extractStatusConditionsFromApplicationStatusChildProject(project);
-            const failedConditions = this.getFailedConditions(conditions, measures);
-
-            return {
-              failedConditions,
-              isCaycCompliant,
-              key,
-              name,
-              status,
-              branchLike: projectBranchLike,
-            };
-          });
+          const qgStatuses = results
+            .map(({ measures = [], project, projectBranchLike }) => {
+              const { key, name, status, isCaycCompliant } = project;
+              const conditions = extractStatusConditionsFromApplicationStatusChildProject(project);
+              const failedConditions = this.getFailedConditions(conditions, measures);
+
+              return {
+                failedConditions,
+                isCaycCompliant,
+                key,
+                name,
+                status,
+                branchLike: projectBranchLike,
+              };
+            })
+            .sort((a, b) => Math.sign(b.failedConditions.length - a.failedConditions.length));
 
           this.setState({
             loadingStatus: false,
index a3c6acce726f81e370f3362beb2632069252c3fe..c3dca159e4bacc2bc136f481154110c83cd747ef 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
 import Link from '../../../components/common/Link';
 import { Alert } from '../../../components/ui/Alert';
 import { translate } from '../../../helpers/l10n';
+import { getQualityGateUrl } from '../../../helpers/urls';
+import { Component } from '../../../types/types';
 
-export default function CleanAsYouCodeWarning() {
+interface Props {
+  component: Pick<Component, 'key' | 'qualifier' | 'qualityGate'>;
+}
+
+export default function CleanAsYouCodeWarning({ component }: Props) {
   return (
     <>
       <Alert variant="warning">{translate('overview.quality_gate.conditions.cayc.warning')}</Alert>
       <p className="big-spacer-top big-spacer-bottom">
-        {translate('overview.quality_gate.conditions.cayc.details')}
+        {component.qualityGate ? (
+          <FormattedMessage
+            id="overview.quality_gate.conditions.cayc.details"
+            defaultMessage={translate('overview.quality_gate.conditions.cayc.details')}
+            values={{
+              link: (
+                <Link to={getQualityGateUrl(component.qualityGate.key)}>
+                  {translate('overview.quality_gate.conditions.cayc.details.link')}
+                </Link>
+              ),
+            }}
+          />
+        ) : (
+          translate('overview.quality_gate.conditions.cayc.details.no_link')
+        )}
       </p>
+
       <Link
         target="_blank"
         to="https://docs.sonarqube.org/latest/user-guide/clean-as-you-code/#quality-gate"
index d810c7e245024956a032be67938a6d5af09765a1..25610006a7b6d5b5a54a1165174de566cf652d17 100644 (file)
 import classNames from 'classnames';
 import { flatMap } from 'lodash';
 import * as React from 'react';
+import Link from '../../../components/common/Link';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
+import QualifierIcon from '../../../components/icons/QualifierIcon';
 import { Alert } from '../../../components/ui/Alert';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { ComponentQualifier } from '../../../types/component';
+import { getProjectQueryUrl } from '../../../helpers/urls';
+import { ComponentQualifier, isApplication } from '../../../types/component';
 import { QualityGateStatus } from '../../../types/quality-gates';
 import { Component } from '../../../types/types';
 import SonarLintPromotion from '../components/SonarLintPromotion';
 import QualityGatePanelSection from './QualityGatePanelSection';
 
 export interface QualityGatePanelProps {
-  component: Pick<Component, 'key' | 'qualifier'>;
+  component: Pick<Component, 'key' | 'qualifier' | 'qualityGate'>;
   loading?: boolean;
   qgStatuses?: QualityGateStatus[];
 }
@@ -51,6 +55,12 @@ export function QualityGatePanel(props: QualityGatePanelProps) {
     0
   );
 
+  const nonCaycProjectsInApp = isApplication(component.qualifier)
+    ? qgStatuses
+        .filter(({ isCaycCompliant }) => !isCaycCompliant)
+        .sort(({ name: a }, { name: b }) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
+    : [];
+
   const showIgnoredConditionWarning =
     component.qualifier === ComponentQualifier.Project &&
     qgStatuses.some((p) => Boolean(p.ignoredConditions));
@@ -119,6 +129,42 @@ export function QualityGatePanel(props: QualityGatePanelProps) {
                 ))}
               </div>
             )}
+
+            {nonCaycProjectsInApp.length > 0 && (
+              <div className="overview-quality-gate-conditions-list padded big-spacer-top">
+                <Alert variant="warning">
+                  {translateWithParameters(
+                    'overview.quality_gate.application.non_cayc.projects_x',
+                    nonCaycProjectsInApp.length
+                  )}
+                </Alert>
+                <div className="spacer big-spacer-bottom big-spacer-top">
+                  <Link
+                    target="_blank"
+                    to="https://docs.sonarqube.org/latest/user-guide/clean-as-you-code/#quality-gate"
+                  >
+                    {translate('overview.quality_gate.conditions.cayc.link')}
+                  </Link>
+                </div>
+                <hr className="big-spacer-top big-spacer-bottom" />
+                <ul className="spacer-left spacer-bottom">
+                  {nonCaycProjectsInApp.map(({ key, name, branchLike }) => (
+                    <li key={key} className="text-ellipsis spacer-bottom" title={name}>
+                      <Link
+                        className="link-no-underline"
+                        to={getProjectQueryUrl(key, getBranchLikeQuery(branchLike))}
+                      >
+                        <QualifierIcon
+                          className="little-spacer-right"
+                          qualifier={ComponentQualifier.Project}
+                        />
+                        {name}
+                      </Link>
+                    </li>
+                  ))}
+                </ul>
+              </div>
+            )}
           </>
         )}
       </div>
index ac9bc24303029667e800cacb09a7150e1350d43c..62eba2a1c201045fd88c594ad25bc73ef200860c 100644 (file)
@@ -35,7 +35,7 @@ import CleanAsYouCodeWarning from './CleanAsYouCodeWarning';
 
 export interface QualityGatePanelSectionProps {
   branchLike?: BranchLike;
-  component: Pick<Component, 'key' | 'qualifier'>;
+  component: Pick<Component, 'key' | 'qualifier' | 'qualityGate'>;
   qgStatus: QualityGateStatus;
 }
 
@@ -77,7 +77,17 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
     setCollapsed(!collapsed);
   }, [collapsed]);
 
-  if (qgStatus.failedConditions.length === 0 && qgStatus.isCaycCompliant) {
+  /*
+   * Show if project has failed conditions or that
+   * it is a single non-cayc project
+   * In the context of an App, only show projects with failed conditions
+   */
+  if (
+    !(
+      qgStatus.failedConditions.length > 0 ||
+      (!qgStatus.isCaycCompliant && !isApplication(component.qualifier))
+    )
+  ) {
     return null;
   }
 
@@ -88,6 +98,7 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
   const showName = isApplication(component.qualifier);
 
   const showSectionTitles =
+    isApplication(component.qualifier) ||
     !qgStatus.isCaycCompliant ||
     (overallFailedConditions.length > 0 && newCodeFailedConditions.length > 0);
 
@@ -119,9 +130,9 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
 
       {!collapsed && (
         <>
-          {!qgStatus.isCaycCompliant && (
+          {!qgStatus.isCaycCompliant && !isApplication(component.qualifier) && (
             <div className="big-padded bordered-bottom overview-quality-gate-conditions-list">
-              <CleanAsYouCodeWarning />
+              <CleanAsYouCodeWarning component={component} />
             </div>
           )}
 
index 7b0e14a91f4a4244486bde99464468ec60996a07..24c1b6dbf336efed0adb057032c06b816a661e13 100644 (file)
@@ -23,17 +23,24 @@ import * as React from 'react';
 import selectEvent from 'react-select-event';
 import { getMeasuresWithPeriodAndMetrics } from '../../../../api/measures';
 import { getProjectActivity } from '../../../../api/projectActivity';
-import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
+import {
+  getApplicationQualityGate,
+  getQualityGateProjectStatus,
+} from '../../../../api/quality-gates';
 import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
 import { getActivityGraph, saveActivityGraph } from '../../../../components/activity-graph/utils';
 import { isDiffMetric } from '../../../../helpers/measures';
 import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockAnalysis } from '../../../../helpers/mocks/project-activity';
-import { mockQualityGateProjectStatus } from '../../../../helpers/mocks/quality-gates';
+import {
+  mockQualityGateApplicationStatus,
+  mockQualityGateProjectStatus,
+} from '../../../../helpers/mocks/quality-gates';
 import { mockLoggedInUser, mockPeriod } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
 import { ComponentQualifier } from '../../../../types/component';
+import { MetricKey } from '../../../../types/metrics';
 import { GraphType } from '../../../../types/project-activity';
 import { Measure, Metric } from '../../../../types/types';
 import BranchOverview, { BRANCH_OVERVIEW_ACTIVITY_GRAPH, NO_CI_DETECTED } from '../BranchOverview';
@@ -257,6 +264,40 @@ describe('application overview', () => {
     expect(screen.getByText('Bar')).toBeInTheDocument();
   });
 
+  it("should show projects that don't have a compliant quality gate", async () => {
+    const appStatus = mockQualityGateApplicationStatus({
+      projects: [
+        { key: '1', name: 'first project', conditions: [], isCaycCompliant: false, status: 'OK' },
+        { key: '2', name: 'second', conditions: [], isCaycCompliant: true, status: 'OK' },
+        { key: '3', name: 'number 3', conditions: [], isCaycCompliant: false, status: 'OK' },
+        {
+          key: '4',
+          name: 'four',
+          conditions: [
+            {
+              comparator: 'GT',
+              metric: MetricKey.bugs,
+              status: 'ERROR',
+              value: '3',
+              errorThreshold: '0',
+            },
+          ],
+          isCaycCompliant: false,
+          status: 'ERROR',
+        },
+      ],
+    });
+    jest.mocked(getApplicationQualityGate).mockResolvedValueOnce(appStatus);
+
+    renderBranchOverview({ component });
+    expect(
+      await screen.findByText('overview.quality_gate.application.non_cayc.projects_x.3')
+    ).toBeInTheDocument();
+    expect(screen.getByText('first project')).toBeInTheDocument();
+    expect(screen.queryByText('second')).not.toBeInTheDocument();
+    expect(screen.getByText('number 3')).toBeInTheDocument();
+  });
+
   it('should correctly show an app as empty', async () => {
     jest.mocked(getMeasuresWithPeriodAndMetrics).mockResolvedValueOnce({
       component: { key: '', name: '', qualifier: ComponentQualifier.Application, measures: [] },
index dd37526a134e137552f2f2b34a922eb857bb307a..aff23a9b3bdc44eb34848d89113dfea7901a9a97 100644 (file)
@@ -7,7 +7,30 @@ exports[`should render correctly 1`] = `
   <div
     className="big-padded bordered-bottom overview-quality-gate-conditions-list"
   >
-    <CleanAsYouCodeWarning />
+    <CleanAsYouCodeWarning
+      component={
+        {
+          "breadcrumbs": [],
+          "key": "my-project",
+          "name": "MyProject",
+          "qualifier": "TRK",
+          "qualityGate": {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          },
+          "qualityProfiles": [
+            {
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
+            },
+          ],
+          "tags": [],
+        }
+      }
+    />
   </div>
   <h4
     className="big-padded overview-quality-gate-conditions-section-title"
@@ -220,11 +243,6 @@ exports[`should render correctly 2`] = `
       </h3>
     </div>
   </ButtonPlain>
-  <div
-    className="big-padded bordered-bottom overview-quality-gate-conditions-list"
-  >
-    <CleanAsYouCodeWarning />
-  </div>
   <h4
     className="big-padded overview-quality-gate-conditions-section-title"
   >
index cbe186bf8a244703de0ed2b098cd0fef242c6eb1..7d76754f96550d107b6a2890e730aeec67ada9c6 100644 (file)
@@ -3257,8 +3257,11 @@ overview.quality_gate.ignored_conditions=Some Quality Gate conditions on New Cod
 overview.quality_gate.ignored_conditions.tooltip=At the start of a new code period, if very few lines have been added or modified, it might be difficult to reach the desired level of code coverage or duplications. To prevent Quality Gate failure when there's little that can be done about it, Quality Gate conditions about duplications in new code and coverage on new code are ignored until the number of new lines is at least 20. An administrator can disable this in the general settings.
 overview.quality_gate.conditions_on_new_code=Only conditions on new code that are defined in the Quality Gate are checked. See the {link} associated to the project for details.
 overview.quality_gate.conditions.cayc.warning=This Quality Gate does not comply with Clean as You Code
-overview.quality_gate.conditions.cayc.details=Clean as You Code conditions ensure that only Clean Code passes the gate.
+overview.quality_gate.conditions.cayc.details.no_link=A Clean as You Code quality gate ensures that only Clean Code passes it.
+overview.quality_gate.conditions.cayc.details=A Clean as You Code quality gate ensures that only Clean Code passes it. {link} to view this project's quality gate.
+overview.quality_gate.conditions.cayc.details.link=Click here 
 overview.quality_gate.conditions.cayc.link=Learn more: Clean as You Code
+overview.quality_gate.application.non_cayc.projects_x={0} project(s) in this application use a Quality Gate that does not comply with Clean as You Code
 overview.quality_gate.show_project_conditions_x=Show failed conditions for project {0}
 overview.quality_gate.hide_project_conditions_x=Hide failed conditions for project {0}
 overview.quality_profiles=Quality Profiles used