]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20742 Implement pull request overview QG measures body component
author7PH <benjamin.raymond@sonarsource.com>
Tue, 17 Oct 2023 10:12:37 +0000 (12:12 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 20 Oct 2023 20:02:41 +0000 (20:02 +0000)
18 files changed:
server/sonar-web/design-system/src/components/Link.tsx
server/sonar-web/design-system/src/components/QualityGateIndicator.tsx
server/sonar-web/design-system/src/components/__tests__/Link-test.tsx
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/design-system/src/types/quality-gates.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/utils.ts
server/sonar-web/src/main/js/apps/overview/branches/MeasuresCard.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardNumber.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPercent.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/SonarLintPromotion-test.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
server/sonar-web/src/main/js/apps/overview/utils.ts
server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts
server/sonar-web/src/main/js/helpers/qualityGates.ts
server/sonar-web/src/main/js/types/quality-gates.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index c55222ed4b0e1514612ab417ebf9527902de5bc0..5d38190d3cf7eca0ab78bad4eeba379d12760ec1 100644 (file)
@@ -204,6 +204,12 @@ export const DiscreetLink = styled(HoverLink)`
 `;
 DiscreetLink.displayName = 'DiscreetLink';
 
+export const ContentLink = styled(HoverLink)`
+  --color: ${themeColor('pageContent')};
+  --border: ${themeBorder('default', 'contentLinkBorder')};
+`;
+ContentLink.displayName = 'ContentLink';
+
 export const StandoutLink = styled(StyledBaseLink)`
   ${tw`sw-font-semibold`}
   ${tw`sw-no-underline`}
index 810269c357aa141f8e1f797ed8f8244ded1d82b6..f1ddd5f4281bdd9e50284de17b9b989b0764bf76 100644 (file)
@@ -22,6 +22,7 @@ import React from 'react';
 import { theme as twTheme } from 'twin.macro';
 import { BasePlacement, PopupPlacement } from '../helpers/positioning';
 import { themeColor, themeContrast } from '../helpers/theme';
+import { QGStatus } from '../types/quality-gates';
 
 const SIZE = {
   sm: twTheme('spacing.4'),
@@ -29,8 +30,6 @@ const SIZE = {
   xl: twTheme('spacing.16'),
 };
 
-type QGStatus = 'ERROR' | 'OK' | 'NONE' | 'NOT_COMPUTED';
-
 interface Props {
   ariaLabel?: string;
   className?: string;
index 611c26d62f13142f062fe13f7d17470f6e21f712..244aaace483ea5b9de108400b8f61f00b439bf92 100644 (file)
@@ -22,7 +22,7 @@ import { screen } from '@testing-library/react';
 import React from 'react';
 import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
 import { render } from '../../helpers/testUtils';
-import { DiscreetLink, StandoutLink as Link } from '../Link';
+import { ContentLink, DiscreetLink, StandoutLink as Link } from '../Link';
 
 beforeAll(() => {
   const { location } = window;
@@ -37,7 +37,7 @@ beforeEach(() => {
 // This functionality won't be needed once we update the breadcrumbs
 it('should remove focus after link is clicked', async () => {
   const { user } = setupWithMemoryRouter(
-    <Link blurAfterClick icon={<div>Icon</div>} to="/initial" />
+    <Link blurAfterClick icon={<div>Icon</div>} to="/initial" />,
   );
 
   await user.click(screen.getByRole('link'));
@@ -63,7 +63,7 @@ it('should stop propagation when stopPropagation is true', async () => {
   const { user } = setupWithMemoryRouter(
     <button onClick={buttonOnClick} type="button">
       <Link stopPropagation to="/second" />
-    </button>
+    </button>,
   );
 
   await user.click(screen.getByRole('link'));
@@ -73,9 +73,7 @@ it('should stop propagation when stopPropagation is true', async () => {
 
 it('should call onClick when one is passed', async () => {
   const onClick = jest.fn();
-  const { user } = setupWithMemoryRouter(
-    <Link onClick={onClick} stopPropagation to="/second" />
-  );
+  const { user } = setupWithMemoryRouter(<Link onClick={onClick} stopPropagation to="/second" />);
 
   await user.click(screen.getByRole('link'));
 
@@ -98,8 +96,11 @@ it('external links are indicated by OpenNewTabIcon', () => {
   expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
 });
 
-it('discreet links also can be external indicated by the OpenNewTabIcon', () => {
-  setupWithMemoryRouter(<DiscreetLink to="https://google.com">external link</DiscreetLink>);
+it.each([
+  ['discreet', DiscreetLink],
+  ['content', ContentLink],
+])('%s links also can be external indicated by the OpenNewTabIcon', (_, LinkComponent) => {
+  setupWithMemoryRouter(<LinkComponent to="https://google.com">external link</LinkComponent>);
   expect(screen.getByRole('link')).toBeVisible();
 
   expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
@@ -125,6 +126,6 @@ const setupWithMemoryRouter = (component: JSX.Element, initialEntries = ['/initi
         />
         <Route element={<ShowPath />} path="/second" />
       </Routes>
-    </MemoryRouter>
+    </MemoryRouter>,
   );
 };
index 7494c1ab7715f1c160fa5209025259419cd43dc8..eed6bd73e7483754fbcd7da17b00b3ceedf36cd5 100644 (file)
@@ -318,6 +318,7 @@ export const lightTheme = {
     linkTooltipDefault: COLORS.indigo[200],
     linkTooltipActive: COLORS.indigo[100],
     linkBorder: COLORS.indigo[300],
+    contentLinkBorder: COLORS.blueGrey[200],
 
     // discreet select
     discreetBorder: secondary.default,
@@ -427,9 +428,13 @@ export const lightTheme = {
     qgIndicatorFailed: COLORS.red[200],
     qgIndicatorNotComputed: COLORS.blueGrey[200],
 
+    // quality gate status card
+    qgCardFailed: COLORS.red[300],
+
     // quality gate texts colors
     qgConditionNotCayc: COLORS.red[600],
     qgConditionCayc: COLORS.green[600],
+    qgCardTitle: COLORS.blueGrey[700],
 
     // main bar
     mainBar: COLORS.white,
diff --git a/server/sonar-web/design-system/src/types/quality-gates.ts b/server/sonar-web/design-system/src/types/quality-gates.ts
new file mode 100644 (file)
index 0000000..50b71b2
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+export type QGStatus = 'ERROR' | 'OK' | 'NONE' | 'NOT_COMPUTED';
index 886ec42cebf05f3d2403102025f47bf137e9053c..e4465315cbe5aa5b3902b17260539469f3629ba7 100644 (file)
@@ -88,10 +88,6 @@ function prepareChildren(r: any): Children {
   };
 }
 
-export function showLeakMeasure(branchLike?: BranchLike) {
-  return isPullRequest(branchLike);
-}
-
 function skipRootDir(breadcrumbs: ComponentMeasure[]) {
   return breadcrumbs.filter((component) => {
     return !(component.qualifier === ComponentQualifier.Directory && component.name === '/');
@@ -131,7 +127,7 @@ export function getCodeMetrics(
   if (qualifier === ComponentQualifier.Application) {
     return [...APPLICATION_METRICS];
   }
-  if (showLeakMeasure(branchLike)) {
+  if (isPullRequest(branchLike)) {
     return [...LEAK_METRICS];
   }
   return [...METRICS];
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCard.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCard.tsx
new file mode 100644 (file)
index 0000000..7f1f825
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * 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 styled from '@emotion/styled';
+import classNames from 'classnames';
+import { Card, ContentLink, PageContentFontWrapper, themeColor } from 'design-system';
+import * as React from 'react';
+import { To } from 'react-router-dom';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { localizeMetric } from '../../../helpers/measures';
+import { MetricKey } from '../../../types/metrics';
+
+interface Props {
+  url: To;
+  value: string;
+  metric: MetricKey;
+  label: string;
+  failed?: boolean;
+  icon?: React.ReactElement;
+}
+
+export default function MeasuresCard(
+  props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>,
+) {
+  const { failed, children, metric, icon, value, url, label, ...rest } = props;
+
+  return (
+    <StyledCard
+      className={classNames(
+        'sw-h-fit sw-p-8 sw-rounded-2 sw-flex sw-justify-between sw-items-center sw-text-base',
+        {
+          failed,
+        },
+      )}
+      {...rest}
+    >
+      <PageContentFontWrapper className="sw-flex sw-flex-col sw-gap-1 sw-justify-between">
+        <StyledTitleContainer className="sw-flex sw-items-center sw-gap-2 sw-font-semibold">
+          {value ? (
+            <ContentLink
+              aria-label={translateWithParameters(
+                'overview.see_more_details_on_x_of_y',
+                value,
+                localizeMetric(metric),
+              )}
+              className="it__overview-measures-value sw-text-lg"
+              to={url}
+            >
+              {value}
+            </ContentLink>
+          ) : (
+            <span> — </span>
+          )}
+          {translate(label)}
+        </StyledTitleContainer>
+        {children && <div className="sw-flex sw-flex-col">{children}</div>}
+      </PageContentFontWrapper>
+
+      {icon && <div>{icon}</div>}
+    </StyledCard>
+  );
+}
+
+export const StyledCard = styled(Card)`
+  &.failed {
+    border-color: ${themeColor('qgCardFailed')};
+  }
+`;
+
+const StyledTitleContainer = styled.div`
+  color: ${themeColor('qgCardTitle')};
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardNumber.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardNumber.tsx
new file mode 100644 (file)
index 0000000..9c512ce
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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 { TextError } from 'design-system';
+import * as React from 'react';
+import { To } from 'react-router-dom';
+import { formatMeasure } from '../../../helpers/measures';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
+import MeasuresCard from './MeasuresCard';
+
+interface Props {
+  failedConditions: QualityGateStatusConditionEnhanced[];
+  label: string;
+  url: To;
+  value: string;
+  failingConditionMetric: MetricKey;
+  requireLabel: string;
+}
+
+export default function MeasuresCardNumber(
+  props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>,
+) {
+  const { label, value, failedConditions, url, failingConditionMetric, requireLabel, ...rest } =
+    props;
+
+  const failed = Boolean(
+    failedConditions.find((condition) => condition.metric === failingConditionMetric),
+  );
+
+  return (
+    <MeasuresCard
+      url={url}
+      value={formatMeasure(value, MetricType.ShortInteger)}
+      metric={failingConditionMetric}
+      label={label}
+      failed={failed}
+      {...rest}
+    >
+      {failed && <TextError className="sw-font-regular sw-mt-2" text={requireLabel} />}
+    </MeasuresCard>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx
new file mode 100644 (file)
index 0000000..f123188
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * 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 classNames from 'classnames';
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { getLeakValue } from '../../../components/measure/utils';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { findMeasure } from '../../../helpers/measures';
+import {
+  getComponentDrilldownUrl,
+  getComponentIssuesUrl,
+  getComponentSecurityHotspotsUrl,
+} from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricKey } from '../../../types/metrics';
+import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
+import { Component, MeasureEnhanced } from '../../../types/types';
+import { MeasurementType, getMeasurementMetricKey } from '../utils';
+import MeasuresCardNumber from './MeasuresCardNumber';
+import MeasuresCardPercent from './MeasuresCardPercent';
+
+interface Props {
+  className?: string;
+  branchLike?: BranchLike;
+  component: Component;
+  measures: MeasureEnhanced[];
+  failedConditions: QualityGateStatusConditionEnhanced[];
+}
+
+export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>) {
+  const { branchLike, component, measures, failedConditions, className } = props;
+
+  const intl = useIntl();
+
+  const newViolations = getLeakValue(findMeasure(measures, MetricKey.new_violations)) as string;
+  const newSecurityHotspots = getLeakValue(
+    findMeasure(measures, MetricKey.new_security_hotspots),
+  ) as string;
+
+  return (
+    <div className={classNames('sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4', className)}>
+      <MeasuresCardNumber
+        data-test="overview__measures-new-violations"
+        label={newViolations === '1' ? 'issue' : 'issues'}
+        url={getComponentIssuesUrl(component.key, {
+          ...getBranchLikeQuery(branchLike),
+          resolved: 'false',
+        })}
+        value={newViolations}
+        failedConditions={failedConditions}
+        failingConditionMetric={MetricKey.new_violations}
+        requireLabel={intl.formatMessage(
+          { id: 'overview.quality_gate.require_fixing' },
+          {
+            count: newViolations,
+          },
+        )}
+      />
+
+      <MeasuresCardNumber
+        label={
+          newSecurityHotspots === '1'
+            ? 'issue.type.SECURITY_HOTSPOT'
+            : 'issue.type.SECURITY_HOTSPOT.plural'
+        }
+        url={getComponentSecurityHotspotsUrl(component.key, {
+          ...getBranchLikeQuery(branchLike),
+          resolved: 'false',
+        })}
+        value={newSecurityHotspots}
+        failedConditions={failedConditions}
+        failingConditionMetric={MetricKey.new_security_hotspots_reviewed}
+        requireLabel={intl.formatMessage(
+          { id: 'overview.quality_gate.require_reviewing' },
+          {
+            count: newSecurityHotspots,
+          },
+        )}
+      />
+
+      <MeasuresCardPercent
+        componentKey={component.key}
+        branchLike={branchLike}
+        measurementType={MeasurementType.Coverage}
+        label="overview.quality_gate.coverage"
+        url={getComponentDrilldownUrl({
+          componentKey: component.key,
+          metric: getMeasurementMetricKey(MeasurementType.Coverage, true),
+          branchLike,
+          listView: true,
+        })}
+        failedConditions={failedConditions}
+        failingConditionMetric={MetricKey.new_coverage}
+        newLinesMetric={MetricKey.new_lines_to_cover}
+        afterMergeMetric={MetricKey.coverage}
+        measures={measures}
+      />
+
+      <MeasuresCardPercent
+        componentKey={component.key}
+        branchLike={branchLike}
+        measurementType={MeasurementType.Duplication}
+        label="overview.quality_gate.duplications"
+        url={getComponentDrilldownUrl({
+          componentKey: component.key,
+          metric: getMeasurementMetricKey(MeasurementType.Duplication, true),
+          branchLike,
+          listView: true,
+        })}
+        failedConditions={failedConditions}
+        failingConditionMetric={MetricKey.new_duplicated_lines_density}
+        newLinesMetric={MetricKey.new_lines}
+        afterMergeMetric={MetricKey.duplicated_lines_density}
+        measures={measures}
+      />
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPercent.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPercent.tsx
new file mode 100644 (file)
index 0000000..6b7da48
--- /dev/null
@@ -0,0 +1,171 @@
+/*
+ * 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 {
+  ContentLink,
+  CoverageIndicator,
+  DuplicationsIndicator,
+  LightLabel,
+  TextError,
+} from 'design-system';
+import * as React from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { To } from 'react-router-dom';
+import { duplicationRatingConverter, getLeakValue } from '../../../components/measure/utils';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
+import { MeasureEnhanced } from '../../../types/types';
+import { MeasurementType, getMeasurementMetricKey } from '../utils';
+import MeasuresCard from './MeasuresCard';
+
+interface Props {
+  componentKey: string;
+  branchLike?: BranchLike;
+  measurementType: MeasurementType;
+  label: string;
+  url: To;
+  measures: MeasureEnhanced[];
+  failedConditions: QualityGateStatusConditionEnhanced[];
+  failingConditionMetric: MetricKey;
+  newLinesMetric: MetricKey;
+  afterMergeMetric: MetricKey;
+}
+
+export default function MeasuresCardPercent(
+  props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>,
+) {
+  const {
+    componentKey,
+    branchLike,
+    measurementType,
+    label,
+    url,
+    measures,
+    failedConditions,
+    failingConditionMetric,
+    newLinesMetric,
+    afterMergeMetric,
+  } = props;
+
+  const intl = useIntl();
+
+  const metricKey = getMeasurementMetricKey(measurementType, true);
+
+  const value = getLeakValue(findMeasure(measures, metricKey));
+
+  const newLinesValue = getLeakValue(findMeasure(measures, newLinesMetric));
+  const newLinesLabel =
+    measurementType === MeasurementType.Coverage
+      ? 'overview.quality_gate.on_x_new_lines_to_cover'
+      : 'overview.quality_gate.on_x_new_lines';
+  const newLinesUrl = getComponentDrilldownUrl({
+    componentKey,
+    metric: newLinesMetric,
+    branchLike,
+    listView: true,
+  });
+
+  const afterMergeValue = findMeasure(measures, afterMergeMetric)?.value;
+
+  const failedCondition = failedConditions.find(
+    (condition) => condition.metric === failingConditionMetric,
+  );
+
+  let errorRequireLabel = '';
+  if (failedCondition) {
+    errorRequireLabel = intl.formatMessage(
+      { id: 'overview.quality_gate.required_x' },
+      {
+        operator: failedCondition.op === 'GT' ? '<=' : '>=',
+        value: formatMeasure(
+          failedCondition.level === 'ERROR' ? failedCondition.error : failedCondition.warning,
+          MetricType.Percent,
+          {
+            decimals: 2,
+            omitExtraDecimalZeros: true,
+          },
+        ),
+      },
+    );
+  }
+
+  return (
+    <MeasuresCard
+      value={formatMeasure(value, MetricType.Percent)}
+      metric={metricKey}
+      url={url}
+      label={label}
+      failed={Boolean(failedCondition)}
+      icon={renderIcon(measurementType, value)}
+    >
+      <div className="sw-flex sw-flex-col">
+        <LightLabel className="sw-flex sw-items-center sw-gap-1">
+          <FormattedMessage
+            defaultMessage={translate(newLinesLabel)}
+            id={newLinesLabel}
+            values={{
+              link: (
+                <ContentLink
+                  aria-label={translateWithParameters(
+                    'overview.see_more_details_on_x_y',
+                    newLinesValue ?? '0',
+                    localizeMetric(newLinesMetric),
+                  )}
+                  className="sw-body-md-highlight sw-text-lg"
+                  to={newLinesUrl}
+                >
+                  {formatMeasure(newLinesValue ?? '0', MetricType.ShortInteger)}
+                </ContentLink>
+              ),
+            }}
+          />
+        </LightLabel>
+
+        {afterMergeValue && (
+          <LightLabel className="sw-mt-2">
+            <FormattedMessage
+              defaultMessage={translate('overview.quality_gate.x_estimated_after_merge')}
+              id="overview.quality_gate.x_estimated_after_merge"
+              values={{
+                value: <strong>{formatMeasure(afterMergeValue, MetricType.Percent)}</strong>,
+              }}
+            />
+          </LightLabel>
+        )}
+
+        {failedCondition && (
+          <TextError className="sw-mt-2 sw-font-regular" text={errorRequireLabel} />
+        )}
+      </div>
+    </MeasuresCard>
+  );
+}
+
+function renderIcon(type: MeasurementType, value?: string) {
+  if (type === MeasurementType.Coverage) {
+    return <CoverageIndicator value={value} size="md" />;
+  }
+
+  const rating = duplicationRatingConverter(Number(value));
+  return <DuplicationsIndicator rating={rating} size="md" />;
+}
index 0f5a23cdf86e037d025131fa5d9c33f884babe9a..33d73e09e6026b509fb6488cc4c96b7bb0857328 100644 (file)
@@ -31,7 +31,7 @@ import {
 } from '../../../helpers/urls';
 import { BranchLike } from '../../../types/branch-like';
 import { IssueType } from '../../../types/issues';
-import { MetricKey, MetricType } from '../../../types/metrics';
+import { MetricType } from '../../../types/metrics';
 import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
 import { Component } from '../../../types/types';
 import {
@@ -50,7 +50,7 @@ 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),
+    (condition) => !METRICS_REPORTED_IN_OVERVIEW_CARDS.includes(condition.metric),
   );
 
   return (
index 734f7a40a2211f4db63e711bf20460b553e64888..9e81b9937448c62655bb4c4bc473ded32a66f2fe 100644 (file)
@@ -54,7 +54,7 @@ it.each(
   ].map(Array.of),
 )('should show message for %s', async (metric) => {
   renderSonarLintPromotion({
-    qgConditions: [mockQualityGateStatusCondition({ metric: metric as string })],
+    qgConditions: [mockQualityGateStatusCondition({ metric: metric as MetricKey })],
   });
 
   expect(
index d070c52d00641e3e0ec514cc36aea9d64dadc59a..4f52f9ce8ef5f0315db8b11bf132d00a0d73c9a9 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 {
-  BasicSeparator,
-  Card,
-  CenteredLayout,
-  CoverageIndicator,
-  DuplicationsIndicator,
-  Spinner,
-} from 'design-system';
+import { BasicSeparator, CenteredLayout, Spinner } from 'design-system';
 import { uniq } from 'lodash';
 import * as React from 'react';
 import { useEffect, useState } from 'react';
 import { getMeasuresWithMetrics } from '../../../api/measures';
-import { duplicationRatingConverter } from '../../../components/measure/utils';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures';
 import { isDefined } from '../../../helpers/types';
 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 MeasuresCardPanel from '../branches/MeasuresCardPanel';
 import BranchQualityGate from '../components/BranchQualityGate';
 import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
 import MetaTopBar from '../components/MetaTopBar';
 import SonarLintPromotion from '../components/SonarLintPromotion';
 import '../styles.css';
-import { MeasurementType, PR_METRICS, Status } from '../utils';
+import { PR_METRICS, Status } from '../utils';
 
 interface Props {
   branchLike: PullRequest;
@@ -121,58 +111,17 @@ export default function PullRequestOverview(props: Props) {
             />
           )}
 
-          <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>
-              ))}
+          <MeasuresCardPanel
+            className="sw-flex-1"
+            branchLike={branchLike}
+            component={component}
+            failedConditions={failedConditions}
+            measures={measures}
+          />
 
-              {[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>
   );
 }
-
-function renderMeasureIcon(type: MeasurementType) {
-  if (type === MeasurementType.Coverage) {
-    return function CoverageIndicatorRenderer(value?: string) {
-      return <CoverageIndicator value={value} size="md" />;
-    };
-  }
-
-  return function renderDuplicationIcon(value?: string) {
-    const rating = duplicationRatingConverter(Number(value));
-
-    return <DuplicationsIndicator rating={rating} size="md" />;
-  };
-}
index aeb0251f16b0e91b546d0eb5142336bf8cd09fd2..dbb77eac779ebd5faa5e4638adcf324f6c6c7d13 100644 (file)
@@ -86,6 +86,7 @@ export const PR_METRICS: string[] = [
   MetricKey.new_coverage,
   MetricKey.new_lines_to_cover,
 
+  MetricKey.new_violations,
   MetricKey.duplicated_lines_density,
   MetricKey.new_duplicated_lines_density,
   MetricKey.new_lines,
index 5e8672a99724a32d7c2ec816f7c515cb597e13e5..679ad51835209fbf68998f91d7c0077bf612885c 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { MetricKey } from '../../types/metrics';
 import {
   QualityGateApplicationStatus,
   QualityGateProjectStatus,
@@ -70,7 +71,7 @@ export function mockQualityGateStatusCondition(
     actual: '10',
     error: '0',
     level: 'ERROR',
-    metric: 'foo',
+    metric: MetricKey.bugs,
     op: 'GT',
     ...overrides,
   };
@@ -83,7 +84,7 @@ export function mockQualityGateStatusConditionEnhanced(
     actual: '10',
     error: '0',
     level: 'ERROR',
-    metric: 'foo',
+    metric: MetricKey.bugs,
     op: 'GT',
     measure: mockMeasureEnhanced({ ...(overrides.measure || {}) }),
     ...overrides,
index b9407fcca22cf0a6d815e1662f97a7d9d4d18225..2055bb1e9d10190645a51aabb4f9a9db8867d922 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { MetricKey } from '../types/metrics';
 import {
   QualityGateApplicationStatusChildProject,
   QualityGateProjectStatus,
@@ -32,7 +33,7 @@ export function extractStatusConditionsFromProjectStatus(
         actual: c.actualValue,
         error: c.errorThreshold,
         level: c.status,
-        metric: c.metricKey,
+        metric: c.metricKey as MetricKey,
         op: c.comparator,
         period: c.periodIndex,
       }))
@@ -48,7 +49,7 @@ export function extractStatusConditionsFromApplicationStatusChildProject(
         actual: c.value,
         error: c.errorThreshold,
         level: c.status,
-        metric: c.metric,
+        metric: c.metric as MetricKey,
         op: c.comparator,
         period: c.periodIndex,
       }))
index 9faa052c5368e07b502dd09e55219e10ee1bc47e..03f5af20b4866ac738e31386d5de9293fe5f61dc 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { BranchLike } from './branch-like';
+import { MetricKey } from './metrics';
 import { CaycStatus, MeasureEnhanced, Metric, Status } from './types';
 import { UserBase } from './users';
 
@@ -76,7 +77,7 @@ export interface QualityGateStatusCondition {
   actual?: string;
   error?: string;
   level: Status;
-  metric: string;
+  metric: MetricKey;
   op: string;
   period?: number;
   warning?: string;
index 869b49dc5972c80f72d999e96527b4e0e44cadff..6d7bb4f79d8c3829818746a6ee3131c01d273392 100644 (file)
@@ -3766,6 +3766,14 @@ overview.quality_gate.conditions.cayc.link=Learn why
 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_gate.coverage=Coverage
+overview.quality_gate.duplications=Duplications
+overview.quality_gate.on_x_new_lines_to_cover=On {link} New Lines to cover
+overview.quality_gate.on_x_new_lines=On {link} New Lines
+overview.quality_gate.x_estimated_after_merge={value} Estimated after merge
+overview.quality_gate.require_fixing={count, plural, one {requires} other {require}} fixing
+overview.quality_gate.require_reviewing={count, plural, one {requires} other {require}} reviewing
+overview.quality_gate.required_x=required {operator} {value}
 overview.quality_profiles=Quality Profiles used
 overview.new_code_period_x=New Code: {0}
 overview.max_new_code_period_from_x=Max New Code from: {0}