]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20742 Implement quality gate status
authorstanislavh <stanislav.honcharov@sonarsource.com>
Wed, 18 Oct 2023 10:17:41 +0000 (12:17 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 20 Oct 2023 20:02:41 +0000 (20:02 +0000)
12 files changed:
server/sonar-web/__mocks__/react-intl.tsx
server/sonar-web/design-system/src/components/__tests__/layouts-test.tsx
server/sonar-web/design-system/src/helpers/constants.ts
server/sonar-web/src/main/js/apps/overview/components/BranchQualityGate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx
server/sonar-web/src/main/js/apps/overview/utils.ts
server/sonar-web/src/main/js/helpers/testMocks.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index aa20e55fbda6bc5cd930259b978b1bc73be6dc39..03edab2f5fdca83a4435024d92a2da17710c73f6 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 { isObject, some } from 'lodash';
 import * as React from 'react';
 
 module.exports = {
   ...jest.requireActual('react-intl'),
   useIntl: () => ({
-    formatMessage: ({ id }, values = {}) => [id, ...Object.values(values)].join('.'),
+    formatMessage: ({ id }, values = {}) => {
+      if (some(values, isObject)) {
+        return (
+          <>
+            {id}
+            {Object.entries(values).map(([key, value]) => (
+              <React.Fragment key={key}>{value}</React.Fragment>
+            ))}
+          </>
+        );
+      }
+      return [id, ...Object.values(values)].join('.');
+    },
   }),
   FormattedMessage: ({ id, values }: { id: string; values?: { [x: string]: React.ReactNode } }) => {
     return (
index 02e32e26edf9ef2a47860971d975c0731192248a..41ed6a7eecc16804e8af80807b48b1ad7d8fa19b 100644 (file)
@@ -26,7 +26,7 @@ describe('CenteredLayout', () => {
 
     expect(screen.getByText('content')).toHaveStyle({
       'min-width': '1280px',
-      'max-width': '1400px',
+      'max-width': '1280px',
     });
   });
 });
index 368b81e998655581d1f19063089a02ed0d0b09d0..ce1c4080f0ea07e46ed263e88c3d89c857b64264 100644 (file)
@@ -50,7 +50,7 @@ export const INPUT_SIZES = {
 };
 
 export const LAYOUT_VIEWPORT_MIN_WIDTH = 1280;
-export const LAYOUT_VIEWPORT_MAX_WIDTH = 1400;
+export const LAYOUT_VIEWPORT_MAX_WIDTH = 1280;
 export const LAYOUT_VIEWPORT_MAX_WIDTH_LARGE = 1680;
 export const LAYOUT_MAIN_CONTENT_GUTTER = 60;
 export const LAYOUT_SIDEBAR_WIDTH = 240;
diff --git a/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGate.tsx b/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGate.tsx
new file mode 100644 (file)
index 0000000..1b39965
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { HelperHintIcon, LightPrimary, QualityGateIndicator, TextMuted } from 'design-system';
+import React from 'react';
+import { useIntl } from 'react-intl';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
+import { BranchLike } from '../../../types/branch-like';
+import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
+import { Component, Status } from '../../../types/types';
+import BranchQualityGateConditions from './BranchQualityGateConditions';
+
+interface Props {
+  status: Status;
+  branchLike?: BranchLike;
+  component: Pick<Component, 'key'>;
+  failedConditions: QualityGateStatusConditionEnhanced[];
+}
+
+export default function BranchQualityGate(props: Readonly<Props>) {
+  const { status, branchLike, component, failedConditions } = props;
+
+  return (
+    <>
+      <BranchQGStatus status={status} />
+      <BranchQualityGateConditions
+        branchLike={branchLike}
+        component={component}
+        failedConditions={failedConditions}
+      />
+    </>
+  );
+}
+
+function BranchQGStatus({ status }: Readonly<Pick<Props, 'status'>>) {
+  const intl = useIntl();
+
+  return (
+    <div className="sw-flex sw-items-center sw-mb-5">
+      <QualityGateIndicator
+        status={status}
+        className="sw-mr-2"
+        size="xl"
+        ariaLabel={intl.formatMessage(
+          { id: 'overview.quality_gate_x' },
+          { '0': intl.formatMessage({ id: `overview.gate.${status}` }) },
+        )}
+      />
+      <div className="sw-flex sw-flex-col sw-justify-around">
+        <div className="sw-flex sw-items-center">
+          <TextMuted
+            className="sw-body-sm"
+            text={intl.formatMessage({ id: 'overview.quality_gate' })}
+          />
+          <HelpTooltip
+            className="sw-ml-2"
+            overlay={intl.formatMessage({ id: 'overview.quality_gate.help' })}
+          >
+            <HelperHintIcon aria-label="help-tooltip" />
+          </HelpTooltip>
+        </div>
+        <div>
+          <LightPrimary as="h1" className="sw-heading-xl">
+            {intl.formatMessage({ id: `metric.level.${status}` })}
+          </LightPrimary>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx b/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx
new file mode 100644 (file)
index 0000000..0f5a23c
--- /dev/null
@@ -0,0 +1,197 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { ChevronRightIcon, DangerButtonSecondary } from 'design-system';
+import React from 'react';
+import { useIntl } from 'react-intl';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { getLocalizedMetricName } from '../../../helpers/l10n';
+import { formatMeasure, getShortType, isDiffMetric } from '../../../helpers/measures';
+import {
+  getComponentDrilldownUrl,
+  getComponentIssuesUrl,
+  getComponentSecurityHotspotsUrl,
+} from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { IssueType } from '../../../types/issues';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
+import { Component } from '../../../types/types';
+import {
+  METRICS_REPORTED_IN_OVERVIEW_CARDS,
+  RATING_METRICS_MAPPING,
+  RATING_TO_SEVERITIES_MAPPING,
+} from '../utils';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: Pick<Component, 'key'>;
+  failedConditions: QualityGateStatusConditionEnhanced[];
+}
+
+export default function BranchQualityGateConditions(props: Readonly<Props>) {
+  const { branchLike, component, failedConditions } = props;
+
+  const filteredFailedConditions = failedConditions.filter(
+    (condition) => !METRICS_REPORTED_IN_OVERVIEW_CARDS.includes(condition.metric as MetricKey),
+  );
+
+  return (
+    <ul className="sw-flex sw-items-center sw-gap-2 sw-flex-wrap">
+      {filteredFailedConditions.map((condition) => (
+        <li key={condition.metric}>
+          <FailedQGCondition branchLike={branchLike} component={component} condition={condition} />
+        </li>
+      ))}
+    </ul>
+  );
+}
+
+function FailedQGCondition(
+  props: Readonly<
+    Pick<Props, 'branchLike' | 'component'> & { condition: QualityGateStatusConditionEnhanced }
+  >,
+) {
+  const { branchLike, component, condition } = props;
+  const url = getQGConditionUrl(component.key, condition, branchLike);
+
+  return (
+    <DangerButtonSecondary className="sw-px-2 sw-py-1 sw-rounded-1/2 sw-body-sm" to={url}>
+      <FailedMetric condition={condition} />
+      <ChevronRightIcon className="sw-ml-1" />
+    </DangerButtonSecondary>
+  );
+}
+
+interface FailedMetricProps {
+  condition: QualityGateStatusConditionEnhanced;
+}
+
+export function FailedMetric(props: Readonly<FailedMetricProps>) {
+  const {
+    condition: {
+      measure: { metric },
+    },
+  } = props;
+
+  if (metric.type === MetricType.Rating) {
+    return <FailedRatingMetric {...props} />;
+  }
+
+  return <FailedGeneralMetric {...props} />;
+}
+
+function FailedRatingMetric({ condition }: Readonly<FailedMetricProps>) {
+  const {
+    error,
+    measure: {
+      metric: { type, domain },
+    },
+  } = condition;
+  const intl = useIntl();
+
+  return (
+    <>
+      {intl.formatMessage(
+        { id: 'overview.failed_condition.x_required' },
+        {
+          metric: `${intl.formatMessage({
+            id: `metric_domain.${domain}`,
+          })} ${intl.formatMessage({ id: 'metric.type.RATING' }).toLowerCase()}`,
+          threshold: (
+            <strong className="sw-body-sm-highlight sw-ml-1">{formatMeasure(error, type)}</strong>
+          ),
+        },
+      )}
+    </>
+  );
+}
+
+function FailedGeneralMetric({ condition }: Readonly<FailedMetricProps>) {
+  const {
+    error,
+    measure: { metric },
+  } = condition;
+  const intl = useIntl();
+  const measureFormattingOptions = { decimals: 2, omitExtraDecimalZeros: true };
+
+  return (
+    <>
+      {intl.formatMessage(
+        { id: 'overview.failed_condition.x_required' },
+        {
+          metric: (
+            <>
+              <strong className="sw-body-sm-highlight sw-mr-1">
+                {formatMeasure(
+                  condition.actual,
+                  getShortType(metric.type),
+                  measureFormattingOptions,
+                )}
+              </strong>
+              {getLocalizedMetricName(metric, true)}
+            </>
+          ),
+          threshold: (
+            <strong className="sw-body-sm-highlight sw-ml-1">
+              {condition.op === 'GT' ? <>&le;</> : <>&ge;</>}{' '}
+              {formatMeasure(error, getShortType(metric.type), measureFormattingOptions)}
+            </strong>
+          ),
+        },
+      )}
+    </>
+  );
+}
+
+function getQGConditionUrl(
+  componentKey: string,
+  condition: QualityGateStatusConditionEnhanced,
+  branchLike?: BranchLike,
+) {
+  const { metric } = condition;
+  const sinceLeakPeriod = isDiffMetric(metric);
+  const issueType = RATING_METRICS_MAPPING[metric];
+
+  if (issueType) {
+    if (issueType === IssueType.SecurityHotspot) {
+      return getComponentSecurityHotspotsUrl(componentKey, {
+        ...getBranchLikeQuery(branchLike),
+        ...(sinceLeakPeriod ? { sinceLeakPeriod: 'true' } : {}),
+      });
+    }
+    return getComponentIssuesUrl(componentKey, {
+      resolved: 'false',
+      types: issueType,
+      ...getBranchLikeQuery(branchLike),
+      ...(sinceLeakPeriod ? { sinceLeakPeriod: 'true' } : {}),
+      ...(issueType !== IssueType.CodeSmell
+        ? { severities: RATING_TO_SEVERITIES_MAPPING[Number(condition.error) - 1] }
+        : {}),
+    });
+  }
+
+  return getComponentDrilldownUrl({
+    componentKey,
+    metric,
+    branchLike,
+    listView: true,
+  });
+}
index 1bb11ca11142427882c39f8532229d582c4f8356..2104460da8a3c6177eaf7198568ef4ce099d96fe 100644 (file)
@@ -36,6 +36,7 @@ import { IssueType } from '../../../types/issues';
 import { MetricKey, MetricType } from '../../../types/metrics';
 import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
 import { Component, Dict } from '../../../types/types';
+import { RATING_TO_SEVERITIES_MAPPING } from '../utils';
 
 interface Props {
   branchLike?: BranchLike;
@@ -71,13 +72,6 @@ export default class QualityGateCondition extends React.PureComponent<Props> {
   }
 
   getUrlForBugsOrVulnerabilities(type: string, inNewCodePeriod: boolean) {
-    const RATING_TO_SEVERITIES_MAPPING = [
-      'BLOCKER,CRITICAL,MAJOR,MINOR',
-      'BLOCKER,CRITICAL,MAJOR',
-      'BLOCKER,CRITICAL',
-      'BLOCKER',
-    ];
-
     const { condition } = this.props;
     const threshold = condition.level === 'ERROR' ? condition.error : condition.warning;
 
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx
new file mode 100644 (file)
index 0000000..714a6d7
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockQualityGateStatusConditionEnhanced } from '../../../../helpers/mocks/quality-gates';
+import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byLabelText, byRole } from '../../../../helpers/testSelector';
+import { MetricKey, MetricType } from '../../../../types/metrics';
+import { FCProps } from '../../../../types/misc';
+import { Status } from '../../utils';
+import BranchQualityGate from '../BranchQualityGate';
+
+it('renders failed QG', () => {
+  renderBranchQualityGate();
+
+  // Maintainability rating condition
+  expect(
+    byRole('link', {
+      name: 'overview.failed_condition.x_requiredmetric_domain.Maintainability metric.type.rating A',
+    }).get(),
+  ).toBeInTheDocument();
+
+  // Security Hotspots rating condition
+  expect(
+    byRole('link', {
+      name: 'overview.failed_condition.x_requiredmetric_domain.Security Review metric.type.rating A',
+    }).get(),
+  ).toBeInTheDocument();
+
+  // New code smells
+  expect(
+    byRole('link', {
+      name: 'overview.failed_condition.x_required 5 Code Smells ≤ 1',
+    }).get(),
+  ).toBeInTheDocument();
+
+  // Conditions to cover
+  expect(
+    byRole('link', {
+      name: 'overview.failed_condition.x_required 5 Conditions to cover ≥ 10',
+    }).get(),
+  ).toBeInTheDocument();
+
+  expect(byLabelText('overview.quality_gate_x.overview.gate.ERROR').get()).toBeInTheDocument();
+});
+
+it('renders passed QG', () => {
+  renderBranchQualityGate({ failedConditions: [], status: Status.OK });
+
+  expect(byLabelText('overview.quality_gate_x.overview.gate.OK').get()).toBeInTheDocument();
+  expect(byRole('link').query()).not.toBeInTheDocument();
+});
+
+function renderBranchQualityGate(props: Partial<FCProps<typeof BranchQualityGate>> = {}) {
+  return renderComponent(
+    <BranchQualityGate
+      status={Status.ERROR}
+      branchLike={mockPullRequest()}
+      component={mockComponent()}
+      failedConditions={[
+        mockQualityGateStatusConditionEnhanced({
+          actual: '5.0',
+          error: '1.0',
+          metric: MetricKey.new_maintainability_rating,
+          measure: mockMeasureEnhanced({
+            metric: mockMetric({
+              domain: 'Maintainability',
+              key: MetricKey.new_maintainability_rating,
+              name: 'Maintainability rating',
+              type: MetricType.Rating,
+            }),
+          }),
+        }),
+        mockQualityGateStatusConditionEnhanced({
+          actual: '5.0',
+          error: '1.0',
+          metric: MetricKey.new_security_review_rating,
+          measure: mockMeasureEnhanced({
+            metric: mockMetric({
+              domain: 'Security Review',
+              key: MetricKey.new_security_review_rating,
+              name: 'Security Review Rating',
+              type: MetricType.Rating,
+            }),
+          }),
+        }),
+        mockQualityGateStatusConditionEnhanced({
+          actual: '5',
+          error: '1',
+          metric: MetricKey.new_code_smells,
+          measure: mockMeasureEnhanced({
+            metric: mockMetric({
+              domain: 'Maintainability',
+              key: MetricKey.new_code_smells,
+              name: 'Code Smells',
+              type: MetricType.ShortInteger,
+            }),
+          }),
+        }),
+        mockQualityGateStatusConditionEnhanced({
+          actual: '5',
+          error: '10',
+          op: 'up',
+          metric: MetricKey.conditions_to_cover,
+          measure: mockMeasureEnhanced({
+            metric: mockMetric({
+              key: MetricKey.conditions_to_cover,
+              name: 'Conditions to cover',
+              type: MetricType.ShortInteger,
+            }),
+          }),
+        }),
+      ]}
+      {...props}
+    />,
+  );
+}
index ae2114dac0fb5e6bfca68522127cec7affb27434..d070c52d00641e3e0ec514cc36aea9d64dadc59a 100644 (file)
@@ -23,34 +23,25 @@ import {
   CenteredLayout,
   CoverageIndicator,
   DuplicationsIndicator,
-  HelperHintIcon,
-  Link,
   Spinner,
-  TextMuted,
 } from 'design-system';
 import { uniq } from 'lodash';
 import * as React from 'react';
 import { useEffect, useState } from 'react';
-import { FormattedMessage } from 'react-intl';
 import { getMeasuresWithMetrics } from '../../../api/measures';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
 import { duplicationRatingConverter } from '../../../components/measure/utils';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
-import { translate } from '../../../helpers/l10n';
 import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures';
 import { isDefined } from '../../../helpers/types';
-import { getQualityGateUrl, getQualityGatesUrl } from '../../../helpers/urls';
 import { useBranchStatusQuery } from '../../../queries/branch';
 import { PullRequest } from '../../../types/branch-like';
 import { IssueType } from '../../../types/issues';
 import { Component, MeasureEnhanced } from '../../../types/types';
 import MeasuresPanelIssueMeasure from '../branches/MeasuresPanelIssueMeasure';
 import MeasuresPanelPercentMeasure from '../branches/MeasuresPanelPercentMeasure';
+import BranchQualityGate from '../components/BranchQualityGate';
 import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
 import MetaTopBar from '../components/MetaTopBar';
-import QualityGateConditions from '../components/QualityGateConditions';
-import QualityGateStatusHeader from '../components/QualityGateStatusHeader';
-import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView';
 import SonarLintPromotion from '../components/SonarLintPromotion';
 import '../styles.css';
 import { MeasurementType, PR_METRICS, Status } from '../utils';
@@ -107,11 +98,6 @@ export default function PullRequestOverview(props: Props) {
     return null;
   }
 
-  const path =
-    component.qualityGate === undefined
-      ? getQualityGatesUrl()
-      : getQualityGateUrl(component.qualityGate.name);
-
   const failedConditions = conditions
     .filter((condition) => condition.level === 'ERROR')
     .map((c) => enhanceConditionWithMeasure(c, measures))
@@ -119,93 +105,58 @@ export default function PullRequestOverview(props: Props) {
 
   return (
     <CenteredLayout>
-      <div className="it__pr-overview sw-mt-12">
-        <MetaTopBar branchLike={branchLike} measures={measures} />
-        <BasicSeparator className="sw-my-4" />
-
-        {ignoredConditions && <IgnoredConditionWarning />}
-
-        <div className="sw-flex sw-flex-col sw-mr-12 width-30">
-          <Card>
-            {status && (
-              <QualityGateStatusHeader
-                status={status}
-                failedConditionCount={failedConditions.length}
-              />
-            )}
-
-            <div className="sw-flex sw-items-center sw-mb-4">
-              <TextMuted text={translate('overview.on_new_code_long')} />
-              <HelpTooltip
-                className="sw-ml-2"
-                overlay={
-                  <FormattedMessage
-                    defaultMessage={translate('overview.quality_gate.conditions_on_new_code')}
-                    id="overview.quality_gate.conditions_on_new_code"
-                    values={{
-                      link: <Link to={path}>{translate('overview.quality_gate')}</Link>,
-                    }}
-                  />
-                }
-              >
-                <HelperHintIcon aria-label="help-tooltip" />
-              </HelpTooltip>
-            </div>
-
-            {status === Status.OK && failedConditions.length === 0 && (
-              <QualityGateStatusPassedView />
-            )}
-
-            {status !== Status.OK && <BasicSeparator />}
-
-            {failedConditions.length > 0 && (
-              <div>
-                <QualityGateConditions
-                  branchLike={branchLike}
-                  collapsible
-                  component={component}
-                  failedConditions={failedConditions}
-                />
-              </div>
-            )}
-          </Card>
-          <SonarLintPromotion qgConditions={conditions} />
-        </div>
-
-        <div className="sw-flex-1">
-          <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
-            {[
-              IssueType.Bug,
-              IssueType.CodeSmell,
-              IssueType.Vulnerability,
-              IssueType.SecurityHotspot,
-            ].map((type: IssueType) => (
-              <Card key={type} className="sw-p-8">
-                <MeasuresPanelIssueMeasure
-                  branchLike={branchLike}
-                  component={component}
-                  isNewCodeTab
-                  measures={measures}
-                  type={type}
-                />
-              </Card>
-            ))}
-
-            {[MeasurementType.Coverage, MeasurementType.Duplication].map(
-              (type: MeasurementType) => (
+      <div className="it__pr-overview sw-mt-12 sw-grid sw-grid-cols-12">
+        <div className="sw-col-start-2 sw-col-span-10">
+          <MetaTopBar branchLike={branchLike} measures={measures} />
+          <BasicSeparator className="sw-my-4" />
+
+          {ignoredConditions && <IgnoredConditionWarning />}
+
+          {status && (
+            <BranchQualityGate
+              branchLike={branchLike}
+              component={component}
+              status={status}
+              failedConditions={failedConditions}
+            />
+          )}
+
+          <div className="sw-flex-1">
+            <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
+              {[
+                IssueType.Bug,
+                IssueType.CodeSmell,
+                IssueType.Vulnerability,
+                IssueType.SecurityHotspot,
+              ].map((type: IssueType) => (
                 <Card key={type} className="sw-p-8">
-                  <MeasuresPanelPercentMeasure
+                  <MeasuresPanelIssueMeasure
                     branchLike={branchLike}
                     component={component}
+                    isNewCodeTab
                     measures={measures}
-                    ratingIcon={renderMeasureIcon(type)}
                     type={type}
-                    useDiffMetric
                   />
                 </Card>
-              ),
-            )}
+              ))}
+
+              {[MeasurementType.Coverage, MeasurementType.Duplication].map(
+                (type: MeasurementType) => (
+                  <Card key={type} className="sw-p-8">
+                    <MeasuresPanelPercentMeasure
+                      branchLike={branchLike}
+                      component={component}
+                      measures={measures}
+                      ratingIcon={renderMeasureIcon(type)}
+                      type={type}
+                      useDiffMetric
+                    />
+                  </Card>
+                ),
+              )}
+            </div>
           </div>
+          <SonarLintPromotion qgConditions={conditions} />
         </div>
       </div>
     </CenteredLayout>
index 26af400499a8af5b4cf13f1db21cb2797f6e1f40..7fc29a23c5802619fca195e05c9f8371c27dabef 100644 (file)
@@ -26,6 +26,7 @@ import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockQualityGateProjectCondition } from '../../../../helpers/mocks/quality-gates';
 import { mockLoggedInUser, mockMeasure, mockMetric } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byLabelText, byRole } from '../../../../helpers/testSelector';
 import { ComponentPropsType } from '../../../../helpers/testUtils';
 import { ComponentQualifier } from '../../../../types/component';
 import { MetricKey, MetricType } from '../../../../types/metrics';
@@ -121,6 +122,8 @@ it('should render correctly for a passed QG', async () => {
   renderPullRequestOverview();
 
   await waitFor(async () => expect(await screen.findByText('metric.level.OK')).toBeInTheDocument());
+  expect(screen.getByLabelText('overview.quality_gate_x.overview.gate.OK')).toBeInTheDocument();
+
   expect(screen.getByText('metric.new_lines.name')).toBeInTheDocument();
   expect(screen.getByText(/overview.last_analysis_x/)).toBeInTheDocument();
 });
@@ -165,18 +168,21 @@ it('should render correctly for a failed QG', async () => {
   renderPullRequestOverview();
 
   await waitFor(async () =>
-    expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument(),
+    expect(
+      await byLabelText('overview.quality_gate_x.overview.gate.ERROR').find(),
+    ).toBeInTheDocument(),
   );
 
-  expect(await screen.findByText('1.0% metric.new_coverage.name')).toBeInTheDocument();
-  expect(await screen.findByText('quality_gates.operator.GT 2.0%')).toBeInTheDocument();
-
   expect(
-    await screen.findByText('1.0% metric.duplicated_lines.name quality_gates.conditions.new_code'),
+    byRole('link', {
+      name: 'overview.failed_condition.x_required 10.0% duplicated_lines ≤ 1.0%',
+    }).get(),
+  ).toBeInTheDocument();
+  expect(
+    byRole('link', {
+      name: 'overview.failed_condition.x_required 10 new_bugs ≤ 3',
+    }).get(),
   ).toBeInTheDocument();
-  expect(await screen.findByText('quality_gates.operator.GT 1.0%')).toBeInTheDocument();
-
-  expect(screen.getByText('quality_gates.operator.GT 3')).toBeInTheDocument();
 });
 
 function renderPullRequestOverview(
index abca784fab816f002cb6a984bbf23d966fb5ff92..aeb0251f16b0e91b546d0eb5142336bf8cd09fd2 100644 (file)
@@ -26,7 +26,7 @@ import { parseAsString } from '../../helpers/query';
 import { IssueType } from '../../types/issues';
 import { MetricKey } from '../../types/metrics';
 import { AnalysisMeasuresVariations, MeasureHistory } from '../../types/project-activity';
-import { RawQuery } from '../../types/types';
+import { Dict, RawQuery } from '../../types/types';
 
 export const METRICS: string[] = [
   // quality gate
@@ -152,6 +152,35 @@ const MEASUREMENTS_MAP = {
   },
 };
 
+export const RATING_TO_SEVERITIES_MAPPING = [
+  'BLOCKER,CRITICAL,MAJOR,MINOR',
+  'BLOCKER,CRITICAL,MAJOR',
+  'BLOCKER,CRITICAL',
+  'BLOCKER',
+];
+
+export const RATING_METRICS_MAPPING: Dict<IssueType> = {
+  [MetricKey.reliability_rating]: IssueType.Bug,
+  [MetricKey.new_reliability_rating]: IssueType.Bug,
+  [MetricKey.security_rating]: IssueType.Vulnerability,
+  [MetricKey.new_security_rating]: IssueType.Vulnerability,
+  [MetricKey.sqale_rating]: IssueType.CodeSmell,
+  [MetricKey.new_maintainability_rating]: IssueType.CodeSmell,
+  [MetricKey.security_review_rating]: IssueType.SecurityHotspot,
+  [MetricKey.new_security_review_rating]: IssueType.SecurityHotspot,
+};
+
+export const METRICS_REPORTED_IN_OVERVIEW_CARDS = [
+  MetricKey.new_violations,
+  MetricKey.violations,
+  MetricKey.new_coverage,
+  MetricKey.coverage,
+  MetricKey.new_security_hotspots_reviewed,
+  MetricKey.security_hotspots_reviewed,
+  MetricKey.new_duplicated_lines_density,
+  MetricKey.duplicated_lines_density,
+];
+
 export function getIssueRatingName(type: IssueType) {
   return translate('metric_domain', ISSUETYPE_METRIC_KEYS_MAP[type].ratingName);
 }
index b353b7baea1d6db3153a99ad1d9559b44dfa1218..b0fece2070af0f209891e3bfd079aa10e58dfa9f 100644 (file)
@@ -34,6 +34,7 @@ import { RuleRepository } from '../types/coding-rules';
 import { EditionKey } from '../types/editions';
 import { IssueScope, IssueSeverity, IssueStatus, IssueType, RawIssue } from '../types/issues';
 import { Language } from '../types/languages';
+import { MetricKey, MetricType } from '../types/metrics';
 import { Notification } from '../types/notifications';
 import { DumpStatus, DumpTask } from '../types/project-dump';
 import { TaskStatuses } from '../types/tasks';
@@ -395,11 +396,14 @@ export function mockLocation(overrides: Partial<Location> = {}): Location {
   };
 }
 
-export function mockMetric(overrides: Partial<Pick<Metric, 'key' | 'name' | 'type'>> = {}): Metric {
-  const key = overrides.key || 'coverage';
+export function mockMetric(
+  overrides: Partial<Pick<Metric, 'key' | 'name' | 'type' | 'domain'>> = {},
+): Metric {
+  const key = overrides.key || MetricKey.coverage;
   const name = overrides.name || key;
-  const type = overrides.type || 'PERCENT';
+  const type = overrides.type || MetricType.Percent;
   return {
+    ...overrides,
     id: key,
     key,
     name,
index fc35a833d05eb1b64d827564ff169283b2da03cd..869b49dc5972c80f72d999e96527b4e0e44cadff 100644 (file)
@@ -2897,6 +2897,7 @@ metric.ncloc_language_distribution.description=Non Commenting Lines of Code Dist
 metric.ncloc_language_distribution.name=Lines of Code Per Language
 metric.new_blocker_violations.description=New Blocker issues
 metric.new_blocker_violations.name=New Blocker Issues
+metric.new_blocker_violations.short_name=Blocker Issues
 metric.new_branch_coverage.description=Condition coverage of new/changed code
 metric.new_branch_coverage.name=Condition Coverage on New Code
 metric.new_branch_coverage.extra_short_name=Condition Coverage
@@ -2915,6 +2916,7 @@ metric.new_coverage.name=Coverage on New Code
 metric.new_coverage.short_name=Coverage
 metric.new_critical_violations.description=New Critical issues
 metric.new_critical_violations.name=New Critical Issues
+metric.new_critical_violations.short_name=Critical Issues
 metric.new_development_cost.description=Development cost on new code
 metric.new_development_cost.name=Development Cost on New Code
 metric.new_duplicated_blocks.name=Duplicated Blocks on New Code
@@ -2930,6 +2932,7 @@ metric.new_duplicated_lines_density.short_name=Duplications
 metric.new_duplicated_lines_density.extra_short_name=Density
 metric.new_info_violations.description=New Info issues
 metric.new_info_violations.name=New Info Issues
+metric.new_info_violations.short_name=Info Issues
 metric.new_it_branch_coverage.description=Integration tests condition coverage of new/changed code
 metric.new_it_branch_coverage.name=Condition Coverage by IT on New Code
 metric.new_it_conditions_to_cover.description=New conditions to cover by integration tests
@@ -2955,8 +2958,10 @@ metric.new_maintainability_rating.name=Maintainability Rating on New Code
 metric.new_maintainability_rating.extra_short_name=Rating
 metric.new_major_violations.description=New Major issues
 metric.new_major_violations.name=New Major Issues
+metric.new_major_violations.short_name=Major Issues
 metric.new_minor_violations.description=New Minor issues
 metric.new_minor_violations.name=New Minor Issues
+metric.new_minor_violations.short_name=Minor Issues
 metric.new_lines.name=New Lines
 metric.new_lines.description=New lines
 metric.new_lines.short_name=Lines
@@ -3740,6 +3745,7 @@ system.version_is_availble={version} is available
 #------------------------------------------------------------------------------
 overview.1_condition_failed=1 failed condition
 overview.X_conditions_failed={0} failed conditions
+overview.failed_condition.x_required={metric} required {threshold}
 overview.fix_failed_conditions_with_sonarlint=Fix issues before they fail your Quality Gate with {link} in your IDE. Power up with connected mode!
 overview.quality_gate.status=Quality Gate Status
 overview.quality_gate=Quality Gate