]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19018 New UI for the Measures section of Project Overview
authorJeremy Davis <jeremy.davis@sonarsource.com>
Thu, 20 Apr 2023 09:55:19 +0000 (11:55 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 25 Apr 2023 20:03:01 +0000 (20:03 +0000)
25 files changed:
server/sonar-web/config/jest/SetupReactTestingLibrary.ts
server/sonar-web/config/jest/SetupTestEnvironment.ts
server/sonar-web/design-system/src/components/Link.tsx
server/sonar-web/design-system/src/components/Text.tsx
server/sonar-web/src/main/js/apps/overview/branches/ApplicationLeakPeriodInfo.tsx
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
server/sonar-web/src/main/js/apps/overview/branches/DrilldownMeasureValue.tsx
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelCard.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasure.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasureRow.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasure.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasureLabel.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx
server/sonar-web/src/main/js/apps/overview/components/IssueRating.tsx
server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueLabel-test.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueRating-test.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
server/sonar-web/src/main/js/helpers/issues.ts
server/sonar-web/src/main/js/helpers/measures.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index f7d03248c533625a616caa3228b0cc4265d94bce..afaa0a4fcfb6d0ca85e4e5312fad5d178f78ed56 100644 (file)
@@ -17,9 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { configure } from '@testing-library/dom';
 import '@testing-library/jest-dom';
+import { configure } from '@testing-library/react';
 
 configure({
-  asyncUtilTimeout: 3000
+  asyncUtilTimeout: 3000,
 });
index 3e393db4fb695b1be3f1ea46ee251917ac9419b6..543508551066199adddf53784ed762579eeafeb4 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 React from 'react';
+
+(window as any).React = React;
+
 const content = document.createElement('div');
 content.id = 'content';
 document.documentElement.appendChild(content);
index 237b9bf32237a95f9bde76acf70a64e1b2c8088c..8f1bb78e43f44378194371a6e1fb2e54ad167e5c 100644 (file)
@@ -173,6 +173,18 @@ export const LinkBox = styled(StyledBaseLink)`
 `;
 LinkBox.displayName = 'LinkBox';
 
+export const DiscreetLinkBox = styled(StyledBaseLink)`
+  text-decoration: none;
+
+  &:hover,
+  &:focus,
+  &:active {
+    background-color: none;
+    display: block;
+  }
+`;
+LinkBox.displayName = 'DiscreetLinkBox';
+
 export const DiscreetLink = styled(HoverLink)`
   --border: ${themeBorder('default', 'linkDiscreet')};
 `;
index 540074b8bc8d218044a082ea0b6f82ae985eccd9..775091713c542c2f991bddce2d15e595008291c6 100644 (file)
@@ -93,3 +93,11 @@ const StyledPageTitle = styled(StyledText)`
 const StyledTextError = styled(StyledText)`
   color: ${themeColor('danger')};
 `;
+
+export const LightLabel = styled.span`
+  color: ${themeColor('pageContentLight')};
+`;
+
+export const LightPrimary = styled.span`
+  color: ${themeContrast('primaryLight')};
+`;
index b103b681c98bb7979fbbfe2c5c378b5bfc66f2f5..e33d5ea5e6735ff3361d17acd8f617068e983868 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 { HelperHintIcon } from 'design-system';
 import * as React from 'react';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
 import DateFromNow from '../../../components/intl/DateFromNow';
@@ -29,18 +30,20 @@ export interface ApplicationLeakPeriodInfoProps {
 
 export function ApplicationLeakPeriodInfo({ leakPeriod }: ApplicationLeakPeriodInfoProps) {
   return (
-    <div className="note spacer-top display-inline-flex-center">
+    <>
       <DateFromNow date={leakPeriod.date}>
         {(fromNow) => translateWithParameters('overview.started_x', fromNow)}
       </DateFromNow>
       <HelpTooltip
-        className="little-spacer-left"
+        className="sw-ml-1"
         overlay={translateWithParameters(
           'overview.max_new_code_period_from_x',
           leakPeriod.projectName
         )}
-      />
-    </div>
+      >
+        <HelperHintIcon />
+      </HelpTooltip>
+    </>
   );
 }
 
index 3b194749b6aa49aec8f3248e43d97db62d3ebd47..7593b4ae651fe3a847d928b96fc451b59b96b26a 100644 (file)
@@ -93,7 +93,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
             <NoCodeWarning branchLike={branch} component={component} measures={measures} />
           ) : (
             <div className="sw-flex">
-              <div className="width-30 sw-mr-12">
+              <div className="width-30 sw-mr-12 sw-pt-6">
                 <QualityGatePanel
                   component={component}
                   loading={loadingStatus}
@@ -102,7 +102,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
               </div>
 
               <div className="sw-flex-1">
-                <div className="sw-flex sw-flex-col">
+                <div className="sw-flex sw-flex-col sw-pt-6">
                   <MeasuresPanel
                     appLeak={appLeak}
                     branch={branch}
@@ -110,6 +110,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
                     loading={loadingStatus}
                     measures={measures}
                     period={period}
+                    qgStatuses={qgStatuses}
                   />
 
                   <ActivityPanel
index 305bb5f58687c4939840312df7e1c3ed0446ef61..ca94e6fe352a0220217845367edd9223425574a0 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 { DrilldownLink } from 'design-system';
 import * as React from 'react';
-import DrilldownLink from '../../../components/shared/DrilldownLink';
-import { getLocalizedMetricName, translateWithParameters } from '../../../helpers/l10n';
+import { translateWithParameters } from '../../../helpers/l10n';
 import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
 import { BranchLike } from '../../../types/branch-like';
-import { MetricKey } from '../../../types/metrics';
+import { MetricKey, MetricType } from '../../../types/metrics';
 import { Component, MeasureEnhanced } from '../../../types/types';
 
 export interface DrilldownMeasureValueProps {
@@ -36,34 +37,29 @@ export function DrilldownMeasureValue(props: DrilldownMeasureValueProps) {
   const { branchLike, component, measures, metric } = props;
   const measure = findMeasure(measures, metric);
 
-  let content;
   if (!measure || measure.value === undefined) {
-    content = <span className="overview-measures-value text-light">-</span>;
-  } else {
-    content = (
-      <span>
-        <DrilldownLink
-          ariaLabel={translateWithParameters(
-            'overview.see_more_details_on_x_y',
-            measure.value,
-            localizeMetric(metric)
-          )}
-          branchLike={branchLike}
-          className="overview-measures-value text-light"
-          component={component.key}
-          metric={metric}
-        >
-          {formatMeasure(measure.value, 'SHORT_INT')}
-        </DrilldownLink>
-      </span>
-    );
+    return <span>–</span>;
   }
 
+  const url = getComponentDrilldownUrl({
+    branchLike,
+    componentKey: component.key,
+    metric,
+  });
+
   return (
-    <div className="display-flex-column display-flex-center">
-      {content}
-      <span className="spacer-top">{getLocalizedMetricName({ key: metric })}</span>
-    </div>
+    <span>
+      <DrilldownLink
+        aria-label={translateWithParameters(
+          'overview.see_more_details_on_x_y',
+          measure.value,
+          localizeMetric(metric)
+        )}
+        to={url}
+      >
+        {formatMeasure(measure.value, MetricType.ShortInteger)}
+      </DrilldownLink>
+    </span>
   );
 }
 
index ea5733ae563ef5a03a662768da2c0d9fc24f5a48..1cbfcaafef27d0ecad53443bfc97f73fea483894 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 {
+  Card,
+  CoverageIndicator,
+  DeferredSpinner,
+  DuplicationsIndicator,
+  LightLabel,
+  PageTitle,
+  ToggleButton,
+} from 'design-system';
 import * as React from 'react';
-import { rawSizes } from '../../../app/theme';
-import BoxedTabs, { getTabId, getTabPanelId } from '../../../components/controls/BoxedTabs';
 import ComponentReportActions from '../../../components/controls/ComponentReportActions';
 import { Location, withRouter } from '../../../components/hoc/withRouter';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { translate } from '../../../helpers/l10n';
+import { duplicationRatingConverter } from '../../../components/measure/utils';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { findMeasure, isDiffMetric } from '../../../helpers/measures';
 import { CodeScope } from '../../../helpers/urls';
 import { ApplicationPeriod } from '../../../types/application';
@@ -31,13 +38,13 @@ import { Branch } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
 import { IssueType } from '../../../types/issues';
 import { MetricKey } from '../../../types/metrics';
+import { QualityGateStatus } from '../../../types/quality-gates';
 import { Component, MeasureEnhanced, Period } from '../../../types/types';
-import MeasurementLabel from '../components/MeasurementLabel';
 import { MeasurementType, parseQuery } from '../utils';
-import { DrilldownMeasureValue } from './DrilldownMeasureValue';
 import { LeakPeriodInfo } from './LeakPeriodInfo';
-import MeasuresPanelIssueMeasureRow from './MeasuresPanelIssueMeasureRow';
+import MeasuresPanelIssueMeasure from './MeasuresPanelIssueMeasure';
 import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode';
+import MeasuresPanelPercentMeasure from './MeasuresPanelPercentMeasure';
 
 export interface MeasuresPanelProps {
   appLeak?: ApplicationPeriod;
@@ -47,6 +54,7 @@ export interface MeasuresPanelProps {
   measures?: MeasureEnhanced[];
   period?: Period;
   location: Location;
+  qgStatuses?: QualityGateStatus[];
 }
 
 export enum MeasuresPanelTabs {
@@ -55,13 +63,26 @@ export enum MeasuresPanelTabs {
 }
 
 export function MeasuresPanel(props: MeasuresPanelProps) {
-  const { appLeak, branch, component, loading, measures = [], period, location } = props;
+  const {
+    appLeak,
+    branch,
+    component,
+    loading,
+    measures = [],
+    period,
+    qgStatuses = [],
+    location,
+  } = props;
 
   const hasDiffMeasures = measures.some((m) => isDiffMetric(m.metric.key));
   const isApp = component.qualifier === ComponentQualifier.Application;
   const leakPeriod = isApp ? appLeak : period;
   const query = parseQuery(location.query);
 
+  const { failingConditionsOnNewCode, failingConditionsOnOverallCode } =
+    countFailingConditions(qgStatuses);
+  const failingConditions = failingConditionsOnNewCode + failingConditionsOnOverallCode;
+
   const [tab, selectTab] = React.useState(() => {
     return query.codeScope === CodeScope.Overall
       ? MeasuresPanelTabs.Overall
@@ -82,120 +103,99 @@ export function MeasuresPanel(props: MeasuresPanelProps) {
 
   const tabs = [
     {
-      key: MeasuresPanelTabs.New,
-      label: (
-        <div className="text-left overview-measures-tab">
-          <span className="text-bold">{translate('overview.new_code')}</span>
-          {leakPeriod && <LeakPeriodInfo leakPeriod={leakPeriod} />}
-        </div>
-      ),
+      value: MeasuresPanelTabs.New,
+      label: translate('overview.new_code'),
+      counter: failingConditionsOnNewCode,
     },
     {
-      key: MeasuresPanelTabs.Overall,
-      label: (
-        <div className="text-left overview-measures-tab">
-          <span className="text-bold" style={{ position: 'absolute', top: 2 * rawSizes.grid }}>
-            {translate('overview.overall_code')}
-          </span>
-        </div>
-      ),
+      value: MeasuresPanelTabs.Overall,
+      label: translate('overview.overall_code'),
+      counter: failingConditionsOnOverallCode,
     },
   ];
 
   return (
-    <div className="overview-panel" data-test="overview__measures-panel">
-      <div className="display-flex-space-between display-flex-start">
-        <h2 className="overview-panel-title">{translate('overview.measures')}</h2>
+    <div data-test="overview__measures-panel">
+      <div className="sw-float-right -sw-mt-6">
         <ComponentReportActions component={component} branch={branch} />
       </div>
+      <h2 className="sw-flex sw-mb-4">
+        <PageTitle text={translate('overview.measures')} />
+      </h2>
 
       {loading ? (
-        <div className="overview-panel-content overview-panel-big-padded">
+        <div>
           <DeferredSpinner loading={loading} />
         </div>
       ) : (
         <>
-          <BoxedTabs onSelect={(key) => selectTab(key)} selected={tab} tabs={tabs} />
-
-          <div
-            className="overview-panel-content flex-1 bordered"
-            role="tabpanel"
-            id={getTabPanelId(tab)}
-            aria-labelledby={getTabId(tab)}
-          >
-            {!hasDiffMeasures && isNewCodeTab ? (
-              <MeasuresPanelNoNewCode branch={branch} component={component} period={period} />
-            ) : (
-              <>
-                {[
-                  IssueType.Bug,
-                  IssueType.Vulnerability,
-                  IssueType.SecurityHotspot,
-                  IssueType.CodeSmell,
-                ].map((type: IssueType) => (
-                  <MeasuresPanelIssueMeasureRow
+          <div className="sw-flex sw-items-center">
+            <ToggleButton onChange={(key) => selectTab(key)} options={tabs} value={tab} />
+            {failingConditions > 0 && (
+              <LightLabel className="sw-body-sm-highlight sw-ml-8">
+                {translateWithParameters('overview.X_conditions_failed', failingConditions)}
+              </LightLabel>
+            )}
+          </div>
+
+          {tab === MeasuresPanelTabs.New && leakPeriod ? (
+            <LightLabel className="sw-body-sm sw-flex sw-items-center sw-mt-4">
+              <span className="sw-mr-1">{translate('overview.new_code')}:</span>
+              <LeakPeriodInfo leakPeriod={leakPeriod} />
+            </LightLabel>
+          ) : (
+            <div className="sw-h-4 sw-pt-1 sw-mt-4" />
+          )}
+
+          {!hasDiffMeasures && isNewCodeTab ? (
+            <MeasuresPanelNoNewCode branch={branch} component={component} period={period} />
+          ) : (
+            <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={branch}
                     component={component}
                     isNewCodeTab={isNewCodeTab}
-                    key={type}
                     measures={measures}
                     type={type}
                   />
-                ))}
-
-                <div className="display-flex-row overview-measures-row">
-                  {(findMeasure(measures, MetricKey.coverage) ||
-                    findMeasure(measures, MetricKey.new_coverage)) && (
-                    <div
-                      className="overview-panel-huge-padded flex-1 bordered-right display-flex-center"
-                      data-test="overview__measures-coverage"
-                    >
-                      <MeasurementLabel
-                        branchLike={branch}
-                        centered={isNewCodeTab}
-                        component={component}
-                        measures={measures}
-                        type={MeasurementType.Coverage}
-                        useDiffMetric={isNewCodeTab}
-                      />
-
-                      {tab === MeasuresPanelTabs.Overall && (
-                        <div className="huge-spacer-left">
-                          <DrilldownMeasureValue
-                            branchLike={branch}
-                            component={component}
-                            measures={measures}
-                            metric={MetricKey.tests}
-                          />
-                        </div>
-                      )}
-                    </div>
-                  )}
-                  <div className="overview-panel-huge-padded flex-1 display-flex-center">
-                    <MeasurementLabel
-                      branchLike={branch}
-                      centered={isNewCodeTab}
-                      component={component}
-                      measures={measures}
-                      type={MeasurementType.Duplication}
-                      useDiffMetric={isNewCodeTab}
-                    />
-
-                    {tab === MeasuresPanelTabs.Overall && (
-                      <div className="huge-spacer-left">
-                        <DrilldownMeasureValue
-                          branchLike={branch}
-                          component={component}
-                          measures={measures}
-                          metric={MetricKey.duplicated_blocks}
-                        />
-                      </div>
-                    )}
-                  </div>
-                </div>
-              </>
-            )}
-          </div>
+                </Card>
+              ))}
+
+              {(findMeasure(measures, MetricKey.coverage) ||
+                findMeasure(measures, MetricKey.new_coverage)) && (
+                <Card className="sw-p-8" data-test="overview__measures-coverage">
+                  <MeasuresPanelPercentMeasure
+                    branchLike={branch}
+                    component={component}
+                    measures={measures}
+                    ratingIcon={renderCoverageIcon}
+                    secondaryMetricKey={MetricKey.tests}
+                    type={MeasurementType.Coverage}
+                    useDiffMetric={isNewCodeTab}
+                  />
+                </Card>
+              )}
+
+              <Card className="sw-p-8">
+                <MeasuresPanelPercentMeasure
+                  branchLike={branch}
+                  component={component}
+                  measures={measures}
+                  ratingIcon={renderDuplicationIcon}
+                  secondaryMetricKey={MetricKey.duplicated_blocks}
+                  type={MeasurementType.Duplication}
+                  useDiffMetric={isNewCodeTab}
+                />
+              </Card>
+            </div>
+          )}
         </>
       )}
     </div>
@@ -203,3 +203,30 @@ export function MeasuresPanel(props: MeasuresPanelProps) {
 }
 
 export default withRouter(React.memo(MeasuresPanel));
+
+function renderCoverageIcon(value?: string) {
+  return <CoverageIndicator value={value} size="md" />;
+}
+
+function renderDuplicationIcon(value?: string) {
+  const rating = value !== undefined ? duplicationRatingConverter(Number(value)) : undefined;
+
+  return <DuplicationsIndicator rating={rating} size="md" />;
+}
+
+function countFailingConditions(qgStatuses: QualityGateStatus[]) {
+  let failingConditionsOnNewCode = 0;
+  let failingConditionsOnOverallCode = 0;
+
+  qgStatuses.forEach(({ failedConditions }) => {
+    failedConditions.forEach((condition) => {
+      if (isDiffMetric(condition.metric)) {
+        failingConditionsOnNewCode += 1;
+      } else {
+        failingConditionsOnOverallCode += 1;
+      }
+    });
+  });
+
+  return { failingConditionsOnNewCode, failingConditionsOnOverallCode };
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelCard.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelCard.tsx
new file mode 100644 (file)
index 0000000..ab42b7a
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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';
+
+interface Props {
+  category: React.ReactElement;
+  rating: React.ReactElement | null;
+}
+
+export default function MeasuresPanelCard(
+  props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>
+) {
+  const { category, children, rating, ...attributes } = props;
+
+  return (
+    <div className="sw-flex sw-justify-between sw-items-center" {...attributes}>
+      <div className="sw-flex sw-flex-col sw-justify-between">
+        <div className="sw-body-sm-highlight sw-flex sw-items-center">{category}</div>
+
+        <div className="sw-mt-3">{children}</div>
+      </div>
+
+      <div>{rating}</div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasure.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasure.tsx
new file mode 100644 (file)
index 0000000..0a2d6e1
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * 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 { LightPrimary, ThemeColors } from 'design-system';
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+import { BranchLike } from '../../../types/branch-like';
+import { ComponentQualifier } from '../../../types/component';
+import { IssueType } from '../../../types/issues';
+import { Component, MeasureEnhanced } from '../../../types/types';
+import IssueLabel from '../components/IssueLabel';
+import IssueRating from '../components/IssueRating';
+import { getIssueIconClass, getIssueRatingName } from '../utils';
+import MeasuresPanelCard from './MeasuresPanelCard';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: Component;
+  isNewCodeTab: boolean;
+  measures: MeasureEnhanced[];
+  type: IssueType;
+}
+
+export default function MeasuresPanelIssueMeasure(props: Props) {
+  const { branchLike, component, isNewCodeTab, measures, type } = props;
+
+  const isApp = component.qualifier === ComponentQualifier.Application;
+
+  const IconClass = getIssueIconClass(type) as (args: {
+    className?: string;
+    fill?: ThemeColors;
+  }) => JSX.Element;
+
+  return (
+    <MeasuresPanelCard
+      data-test={`overview__measures-${type.toString().toLowerCase()}`}
+      category={
+        <div className="sw-flex sw-items-center">
+          <IconClass className="sw-mr-1" fill="discreetInteractiveIcon" />
+          <LightPrimary>{getIssueRatingName(type)}</LightPrimary>
+        </div>
+      }
+      rating={
+        !isApp || !isNewCodeTab ? (
+          <IssueRating
+            branchLike={branchLike}
+            component={component}
+            measures={measures}
+            type={type}
+            useDiffMetric={isNewCodeTab}
+          />
+        ) : null
+      }
+    >
+      <IssueLabel
+        branchLike={branchLike}
+        component={component}
+        helpTooltip={
+          type === IssueType.SecurityHotspot
+            ? translate('metric.security_hotspots.full_description')
+            : undefined
+        }
+        measures={measures}
+        type={type}
+        useDiffMetric={isNewCodeTab}
+      />
+    </MeasuresPanelCard>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasureRow.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasureRow.tsx
deleted file mode 100644 (file)
index 4f6da6d..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * 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 { translate } from '../../../helpers/l10n';
-import { BranchLike } from '../../../types/branch-like';
-import { ComponentQualifier } from '../../../types/component';
-import { IssueType } from '../../../types/issues';
-import { Component, MeasureEnhanced } from '../../../types/types';
-import IssueLabel from '../components/IssueLabel';
-import IssueRating from '../components/IssueRating';
-import DebtValue from './DebtValue';
-import SecurityHotspotsReviewed from './SecurityHotspotsReviewed';
-
-export interface MeasuresPanelIssueMeasureRowProps {
-  branchLike?: BranchLike;
-  component: Component;
-  isNewCodeTab: boolean;
-  measures: MeasureEnhanced[];
-  type: IssueType;
-}
-
-export default function MeasuresPanelIssueMeasureRow(props: MeasuresPanelIssueMeasureRowProps) {
-  const { branchLike, component, isNewCodeTab, measures, type } = props;
-
-  const isApp = component.qualifier === ComponentQualifier.Application;
-
-  return (
-    <div
-      className="display-flex-row overview-measures-row"
-      data-test={`overview__measures-${type.toString().toLowerCase()}`}
-    >
-      {type === IssueType.CodeSmell ? (
-        <>
-          <div className="overview-panel-big-padded flex-1 small display-flex-center big-spacer-left">
-            <DebtValue
-              branchLike={branchLike}
-              component={component}
-              measures={measures}
-              useDiffMetric={isNewCodeTab}
-            />
-          </div>
-          <div className="flex-1 small display-flex-center">
-            <IssueLabel
-              branchLike={branchLike}
-              component={component}
-              measures={measures}
-              type={type}
-              useDiffMetric={isNewCodeTab}
-            />
-          </div>
-        </>
-      ) : (
-        <div className="overview-panel-big-padded flex-1 small display-flex-center big-spacer-left">
-          <IssueLabel
-            branchLike={branchLike}
-            component={component}
-            helpTooltip={
-              type === IssueType.SecurityHotspot
-                ? translate('metric.security_hotspots.full_description')
-                : undefined
-            }
-            measures={measures}
-            type={type}
-            useDiffMetric={isNewCodeTab}
-          />
-        </div>
-      )}
-      {type === IssueType.SecurityHotspot && (
-        <div className="flex-1 small display-flex-center">
-          <SecurityHotspotsReviewed measures={measures} useDiffMetric={isNewCodeTab} />
-        </div>
-      )}
-      {(!isApp || !isNewCodeTab) && (
-        <div className="overview-panel-big-padded overview-measures-aside display-flex-center">
-          <IssueRating
-            branchLike={branchLike}
-            component={component}
-            measures={measures}
-            type={type}
-            useDiffMetric={isNewCodeTab}
-          />
-        </div>
-      )}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasure.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasure.tsx
new file mode 100644 (file)
index 0000000..cf7c7a1
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * 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 { DrilldownLink, GreySeparator, LightLabel, LightPrimary } from 'design-system';
+import * as React from 'react';
+import { getLeakValue } from '../../../components/measure/utils';
+import { isPullRequest } from '../../../helpers/branch-like';
+import { getLocalizedMetricName, 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 { Component, MeasureEnhanced } from '../../../types/types';
+import AfterMergeEstimate from '../pullRequests/AfterMergeEstimate';
+import { MeasurementType, getMeasurementMetricKey } from '../utils';
+import DrilldownMeasureValue from './DrilldownMeasureValue';
+import MeasuresPanelCard from './MeasuresPanelCard';
+import MeasuresPanelPercentMeasureLabel from './MeasuresPanelPercentMeasureLabel';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: Component;
+  useDiffMetric: boolean;
+  measures: MeasureEnhanced[];
+  ratingIcon: (value: string | undefined) => React.ReactElement;
+  secondaryMetricKey?: MetricKey;
+  type: MeasurementType;
+}
+
+export default function MeasuresPanelPercentMeasure(props: Props) {
+  const {
+    branchLike,
+    component,
+    measures,
+    ratingIcon,
+    secondaryMetricKey,
+    type,
+    useDiffMetric = false,
+  } = props;
+  const metricKey = getMeasurementMetricKey(type, useDiffMetric);
+  const measure = findMeasure(measures, metricKey);
+
+  let value;
+  if (measure) {
+    value = useDiffMetric ? getLeakValue(measure) : measure.value;
+  }
+
+  const url = getComponentDrilldownUrl({
+    componentKey: component.key,
+    metric: metricKey,
+    branchLike,
+    listView: true,
+  });
+
+  const formattedValue = formatMeasure(value, MetricType.Percent, {
+    decimals: 2,
+    omitExtraDecimalZeros: true,
+  });
+
+  return (
+    <MeasuresPanelCard
+      data-test={`overview__measures-${type.toString().toLowerCase()}`}
+      category={<LightPrimary>{translate('overview.measurement_type', type)}</LightPrimary>}
+      rating={ratingIcon(value)}
+    >
+      <>
+        <div className="sw-body-md sw-flex sw-items-center sw-mb-3">
+          {value === undefined ? (
+            <LightLabel aria-label={translate('no_data')}> — </LightLabel>
+          ) : (
+            <DrilldownLink
+              aria-label={translateWithParameters(
+                'overview.see_more_details_on_x_of_y',
+                value,
+                localizeMetric(metricKey)
+              )}
+              to={url}
+            >
+              {formattedValue}
+            </DrilldownLink>
+          )}
+
+          <LightLabel className="sw-ml-2">
+            {translate('overview.measurement_type', type)}
+          </LightLabel>
+        </div>
+        <MeasuresPanelPercentMeasureLabel
+          component={component}
+          measures={measures}
+          type={type}
+          useDiffMetric={useDiffMetric}
+          branchLike={branchLike}
+        />
+
+        {!useDiffMetric && secondaryMetricKey && (
+          <>
+            <GreySeparator className="sw-mt-4" />
+            <div className="sw-body-md sw-flex sw-items-center sw-mt-4">
+              <DrilldownMeasureValue
+                branchLike={branchLike}
+                component={component}
+                measures={measures}
+                metric={secondaryMetricKey}
+              />
+              <LightLabel className="sw-ml-2">
+                {getLocalizedMetricName({ key: secondaryMetricKey })}
+              </LightLabel>
+            </div>
+          </>
+        )}
+
+        {isPullRequest(branchLike) && (
+          <>
+            <GreySeparator className="sw-mt-4" />
+            <div className="sw-body-md sw-flex sw-items-center sw-mt-4">
+              <AfterMergeEstimate measures={measures} type={type} />
+              <LightLabel className="sw-ml-2">
+                {translate('component_measures.facet_category.overall_category.estimated')}
+              </LightLabel>
+            </div>
+          </>
+        )}
+      </>
+    </MeasuresPanelCard>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasureLabel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasureLabel.tsx
new file mode 100644 (file)
index 0000000..e65e832
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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 { DrilldownLink, LightLabel } from 'design-system';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { getLeakValue } from '../../../components/measure/utils';
+import { translate } from '../../../helpers/l10n';
+import { findMeasure, formatMeasure } from '../../../helpers/measures';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricType } from '../../../types/metrics';
+import { Component, MeasureEnhanced } from '../../../types/types';
+import { MeasurementType, getMeasurementLabelKeys, getMeasurementLinesMetricKey } from '../utils';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: Component;
+  useDiffMetric: boolean;
+  measures: MeasureEnhanced[];
+  type: MeasurementType;
+}
+
+export default function MeasuresPanelPercentMeasureLabel(props: Props) {
+  const { branchLike, component, measures, type, useDiffMetric = false } = props;
+  const { expandedLabelKey, labelKey } = getMeasurementLabelKeys(type, useDiffMetric);
+  const linesMetric = getMeasurementLinesMetricKey(type, useDiffMetric);
+  const measure = findMeasure(measures, linesMetric);
+
+  if (!measure) {
+    return <LightLabel>{translate(labelKey)}</LightLabel>;
+  }
+
+  const value = useDiffMetric ? getLeakValue(measure) : measure.value;
+
+  const url = getComponentDrilldownUrl({
+    componentKey: component.key,
+    metric: linesMetric,
+    branchLike,
+    listView: true,
+  });
+
+  return (
+    <LightLabel>
+      <FormattedMessage
+        defaultMessage={translate(expandedLabelKey)}
+        id={expandedLabelKey}
+        values={{
+          count: (
+            <DrilldownLink className="sw-body-md-highlight" to={url}>
+              {formatMeasure(value, MetricType.ShortInteger)}
+            </DrilldownLink>
+          ),
+        }}
+      />
+    </LightLabel>
+  );
+}
index ec76ddfafde26cda82bd1257b89a7c4dbd6c4255..4a9025ba54c0cf70ce919eaef32b20796092a529 100644 (file)
@@ -52,7 +52,7 @@ export function ProjectLeakPeriodInfo(props: ProjectLeakPeriodInfoProps) {
     leakPeriod.mode === NewCodePeriodSettingType.NUMBER_OF_DAYS ||
     leakPeriod.mode === NewCodePeriodSettingType.REFERENCE_BRANCH
   ) {
-    return <div className="note spacer-top">{leakPeriodLabel} </div>;
+    return <div>{leakPeriodLabel} </div>;
   }
 
   const leakPeriodDate = getPeriodDate(leakPeriod);
@@ -63,20 +63,19 @@ export function ProjectLeakPeriodInfo(props: ProjectLeakPeriodInfoProps) {
 
   return (
     <>
-      <div className="note spacer-top text-ellipsis" title={leakPeriodLabel}>
+      <div className="sw-mr-2 sw-text-ellipsis" title={leakPeriodLabel}>
         {leakPeriodLabel}
       </div>
+
       <DateFromNow date={leakPeriodDate}>
-        {(fromNow) => (
-          <div className="note little-spacer-top">
-            {translateWithParameters(
-              leakPeriod.mode === 'previous_analysis'
-                ? 'overview.previous_analysis_x'
-                : 'overview.started_x',
-              fromNow
-            )}
-          </div>
-        )}
+        {(fromNow) =>
+          translateWithParameters(
+            leakPeriod.mode === 'previous_analysis'
+              ? 'overview.previous_analysis_x'
+              : 'overview.started_x',
+            fromNow
+          )
+        }
       </DateFromNow>
     </>
   );
index 1aae810e9a1bd7a92a502e6439f80a9797c5b525..93f8b78d68731e1d55da549f8c498ef0fa71f00d 100644 (file)
@@ -236,7 +236,7 @@ describe('project overview', () => {
     renderBranchOverview();
 
     expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument();
-    expect(screen.getByText('overview.X_conditions_failed.2')).toBeInTheDocument();
+    expect(screen.getAllByText('overview.X_conditions_failed.2')).toHaveLength(2);
   });
 
   it('should correctly show a project as empty', async () => {
index 90a3c876f1d5cd9750662ae3f7aaebc1816083bb..511cf069cb275073e46d77afce6bc7e8e861aa7e 100644 (file)
@@ -17,8 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { DrilldownLink, HelperHintIcon, LightLabel } from 'design-system';
 import * as React from 'react';
-import Link from '../../../components/common/Link';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
 import { getLeakValue } from '../../../components/measure/utils';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
@@ -27,8 +27,9 @@ import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/mea
 import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl } from '../../../helpers/urls';
 import { BranchLike } from '../../../types/branch-like';
 import { IssueType } from '../../../types/issues';
+import { MetricType } from '../../../types/metrics';
 import { Component, MeasureEnhanced } from '../../../types/types';
-import { getIssueIconClass, getIssueMetricKey } from '../utils';
+import { getIssueMetricKey } from '../utils';
 
 export interface IssueLabelProps {
   branchLike?: BranchLike;
@@ -43,7 +44,6 @@ export function IssueLabel(props: IssueLabelProps) {
   const { branchLike, component, helpTooltip, measures, type, useDiffMetric = false } = props;
   const metricKey = getIssueMetricKey(type, useDiffMetric);
   const measure = findMeasure(measures, metricKey);
-  const iconClass = getIssueIconClass(type);
 
   let value;
   if (measure) {
@@ -63,26 +63,29 @@ export function IssueLabel(props: IssueLabelProps) {
       : getComponentIssuesUrl(component.key, params);
 
   return (
-    <>
+    <div className="sw-body-md sw-flex sw-items-center">
       {value === undefined ? (
-        <span aria-label={translate('no_data')} className="overview-measures-empty-value" />
+        <LightLabel aria-label={translate('no_data')}> — </LightLabel>
       ) : (
-        <Link
+        <DrilldownLink
           aria-label={translateWithParameters(
             'overview.see_list_of_x_y_issues',
             value,
             localizeMetric(metricKey)
           )}
-          className="overview-measures-value text-light"
+          className="it__overview-measures-value"
           to={url}
         >
-          {formatMeasure(value, 'SHORT_INT')}
-        </Link>
+          {formatMeasure(value, MetricType.ShortInteger)}
+        </DrilldownLink>
       )}
-      {React.createElement(iconClass, { className: 'big-spacer-left little-spacer-right' })}
-      {localizeMetric(metricKey)}
-      {helpTooltip && <HelpTooltip className="little-spacer-left" overlay={helpTooltip} />}
-    </>
+      <LightLabel className="sw-mx-2">{localizeMetric(metricKey)}</LightLabel>
+      {helpTooltip && (
+        <HelpTooltip overlay={helpTooltip}>
+          <HelperHintIcon aria-label={helpTooltip} />
+        </HelpTooltip>
+      )}
+    </div>
   );
 }
 
index c1dbf140e593b7c9afd05c24257ae38ba911d85b..971237adb0241d6b464746a2146b7dd547631ef5 100644 (file)
  */
 /* eslint-disable react/no-unused-prop-types */
 
+import { DiscreetLinkBox, MetricsRatingBadge } from 'design-system';
 import * as React from 'react';
 import Tooltip from '../../../components/controls/Tooltip';
 import RatingTooltipContent from '../../../components/measure/RatingTooltipContent';
 import { getLeakValue } from '../../../components/measure/utils';
-import DrilldownLink from '../../../components/shared/DrilldownLink';
-import Rating from '../../../components/ui/Rating';
-import { findMeasure } from '../../../helpers/measures';
+import { translateWithParameters } from '../../../helpers/l10n';
+import { findMeasure, formatRating } from '../../../helpers/measures';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
 import { BranchLike } from '../../../types/branch-like';
 import { IssueType } from '../../../types/issues';
 import { Component, MeasureEnhanced } from '../../../types/types';
-import { getIssueRatingMetricKey, getIssueRatingName } from '../utils';
+import { getIssueRatingMetricKey } from '../utils';
 
 export interface IssueRatingProps {
   branchLike?: BranchLike;
@@ -39,46 +40,45 @@ export interface IssueRatingProps {
   useDiffMetric?: boolean;
 }
 
-function renderRatingLink(props: IssueRatingProps) {
+export function IssueRating(props: IssueRatingProps) {
   const { branchLike, component, useDiffMetric = false, measures, type } = props;
-  const rating = getIssueRatingMetricKey(type, useDiffMetric);
-  const measure = findMeasure(measures, rating);
+  const ratingKey = getIssueRatingMetricKey(type, useDiffMetric);
+  const measure = findMeasure(measures, ratingKey);
+  const rawValue = measure && (useDiffMetric ? getLeakValue(measure) : measure.value);
+  const value = formatRating(rawValue);
 
-  if (!rating || !measure) {
-    return (
-      <div className="padded">
-        <Rating value={undefined} />
-      </div>
-    );
+  if (!ratingKey || !measure) {
+    return <NoRating />;
   }
 
-  const value = measure && (useDiffMetric ? getLeakValue(measure) : measure.value);
-
   return (
-    <Tooltip overlay={value && <RatingTooltipContent metricKey={rating} value={value} />}>
+    <Tooltip overlay={rawValue && <RatingTooltipContent metricKey={ratingKey} value={rawValue} />}>
       <span>
-        <DrilldownLink
-          branchLike={branchLike}
-          className="link-no-underline link-rating"
-          component={component.key}
-          metric={rating}
-        >
-          <Rating value={value} />
-        </DrilldownLink>
+        {value ? (
+          <DiscreetLinkBox
+            to={getComponentDrilldownUrl({
+              branchLike,
+              componentKey: component.key,
+              metric: ratingKey,
+              listView: true,
+            })}
+          >
+            <MetricsRatingBadge
+              label={translateWithParameters('metric.has_rating_X', value)}
+              rating={value}
+              size="md"
+            />
+          </DiscreetLinkBox>
+        ) : (
+          <NoRating />
+        )}
       </span>
     </Tooltip>
   );
 }
 
-export function IssueRating(props: IssueRatingProps) {
-  const { type } = props;
+export default IssueRating;
 
-  return (
-    <>
-      <span className="flex-1 big-spacer-right text-right">{getIssueRatingName(type)}</span>
-      {renderRatingLink(props)}
-    </>
-  );
+function NoRating() {
+  return <div className="sw-w-8 sw-h-8 sw-flex sw-justify-center sw-items-center">–</div>;
 }
-
-export default React.memo(IssueRating);
index 55010a5a83b65cc56f849a22e2b47778afa5fb30..c5078fe5e4d483af30503fd919dbd73f06fca0a0 100644 (file)
@@ -26,13 +26,15 @@ import { translate } from '../../../helpers/l10n';
 export function QualityGateStatusTitle() {
   return (
     <div className="sw-flex sw-items-center sw-mb-4">
-      <PageTitle text={translate('overview.quality_gate.status')} />
-      <HelpTooltip
-        className="sw-ml-2"
-        overlay={<div className="sw-my-4">{translate('overview.quality_gate.help')}</div>}
-      >
-        <HelperHintIcon aria-label="help-tooltip" />
-      </HelpTooltip>
+      <h2 className="sw-flex sw-items-center">
+        <PageTitle text={translate('overview.quality_gate.status')} />
+        <HelpTooltip
+          className="sw-ml-2"
+          overlay={<div className="sw-my-4">{translate('overview.quality_gate.help')}</div>}
+        >
+          <HelperHintIcon aria-label="help-tooltip" />
+        </HelpTooltip>
+      </h2>
     </div>
   );
 }
index 8b349a7ae4d3601486e31597d8271a16631a3c2d..6d2de23747f56b2a7c2bf3f0428b9d28fd708eab 100644 (file)
@@ -22,7 +22,7 @@ import * as React from 'react';
 import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks';
-import { findTooltipWithContent, renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
 import { IssueType } from '../../../../types/issues';
 import { MetricKey } from '../../../../types/metrics';
 import { IssueLabel, IssueLabelProps } from '../IssueLabel';
@@ -71,7 +71,7 @@ it('should render correctly for hotspots with tooltip', async () => {
     })
   ).toBeInTheDocument();
 
-  expect(findTooltipWithContent('tooltip text')).toBeInTheDocument();
+  expect(screen.getByText('tooltip text')).toBeInTheDocument();
 });
 
 function renderIssueLabel(props: Partial<IssueLabelProps> = {}) {
index 31a12a7fcfc6bd60871d13be32162f14fde1059a..c10092ce098ef6f085ca6d9a8db9c9936f6351d2 100644 (file)
@@ -28,21 +28,16 @@ import { MetricKey } from '../../../../types/metrics';
 import { IssueRating, IssueRatingProps } from '../IssueRating';
 
 it('should render correctly for vulnerabilities', async () => {
-  renderIssueRating({ type: IssueType.Vulnerability });
-  expect(await screen.findByText('metric_domain.Security')).toBeInTheDocument();
-
   renderIssueRating({ type: IssueType.Vulnerability, useDiffMetric: true });
-  const labels = await screen.findAllByText('metric_domain.Security');
-  expect(labels).toHaveLength(2);
-  const tooltips = await screen.findAllByText('metric.security_rating.tooltip.A');
-  expect(tooltips).toHaveLength(2);
+  expect(await screen.findByLabelText('metric.has_rating_X.A')).toBeInTheDocument();
+  expect(await screen.findByText('metric.security_rating.tooltip.A')).toBeInTheDocument();
 });
 
 it('should render correctly if no values are present', async () => {
   renderIssueRating({
     measures: [mockMeasureEnhanced({ metric: mockMetric({ key: 'NONE' }) })],
   });
-  expect(await screen.findByText('metric_domain.Reliability')).toBeInTheDocument();
+  expect(await screen.findByText('')).toBeInTheDocument();
 });
 
 function renderIssueRating(props: Partial<IssueRatingProps> = {}) {
index c2051bc1b80132609427d91a9b1ec01586b9748b..fa91067a93d3b742b0a4b3e780e084155313dfdd 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import classNames from 'classnames';
+import { LightPrimary } from 'design-system';
 import * as React from 'react';
-import { translate } from '../../../helpers/l10n';
 import { findMeasure, formatMeasure } from '../../../helpers/measures';
+import { MetricType } from '../../../types/metrics';
 import { MeasureEnhanced } from '../../../types/types';
-import { getMeasurementAfterMergeMetricKey, MeasurementType } from '../utils';
+import { MeasurementType, getMeasurementAfterMergeMetricKey } from '../utils';
 
 export interface AfterMergeEstimateProps {
   className?: string;
@@ -40,11 +41,10 @@ export function AfterMergeEstimate({ className, measures, type }: AfterMergeEsti
   }
 
   return (
-    <div className={classNames(className, 'display-flex-center')}>
-      <span className="huge">{formatMeasure(measure.value, 'PERCENT')}</span>
-      <span className="label flex-1 spacer-left text-right">
-        {translate('component_measures.facet_category.overall_category.estimated')}
-      </span>
+    <div className={classNames(className, 'sw-flex sw-items-center')}>
+      <LightPrimary className="sw-heading-lg">
+        {formatMeasure(measure.value, MetricType.Percent)}
+      </LightPrimary>
     </div>
   );
 }
index 03717947f1d48b0d39314533352dafd34c82d357..2a2ce4b34d0164d93022a56ee5f6b8186971af00 100644 (file)
 import {
   BasicSeparator,
   Card,
+  CoverageIndicator,
   DeferredSpinner,
+  DuplicationsIndicator,
   HelperHintIcon,
   LargeCenteredLayout,
   Link,
+  PageTitle,
   TextMuted,
 } from 'design-system';
 import { differenceBy, uniq } from 'lodash';
@@ -34,6 +37,7 @@ import { BranchStatusContextInterface } from '../../../app/components/branch-sta
 import withBranchStatus from '../../../app/components/branch-status/withBranchStatus';
 import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
 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';
@@ -42,10 +46,9 @@ import { getQualityGateUrl, getQualityGatesUrl } from '../../../helpers/urls';
 import { BranchStatusData, 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 IgnoredConditionWarning from '../components/IgnoredConditionWarning';
-import IssueLabel from '../components/IssueLabel';
-import IssueRating from '../components/IssueRating';
-import MeasurementLabel from '../components/MeasurementLabel';
 import QualityGateConditions from '../components/QualityGateConditions';
 import QualityGateStatusHeader from '../components/QualityGateStatusHeader';
 import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView';
@@ -53,7 +56,6 @@ import { QualityGateStatusTitle } from '../components/QualityGateStatusTitle';
 import SonarLintPromotion from '../components/SonarLintPromotion';
 import '../styles.css';
 import { MeasurementType, PR_METRICS } from '../utils';
-import AfterMergeEstimate from './AfterMergeEstimate';
 
 interface Props extends BranchStatusData, Pick<BranchStatusContextInterface, 'fetchBranchStatus'> {
   branchLike: PullRequest;
@@ -228,59 +230,41 @@ export class PullRequestOverview extends React.PureComponent<Props, State> {
               <SonarLintPromotion qgConditions={conditions} />
             </div>
 
-            <div className="flex-1">
-              <h2 className="overview-panel-title spacer-bottom small">
-                {translate('overview.measures')}
+            <div className="sw-flex-1">
+              <h2 className="sw-body-md-highlight">
+                <PageTitle text={translate('overview.measures')} />
               </h2>
 
-              <div className="overview-panel-content">
+              <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
                 {[
                   IssueType.Bug,
                   IssueType.Vulnerability,
                   IssueType.SecurityHotspot,
                   IssueType.CodeSmell,
                 ].map((type: IssueType) => (
-                  <div className="overview-measures-row display-flex-row" key={type}>
-                    <div className="overview-panel-big-padded flex-1 small display-flex-center">
-                      <IssueLabel
-                        branchLike={branchLike}
-                        component={component}
-                        measures={measures}
-                        type={type}
-                        useDiffMetric={true}
-                      />
-                    </div>
-                    <div className="overview-panel-big-padded overview-measures-aside display-flex-center">
-                      <IssueRating
-                        branchLike={branchLike}
-                        component={component}
-                        measures={measures}
-                        type={type}
-                        useDiffMetric={true}
-                      />
-                    </div>
-                  </div>
+                  <Card key={type} className="sw-p-8">
+                    <MeasuresPanelIssueMeasure
+                      branchLike={branchLike}
+                      component={component}
+                      isNewCodeTab={true}
+                      measures={measures}
+                      type={type}
+                    />
+                  </Card>
                 ))}
 
                 {[MeasurementType.Coverage, MeasurementType.Duplication].map(
                   (type: MeasurementType) => (
-                    <div className="overview-measures-row display-flex-row" key={type}>
-                      <div className="overview-panel-big-padded flex-1 small display-flex-center">
-                        <MeasurementLabel
-                          branchLike={branchLike}
-                          component={component}
-                          measures={measures}
-                          type={type}
-                          useDiffMetric={true}
-                        />
-                      </div>
-
-                      <AfterMergeEstimate
-                        className="overview-panel-big-padded overview-measures-aside text-right overview-measures-emphasis"
+                    <Card key={type} className="sw-p-8">
+                      <MeasuresPanelPercentMeasure
+                        branchLike={branchLike}
+                        component={component}
                         measures={measures}
+                        ratingIcon={renderMeasureIcon(type)}
                         type={type}
+                        useDiffMetric={true}
                       />
-                    </div>
+                    </Card>
                   )
                 )}
               </div>
@@ -293,3 +277,17 @@ export class PullRequestOverview extends React.PureComponent<Props, State> {
 }
 
 export default withBranchStatus(withBranchStatusActions(PullRequestOverview));
+
+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 ca963e3ebd1e21851c9fdb32e8cfcb0636c1431f..2e970d123dad5aec9678261c67566c20d726a938 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 { BugIcon, CodeSmellIcon, SecurityHotspotIcon, VulnerabilityIcon } from 'design-system';
 import { flatten, sortBy } from 'lodash';
-import BugIcon from '../components/icons/BugIcon';
-import CodeSmellIcon from '../components/icons/CodeSmellIcon';
-import SecurityHotspotIcon from '../components/icons/SecurityHotspotIcon';
-import VulnerabilityIcon from '../components/icons/VulnerabilityIcon';
 import { IssueType, RawIssue } from '../types/issues';
 import { MetricKey } from '../types/metrics';
 import { Dict, Flow, FlowLocation, Issue, TextRange } from '../types/types';
index 87fdab13db1adda392e4ed026fa03d06a78b5fc1..63ed58f4863ff7b65a95ecd4868a103967946584 100644 (file)
@@ -78,7 +78,10 @@ interface Formatter {
   (value: string | number, options?: any): string;
 }
 
-/** Format a measure value for a given type */
+/**
+ * Format a measure value for a given type
+ * ! For Ratings, use formatRating instead
+ */
 export function formatMeasure(
   value: string | number | undefined,
   type: string,
@@ -89,6 +92,21 @@ export function formatMeasure(
   return useFormatter(value, formatter, options);
 }
 
+type RatingValue = 'A' | 'B' | 'C' | 'D' | 'E';
+const RATING_VALUES: RatingValue[] = ['A', 'B', 'C', 'D', 'E'];
+export function formatRating(value: string | number | undefined): RatingValue | undefined {
+  if (!value) {
+    return undefined;
+  }
+
+  if (typeof value === 'string') {
+    value = parseInt(value, 10);
+  }
+
+  // rating is 1-5, adjust for 0-based indexing
+  return RATING_VALUES[value - 1];
+}
+
 /** Return a localized metric name */
 export function localizeMetric(metricKey: string): string {
   return translate('metric', metricKey, 'name');
index 386ab30be8b07c28f09e49154b764b273e5dfb43..f55eae17eaa24c7528665e46d3f928d5718cf523 100644 (file)
@@ -3476,8 +3476,8 @@ overview.gate.view.no_alert=The view has passed the quality gate.
 overview.gate.view.warnings=The view has warnings on the following quality gate conditions: {0}.
 overview.gate.view.errors=The view failed the quality gate on the following conditions: {0}.
 
-overview.domain.duplications=Duplications
-overview.domain.size=Size
+overview.measurement_type.DUPLICATION=Duplications
+overview.measurement_type.COVERAGE=Coverage
 
 overview.complexity_tooltip.function={0} functions have complexity around {1}
 overview.complexity_tooltip.file={0} files have complexity around {1}