]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21455 Update overall tab secondary measures cards
author7PH <benjamin.raymond@sonarsource.com>
Sun, 28 Jan 2024 14:25:53 +0000 (15:25 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 31 Jan 2024 20:03:36 +0000 (20:03 +0000)
24 files changed:
server/sonar-web/design-system/src/components/CoverageIndicator.tsx
server/sonar-web/design-system/src/components/DuplicationsIndicator.tsx
server/sonar-web/design-system/src/components/icons/NoDataIcon.tsx
server/sonar-web/src/main/js/apps/overview/branches/AcceptedIssuesPanel.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/branches/BranchOverallCodePanel.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasure.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentCards.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasure.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasureLabel.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/IssueMeasuresCardInner.tsx
server/sonar-web/src/main/js/apps/overview/components/MeasuresCard.tsx
server/sonar-web/src/main/js/apps/overview/components/MeasuresCardNumber.tsx
server/sonar-web/src/main/js/apps/overview/components/MeasuresCardPercent.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardPanel.tsx
server/sonar-web/src/main/js/apps/overview/utils.tsx
server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts
server/sonar-web/src/main/js/types/quality-gates.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 8f51514408fa65d42440fbec2afacdf95407e91a..9eeb58479b7d98b856163f5373b7d14a1106ee8a 100644 (file)
@@ -38,7 +38,7 @@ export function CoverageIndicator({ size = 'sm', value }: CoverageIndicatorProps
   const thickness = SIZE_TO_THICKNESS_MAPPING[size];
 
   if (value === undefined) {
-    return <NoDataIcon height={width} width={width} />;
+    return <NoDataIcon size={size} />;
   }
 
   const themeRed = themeColor('coverageRed')({ theme });
index 75e5de0e3601569cb9bd3048a0c0e6d3a2a2d6c4..f43cb8a7206cc5fd6466006384cb9e7079d6470b 100644 (file)
@@ -35,7 +35,7 @@ export function DuplicationsIndicator({ size = 'sm', rating }: Props) {
   const sizePX = SIZE_TO_PX_MAPPING[size];
 
   if (rating === undefined) {
-    return <NoDataIcon height={sizePX} width={sizePX} />;
+    return <NoDataIcon size={size} />;
   }
 
   const primaryColor = themeColor(`duplicationsIndicator.${rating}`)({ theme });
index afd93e23d3ca0d1fa028f12ab01581664730e35d..b746b9da8ff2050864084a25e4260f1457545896 100644 (file)
  */
 import { CustomIcon, IconProps } from './Icon';
 
-export function NoDataIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+export interface NoDataIconProps extends IconProps {
+  size?: 'xs' | 'sm' | 'md';
+}
+
+const SIZES: Record<NonNullable<NoDataIconProps['size']>, number> = {
+  xs: 16,
+  sm: 24,
+  md: 36,
+};
+
+export function NoDataIcon({
+  fill = 'currentColor',
+  size = 'md',
+  ...iconProps
+}: Readonly<NoDataIconProps>) {
+  const iconSize = SIZES[size];
+
   return (
-    <CustomIcon {...iconProps}>
+    <CustomIcon height={iconSize} width={iconSize} {...iconProps}>
       <path
         clipRule="evenodd"
         d="M16 8C16 12.4183 12.4183 16 8 16C5.5106 16 3.28676 14.863 1.81951 13.0799L15.4913 5.1865C15.8201 6.06172 16 7.00986 16 8ZM14.5574 3.41624L0.750565 11.3876C0.269025 10.3589 0 9.21089 0 8C0 3.58172 3.58172 0 8 0C10.7132 0 13.1109 1.35064 14.5574 3.41624Z"
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/AcceptedIssuesPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/AcceptedIssuesPanel.tsx
deleted file mode 100644 (file)
index a4dc0e1..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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,
-  HighImpactCircleIcon,
-  LightLabel,
-  NoDataIcon,
-  PageTitle,
-  SnoozeCircleIcon,
-  Spinner,
-  themeColor,
-} from 'design-system';
-import * as React from 'react';
-import { useIntl } from 'react-intl';
-import { getLeakValue } from '../../../components/measure/utils';
-import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
-import { getBranchLikeQuery } from '../../../helpers/branch-like';
-import { findMeasure, formatMeasure } from '../../../helpers/measures';
-import { getComponentIssuesUrl } from '../../../helpers/urls';
-import { Branch } from '../../../types/branch-like';
-import { SoftwareImpactSeverity } from '../../../types/clean-code-taxonomy';
-import { IssueStatus } from '../../../types/issues';
-import { MetricKey, MetricType } from '../../../types/metrics';
-import { Component, MeasureEnhanced } from '../../../types/types';
-import { IssueMeasuresCardInner } from '../components/IssueMeasuresCardInner';
-
-export interface AcceptedIssuesPanelProps {
-  branch?: Branch;
-  component: Component;
-  measures?: MeasureEnhanced[];
-  isNewCode: boolean;
-  loading?: boolean;
-}
-
-function AcceptedIssuesPanel(props: Readonly<AcceptedIssuesPanelProps>) {
-  const { branch, component, measures = [], isNewCode, loading } = props;
-  const intl = useIntl();
-
-  const acceptedIssuesUrl = getComponentIssuesUrl(component.key, {
-    ...getBranchLikeQuery(branch),
-    issueStatuses: IssueStatus.Accepted,
-    ...(isNewCode ? { inNewCodePeriod: 'true' } : {}),
-  });
-
-  const acceptedIssuesWithHighImpactUrl = getComponentIssuesUrl(component.key, {
-    ...getBranchLikeQuery(branch),
-    ...DEFAULT_ISSUES_QUERY,
-    issueStatuses: IssueStatus.Accepted,
-    impactSeverities: SoftwareImpactSeverity.High,
-  });
-
-  const acceptedCount = isNewCode
-    ? getLeakValue(findMeasure(measures, MetricKey.new_accepted_issues))
-    : findMeasure(measures, MetricKey.accepted_issues)?.value;
-
-  const acceptedWithHighImpactCount = isNewCode
-    ? undefined
-    : findMeasure(measures, MetricKey.high_impact_accepted_issues)?.value;
-
-  return (
-    <div className="sw-mt-8">
-      <PageTitle as="h2" text={intl.formatMessage({ id: 'overview.accepted_issues' })} />
-      <LightLabel as="div" className="sw-mt-1 sw-mb-4">
-        {intl.formatMessage({ id: 'overview.accepted_issues.description' })}
-      </LightLabel>
-      <Spinner loading={loading}>
-        <div
-          className={classNames('sw-grid sw-gap-4', {
-            'sw-grid-cols-2': isNewCode,
-            'sw-grid-cols-1': !isNewCode,
-          })}
-        >
-          <Card className="sw-flex sw-gap-4">
-            <IssueMeasuresCardInner
-              disabled={component.needIssueSync}
-              className={classNames({ 'sw-w-1/2': !isNewCode, 'sw-w-full': isNewCode })}
-              metric={MetricKey.accepted_issues}
-              value={formatMeasure(acceptedCount, MetricType.ShortInteger)}
-              header={intl.formatMessage({
-                id: isNewCode ? 'overview.accepted_issues' : 'overview.accepted_issues.total',
-              })}
-              url={acceptedIssuesUrl}
-              icon={
-                <SnoozeCircleIcon
-                  color={
-                    acceptedCount === '0' ? 'overviewCardDefaultIcon' : 'overviewCardWarningIcon'
-                  }
-                  className="sw--translate-y-3"
-                />
-              }
-            />
-            {!isNewCode && (
-              <>
-                <StyledCardSeparator />
-                <IssueMeasuresCardInner
-                  disabled={Boolean(component.needIssueSync) || !acceptedWithHighImpactCount}
-                  className="sw-w-1/2"
-                  metric={MetricKey.high_impact_accepted_issues}
-                  value={formatMeasure(acceptedWithHighImpactCount, MetricType.ShortInteger)}
-                  header={intl.formatMessage({
-                    id: 'overview.high_impact_accepted_issues',
-                  })}
-                  url={acceptedIssuesWithHighImpactUrl}
-                  icon={
-                    acceptedWithHighImpactCount ? (
-                      <HighImpactCircleIcon className="sw--translate-y-3" />
-                    ) : (
-                      <NoDataIcon className="sw--translate-y-3" width={36} height={36} />
-                    )
-                  }
-                />
-              </>
-            )}
-          </Card>
-        </div>
-      </Spinner>
-    </div>
-  );
-}
-
-const StyledCardSeparator = styled.div`
-  width: 1px;
-  background-color: ${themeColor('projectCardBorder')};
-`;
-
-export default React.memo(AcceptedIssuesPanel);
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverallCodePanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverallCodePanel.tsx
deleted file mode 100644 (file)
index 3f9e7d9..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { SoftwareQuality } from '../../../types/clean-code-taxonomy';
-import { MetricKey } from '../../../types/metrics';
-import { Component, MeasureEnhanced } from '../../../types/types';
-import SoftwareImpactMeasureCard from './SoftwareImpactMeasureCard';
-
-export interface BranchOverallCodePanelProps {
-  component: Component;
-  measures: MeasureEnhanced[];
-}
-
-export default function BranchOverallCodePanel(props: Readonly<BranchOverallCodePanelProps>) {
-  const { component, measures } = props;
-
-  return (
-    <div className="sw-flex sw-gap-4 sw-mt-4">
-      <SoftwareImpactMeasureCard
-        component={component}
-        softwareQuality={SoftwareQuality.Security}
-        ratingMetricKey={MetricKey.security_rating}
-        measures={measures}
-      />
-      <SoftwareImpactMeasureCard
-        component={component}
-        softwareQuality={SoftwareQuality.Reliability}
-        ratingMetricKey={MetricKey.reliability_rating}
-        measures={measures}
-      />
-      <SoftwareImpactMeasureCard
-        component={component}
-        softwareQuality={SoftwareQuality.Maintainability}
-        ratingMetricKey={MetricKey.sqale_rating}
-        measures={measures}
-      />
-    </div>
-  );
-}
index eb6b0d9892d9be45e2ee89fc55303861b5d7750f..89f06d9c6ec9491cec4e3e5d2267bf42767441e0 100644 (file)
@@ -54,7 +54,7 @@ import { Analysis, GraphType, MeasureHistory } from '../../../types/project-acti
 import { QualityGateStatus, QualityGateStatusCondition } from '../../../types/quality-gates';
 import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
 import '../styles.css';
-import { BRANCH_OVERVIEW_METRICS, HISTORY_METRICS_LIST } from '../utils';
+import { BRANCH_OVERVIEW_METRICS, HISTORY_METRICS_LIST, Status } from '../utils';
 import BranchOverviewRenderer from './BranchOverviewRenderer';
 
 interface Props {
@@ -187,12 +187,14 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
       (results) => {
         if (this.mounted) {
           const qgStatuses = results
-            .map(({ measures = [], project, projectBranchLike }) => {
+            .map(({ measures = [], project, projectBranchLike }): QualityGateStatus => {
               const { key, name, status, caycStatus } = project;
               const conditions = extractStatusConditionsFromApplicationStatusChildProject(project);
-              const failedConditions = this.getFailedConditions(conditions, measures);
+              const enhancedConditions = this.getEnhancedConditions(conditions, measures);
+              const failedConditions = enhancedConditions.filter((c) => c.level !== Status.OK);
 
               return {
+                conditions: enhancedConditions,
                 failedConditions,
                 caycStatus,
                 key,
@@ -244,11 +246,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
         if (this.mounted && measures) {
           const { ignoredConditions, caycStatus, status } = projectStatus;
           const conditions = extractStatusConditionsFromProjectStatus(projectStatus);
-          const failedConditions = this.getFailedConditions(conditions, measures);
+          const enhancedConditions = this.getEnhancedConditions(conditions, measures);
+          const failedConditions = enhancedConditions.filter((c) => c.level !== Status.OK);
 
-          const qgStatus = {
+          const qgStatus: QualityGateStatus = {
             ignoredConditions,
             caycStatus,
+            conditions: enhancedConditions,
             failedConditions,
             key,
             name,
@@ -362,10 +366,12 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
     );
   };
 
-  getFailedConditions = (conditions: QualityGateStatusCondition[], measures: MeasureEnhanced[]) => {
+  getEnhancedConditions = (
+    conditions: QualityGateStatusCondition[],
+    measures: MeasureEnhanced[],
+  ) => {
     return (
       conditions
-        .filter((c) => c.level !== 'OK')
         // Enhance them with Metric information, which will be needed
         // to render the conditions properly.
         .map((c) => enhanceConditionWithMeasure(c, measures))
index 85d096cd755f5142c410834a73257b4f3a9ac524..4a2efcc260ba0338cfc33f83d3debf685bf9363d 100644 (file)
@@ -41,15 +41,13 @@ import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../.
 import { AnalysisStatus } from '../components/AnalysisStatus';
 import SonarLintPromotion from '../components/SonarLintPromotion';
 import { MeasuresTabs } from '../utils';
-import AcceptedIssuesPanel from './AcceptedIssuesPanel';
 import ActivityPanel from './ActivityPanel';
 import BranchMetaTopBar from './BranchMetaTopBar';
-import BranchOverallCodePanel from './BranchOverallCodePanel';
 import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif';
-import { MeasuresPanel } from './MeasuresPanel';
 import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode';
 import NewCodeMeasuresPanel from './NewCodeMeasuresPanel';
 import NoCodeWarning from './NoCodeWarning';
+import OverallCodeMeasuresPanel from './OverallCodeMeasuresPanel';
 import QualityGatePanel from './QualityGatePanel';
 import { TabsPanel } from './TabsPanel';
 
@@ -173,14 +171,6 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
                         isNewCode={isNewCodeTab}
                         onTabSelect={selectTab}
                       >
-                        {!hasNewCodeMeasures && isNewCodeTab && (
-                          <MeasuresPanelNoNewCode
-                            branch={branch}
-                            component={component}
-                            period={period}
-                          />
-                        )}
-
                         {hasNewCodeMeasures &&
                           isMissingMeasures &&
                           isApplication(component.qualifier) && (
@@ -191,11 +181,15 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
                             </FlagMessage>
                           )}
 
-                        {!isNewCodeTab && (
-                          <BranchOverallCodePanel component={component} measures={measures} />
+                        {isNewCodeTab && !hasNewCodeMeasures && (
+                          <MeasuresPanelNoNewCode
+                            branch={branch}
+                            component={component}
+                            period={period}
+                          />
                         )}
 
-                        {hasNewCodeMeasures && isNewCodeTab && (
+                        {isNewCodeTab && hasNewCodeMeasures && (
                           <NewCodeMeasuresPanel
                             qgStatuses={qgStatuses}
                             branch={branch}
@@ -204,20 +198,14 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
                           />
                         )}
 
-                        <MeasuresPanel
-                          branch={branch}
-                          component={component}
-                          measures={measures}
-                          isNewCode={isNewCodeTab}
-                        />
-
-                        <AcceptedIssuesPanel
-                          branch={branch}
-                          component={component}
-                          measures={measures}
-                          isNewCode={isNewCodeTab}
-                          loading={loadingStatus}
-                        />
+                        {!isNewCodeTab && (
+                          <OverallCodeMeasuresPanel
+                            branch={branch}
+                            qgStatuses={qgStatuses}
+                            component={component}
+                            measures={measures}
+                          />
+                        )}
                       </TabsPanel>
 
                       <ActivityPanel
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx
deleted file mode 100644 (file)
index 6c6b6c5..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { Card, CoverageIndicator, DuplicationsIndicator } from 'design-system';
-import * as React from 'react';
-import { getTabPanelId } from '../../../components/controls/BoxedTabs';
-import { duplicationRatingConverter } from '../../../components/measure/utils';
-import { findMeasure } from '../../../helpers/measures';
-import { Branch } from '../../../types/branch-like';
-import { IssueType } from '../../../types/issues';
-import { MetricKey } from '../../../types/metrics';
-import { Component, MeasureEnhanced } from '../../../types/types';
-import { MeasurementType, MeasuresTabs } from '../utils';
-import MeasuresPanelIssueMeasure from './MeasuresPanelIssueMeasure';
-import MeasuresPanelPercentMeasure from './MeasuresPanelPercentMeasure';
-
-export interface MeasuresPanelProps {
-  branch?: Branch;
-  component: Component;
-  measures: MeasureEnhanced[];
-  isNewCode: boolean;
-}
-
-export function MeasuresPanel(props: MeasuresPanelProps) {
-  const { branch, component, measures, isNewCode } = props;
-
-  return (
-    <div
-      className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-6"
-      id={getTabPanelId(MeasuresTabs.Overall)}
-    >
-      {[IssueType.Bug, IssueType.CodeSmell, IssueType.Vulnerability, IssueType.SecurityHotspot].map(
-        (type: IssueType) => (
-          <Card key={type} className="sw-p-8">
-            <MeasuresPanelIssueMeasure
-              branchLike={branch}
-              component={component}
-              isNewCodeTab={isNewCode}
-              measures={measures}
-              type={type}
-            />
-          </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={isNewCode}
-          />
-        </Card>
-      )}
-
-      <Card className="sw-p-8">
-        <MeasuresPanelPercentMeasure
-          branchLike={branch}
-          component={component}
-          measures={measures}
-          ratingIcon={renderDuplicationIcon}
-          secondaryMetricKey={MetricKey.duplicated_blocks}
-          type={MeasurementType.Duplication}
-          useDiffMetric={isNewCode}
-        />
-      </Card>
-    </div>
-  );
-}
-
-export default 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" />;
-}
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
deleted file mode 100644 (file)
index 5fbfd20..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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/MeasuresPanelPercentCards.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentCards.tsx
new file mode 100644 (file)
index 0000000..d872135
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { getComponentDrilldownUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { isApplication } from '../../../types/component';
+import { MetricKey } from '../../../types/metrics';
+import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
+import { Component, MeasureEnhanced } from '../../../types/types';
+import MeasuresCardPercent from '../components/MeasuresCardPercent';
+import { MeasurementType, getMeasurementMetricKey } from '../utils';
+
+interface Props {
+  useDiffMetric?: boolean;
+  branch?: BranchLike;
+  component: Component;
+  measures: MeasureEnhanced[];
+  failedConditions: QualityGateStatusConditionEnhanced[];
+}
+
+/**
+ * Renders Coverage and Duplication cards for the Overview page.
+ */
+export default function MeasuresPanelPercentCards(props: Readonly<Props>) {
+  const { useDiffMetric, branch, component, measures, failedConditions } = props;
+
+  const isApp = isApplication(component.qualifier);
+
+  return (
+    <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
+      <MeasuresCardPercent
+        branchLike={branch}
+        componentKey={component.key}
+        conditions={failedConditions}
+        measures={measures}
+        measurementType={MeasurementType.Coverage}
+        label="overview.quality_gate.coverage"
+        url={getComponentDrilldownUrl({
+          componentKey: component.key,
+          metric: getMeasurementMetricKey(MeasurementType.Coverage, Boolean(useDiffMetric)),
+          branchLike: branch,
+          listView: true,
+        })}
+        conditionMetric={useDiffMetric ? MetricKey.new_coverage : MetricKey.coverage}
+        linesMetric={useDiffMetric ? MetricKey.new_lines_to_cover : MetricKey.lines_to_cover}
+        useDiffMetric={useDiffMetric}
+        showRequired={!isApp}
+      />
+
+      <MeasuresCardPercent
+        branchLike={branch}
+        componentKey={component.key}
+        conditions={failedConditions}
+        measures={measures}
+        measurementType={MeasurementType.Duplication}
+        label="overview.quality_gate.duplications"
+        url={getComponentDrilldownUrl({
+          componentKey: component.key,
+          metric: getMeasurementMetricKey(MeasurementType.Duplication, Boolean(useDiffMetric)),
+          branchLike: branch,
+          listView: true,
+        })}
+        conditionMetric={
+          useDiffMetric
+            ? MetricKey.new_duplicated_lines_density
+            : MetricKey.duplicated_lines_density
+        }
+        linesMetric={useDiffMetric ? MetricKey.new_lines : MetricKey.lines}
+        useDiffMetric={useDiffMetric}
+        showRequired={!isApp}
+      />
+    </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
deleted file mode 100644 (file)
index 2e53262..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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) && (
-          <div className="sw-body-sm 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
deleted file mode 100644 (file)
index c753796..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 00a35b001d6a87071e5801992331476a75766022..895a284662511be83fea21a525fa9065e152860f 100644 (file)
@@ -34,11 +34,7 @@ import { getLeakValue } from '../../../components/measure/utils';
 import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { findMeasure, formatMeasure } from '../../../helpers/measures';
-import {
-  getComponentDrilldownUrl,
-  getComponentIssuesUrl,
-  getComponentSecurityHotspotsUrl,
-} from '../../../helpers/urls';
+import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl } from '../../../helpers/urls';
 import { Branch } from '../../../types/branch-like';
 import { isApplication } from '../../../types/component';
 import { IssueStatus } from '../../../types/issues';
@@ -47,14 +43,8 @@ import { QualityGateStatus } from '../../../types/quality-gates';
 import { Component, MeasureEnhanced } from '../../../types/types';
 import { IssueMeasuresCardInner } from '../components/IssueMeasuresCardInner';
 import MeasuresCardNumber from '../components/MeasuresCardNumber';
-import MeasuresCardPercent from '../components/MeasuresCardPercent';
-import {
-  MeasurementType,
-  MeasuresTabs,
-  Status,
-  getConditionRequiredLabel,
-  getMeasurementMetricKey,
-} from '../utils';
+import { MeasuresTabs, Status, getConditionRequiredLabel } from '../utils';
+import MeasuresPanelPercentCards from './MeasuresPanelPercentCards';
 
 interface Props {
   branch?: Branch;
@@ -68,6 +58,7 @@ export default function NewCodeMeasuresPanel(props: Readonly<Props>) {
   const intl = useIntl();
   const isApp = isApplication(component.qualifier);
 
+  const conditions = qgStatuses?.flatMap((qg) => qg.conditions) ?? [];
   const failedConditions = qgStatuses?.flatMap((qg) => qg.failedConditions) ?? [];
 
   const newIssues = getLeakValue(findMeasure(measures, MetricKey.new_violations));
@@ -158,45 +149,16 @@ export default function NewCodeMeasuresPanel(props: Readonly<Props>) {
           }
         />
       </LightGreyCard>
-      <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
-        <MeasuresCardPercent
-          branchLike={branch}
-          componentKey={component.key}
-          conditions={failedConditions}
-          measures={measures}
-          measurementType={MeasurementType.Coverage}
-          label="overview.quality_gate.coverage"
-          url={getComponentDrilldownUrl({
-            componentKey: component.key,
-            metric: getMeasurementMetricKey(MeasurementType.Coverage, true),
-            branchLike: branch,
-            listView: true,
-          })}
-          conditionMetric={MetricKey.new_coverage}
-          linesMetric={MetricKey.new_lines_to_cover}
-          useDiffMetric
-          showRequired={!isApp}
-        />
 
-        <MeasuresCardPercent
-          branchLike={branch}
-          componentKey={component.key}
-          conditions={failedConditions}
-          measures={measures}
-          measurementType={MeasurementType.Duplication}
-          label="overview.quality_gate.duplications"
-          url={getComponentDrilldownUrl({
-            componentKey: component.key,
-            metric: getMeasurementMetricKey(MeasurementType.Duplication, true),
-            branchLike: branch,
-            listView: true,
-          })}
-          conditionMetric={MetricKey.new_duplicated_lines_density}
-          linesMetric={MetricKey.new_lines}
-          useDiffMetric
-          showRequired={!isApp}
-        />
+      <MeasuresPanelPercentCards
+        useDiffMetric
+        branch={branch}
+        component={component}
+        measures={measures}
+        failedConditions={failedConditions}
+      />
 
+      <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
         <MeasuresCardNumber
           label={
             newSecurityHotspots === '1'
@@ -207,7 +169,8 @@ export default function NewCodeMeasuresPanel(props: Readonly<Props>) {
             ...getBranchLikeQuery(branch),
           })}
           value={newSecurityHotspots}
-          conditions={failedConditions}
+          metric={MetricKey.new_security_hotspots}
+          conditions={conditions}
           conditionMetric={MetricKey.new_security_hotspots_reviewed}
           showRequired={!isApp}
         />
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx
new file mode 100644 (file)
index 0000000..4c22852
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 {
+  MetricsRatingBadge,
+  NoDataIcon,
+  SnoozeCircleIcon,
+  TextSubdued,
+  getTabPanelId,
+} from 'design-system';
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { findMeasure, formatMeasure, formatRating } from '../../../helpers/measures';
+import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { SoftwareQuality } from '../../../types/clean-code-taxonomy';
+import { isApplication } from '../../../types/component';
+import { IssueStatus } from '../../../types/issues';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { QualityGateStatus } from '../../../types/quality-gates';
+import { Component, MeasureEnhanced } from '../../../types/types';
+import MeasuresCard from '../components/MeasuresCard';
+import MeasuresCardNumber from '../components/MeasuresCardNumber';
+import { MeasuresTabs } from '../utils';
+import MeasuresPanelPercentCards from './MeasuresPanelPercentCards';
+import SoftwareImpactMeasureCard from './SoftwareImpactMeasureCard';
+
+export interface OverallCodeMeasuresPanelProps {
+  branch?: BranchLike;
+  component: Component;
+  measures: MeasureEnhanced[];
+  qgStatuses?: QualityGateStatus[];
+}
+
+export default function OverallCodeMeasuresPanel(props: Readonly<OverallCodeMeasuresPanelProps>) {
+  const { branch, qgStatuses, component, measures } = props;
+
+  const intl = useIntl();
+
+  const isApp = isApplication(component.qualifier);
+  const conditions = qgStatuses?.flatMap((qg) => qg.conditions) ?? [];
+  const failedConditions = qgStatuses?.flatMap((qg) => qg.failedConditions) ?? [];
+  const acceptedIssues = findMeasure(measures, MetricKey.accepted_issues)?.value;
+  const securityHotspots = findMeasure(measures, MetricKey.security_hotspots)?.value;
+  const securityRating = findMeasure(measures, MetricKey.security_rating)?.value;
+
+  return (
+    <div id={getTabPanelId(MeasuresTabs.Overall)} className="sw-mt-6">
+      <div className="sw-flex sw-gap-4">
+        <SoftwareImpactMeasureCard
+          component={component}
+          softwareQuality={SoftwareQuality.Security}
+          ratingMetricKey={MetricKey.security_rating}
+          measures={measures}
+        />
+        <SoftwareImpactMeasureCard
+          component={component}
+          softwareQuality={SoftwareQuality.Reliability}
+          ratingMetricKey={MetricKey.reliability_rating}
+          measures={measures}
+        />
+        <SoftwareImpactMeasureCard
+          component={component}
+          softwareQuality={SoftwareQuality.Maintainability}
+          ratingMetricKey={MetricKey.sqale_rating}
+          measures={measures}
+        />
+      </div>
+
+      <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
+        <MeasuresCard
+          url={getComponentIssuesUrl(component.key, {
+            ...getBranchLikeQuery(branch),
+            issueStatuses: IssueStatus.Accepted,
+          })}
+          value={formatMeasure(acceptedIssues, MetricType.ShortInteger)}
+          metric={MetricKey.accepted_issues}
+          label="overview.accepted_issues"
+          failed={false}
+          icon={
+            <SnoozeCircleIcon
+              color={acceptedIssues === '0' ? 'overviewCardDefaultIcon' : 'overviewCardWarningIcon'}
+            />
+          }
+        >
+          <TextSubdued className="sw-body-xs sw-mt-3">
+            {intl.formatMessage({
+              id: 'overview.accepted_issues.help_verbose',
+            })}
+          </TextSubdued>
+        </MeasuresCard>
+
+        <MeasuresCardNumber
+          label={
+            securityHotspots === '1'
+              ? 'issue.type.SECURITY_HOTSPOT'
+              : 'issue.type.SECURITY_HOTSPOT.plural'
+          }
+          url={getComponentSecurityHotspotsUrl(component.key, {
+            ...getBranchLikeQuery(branch),
+          })}
+          value={securityHotspots}
+          metric={MetricKey.security_hotspots}
+          conditions={conditions}
+          conditionMetric={MetricKey.security_hotspots_reviewed}
+          showRequired={!isApp}
+          icon={
+            securityRating ? (
+              <MetricsRatingBadge
+                label={securityRating}
+                rating={formatRating(securityRating)}
+                size="md"
+              />
+            ) : (
+              <NoDataIcon size="md" />
+            )
+          }
+        />
+      </div>
+
+      <MeasuresPanelPercentCards
+        branch={branch}
+        component={component}
+        measures={measures}
+        failedConditions={failedConditions}
+      />
+    </div>
+  );
+}
index 8600556c2fa900e21f65e533fa6e01ef23dc87ee..3e47c0b0b378ba36bfa7b186e1befff1603dfe47 100644 (file)
@@ -19,7 +19,7 @@
  */
 import styled from '@emotion/styled';
 import classNames from 'classnames';
-import { Badge, ContentLink, themeColor } from 'design-system';
+import { Badge, ContentLink, NoDataIcon, themeColor } from 'design-system';
 import * as React from 'react';
 import { Path } from 'react-router-dom';
 import Tooltip from '../../../components/controls/Tooltip';
@@ -82,7 +82,7 @@ export function IssueMeasuresCardInner(props: Readonly<IssueMeasuresCardInnerPro
               </ContentLink>
             </Tooltip>
           </div>
-          {icon}
+          {value ? icon : <NoDataIcon size="md" />}
         </div>
       </div>
       {footer}
index f8b35111e22103f519c8e376d3b324bd5234c80f..de80ee18c20630dbafe51c25e5aaecad42d4193c 100644 (file)
@@ -25,9 +25,9 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { localizeMetric } from '../../../helpers/measures';
 import { MetricKey } from '../../../types/metrics';
 
-interface Props {
+export interface MeasuresCardProps {
   url: To;
-  value: string;
+  value?: string;
   metric: MetricKey;
   label: string;
   failed?: boolean;
@@ -35,7 +35,7 @@ interface Props {
 }
 
 export default function MeasuresCard(
-  props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>,
+  props: React.PropsWithChildren<MeasuresCardProps & React.HTMLAttributes<HTMLDivElement>>,
 ) {
   const { failed, children, metric, icon, value, url, label, ...rest } = props;
 
index 1e3214186ea14c7d93995f50e195a9808b18385d..1c48863a3f7fb8a122603239d707106429d5d0ea 100644 (file)
@@ -25,13 +25,13 @@ import { formatMeasure } from '../../../helpers/measures';
 import { MetricKey, MetricType } from '../../../types/metrics';
 import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
 import { Status, getConditionRequiredLabel } from '../utils';
-import MeasuresCard from './MeasuresCard';
+import MeasuresCard, { MeasuresCardProps } from './MeasuresCard';
 
-interface Props {
+interface Props extends MeasuresCardProps {
   conditions: QualityGateStatusConditionEnhanced[];
   label: string;
   url: To;
-  value: string;
+  value?: string;
   conditionMetric: MetricKey;
   showRequired?: boolean;
 }
@@ -51,7 +51,6 @@ export default function MeasuresCardNumber(
     <MeasuresCard
       url={url}
       value={formatMeasure(value, MetricType.ShortInteger)}
-      metric={conditionMetric}
       label={label}
       failed={conditionFailed}
       {...rest}
index 664939e1bb3babcc25a7c14e961785ebee5439e8..d3b6eb96e263f10456a3bd6874ea6d595b88b412 100644 (file)
@@ -80,8 +80,9 @@ export default function MeasuresCardPercent(
   const value = useDiffMetric
     ? getLeakValue(findMeasure(measures, metricKey))
     : findMeasure(measures, metricKey)?.value;
-
-  const linesValue = getLeakValue(findMeasure(measures, linesMetric));
+  const linesValue = useDiffMetric
+    ? getLeakValue(findMeasure(measures, linesMetric))
+    : findMeasure(measures, linesMetric)?.value;
   const linesLabel =
     measurementType === MeasurementType.Coverage
       ? 'overview.quality_gate.on_x_new_lines_to_cover'
@@ -105,44 +106,42 @@ export default function MeasuresCardPercent(
       failed={conditionFailed}
       icon={renderIcon(measurementType, value)}
     >
-      <>
-        <span className="sw-body-xs sw-mt-3">
-          {showRequired &&
-            condition &&
-            (conditionFailed ? (
-              <TextError
-                className="sw-font-regular sw-inline"
-                text={getConditionRequiredLabel(condition, intl, true)}
-              />
-            ) : (
-              <LightLabel>{getConditionRequiredLabel(condition, intl)}</LightLabel>
-            ))}
-        </span>
-
-        <div className="sw-flex sw-body-sm sw-justify-between sw-items-center sw-mt-1">
-          <LightLabel className="sw-flex sw-items-center sw-gap-1 ">
-            <FormattedMessage
-              defaultMessage={translate(linesLabel)}
-              id={linesLabel}
-              values={{
-                link: (
-                  <ContentLink
-                    aria-label={translateWithParameters(
-                      'overview.see_more_details_on_x_y',
-                      linesValue ?? '0',
-                      localizeMetric(linesMetric),
-                    )}
-                    className="sw-body-md-highlight sw-text-lg"
-                    to={linesUrl}
-                  >
-                    {formatMeasure(linesValue ?? '0', MetricType.ShortInteger)}
-                  </ContentLink>
-                ),
-              }}
+      <span className="sw-body-xs sw-mt-3">
+        {showRequired &&
+          condition &&
+          (conditionFailed ? (
+            <TextError
+              className="sw-font-regular sw-inline"
+              text={getConditionRequiredLabel(condition, intl, true)}
             />
-          </LightLabel>
-        </div>
-      </>
+          ) : (
+            <LightLabel>{getConditionRequiredLabel(condition, intl)}</LightLabel>
+          ))}
+      </span>
+
+      <div className="sw-flex sw-body-sm sw-justify-between sw-items-center sw-mt-1">
+        <LightLabel className="sw-flex sw-items-center sw-gap-1 ">
+          <FormattedMessage
+            defaultMessage={translate(linesLabel)}
+            id={linesLabel}
+            values={{
+              link: (
+                <ContentLink
+                  aria-label={translateWithParameters(
+                    'overview.see_more_details_on_x_y',
+                    linesValue ?? '0',
+                    localizeMetric(linesMetric),
+                  )}
+                  className="sw-body-md-highlight sw-text-lg"
+                  to={linesUrl}
+                >
+                  {formatMeasure(linesValue ?? '0', MetricType.ShortInteger)}
+                </ContentLink>
+              ),
+            }}
+          />
+        </LightLabel>
+      </div>
     </MeasuresCard>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx
deleted file mode 100644 (file)
index 41e8b9e..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { LightPrimary } from 'design-system';
-import * as React from 'react';
-import { findMeasure, formatMeasure } from '../../../helpers/measures';
-import { MetricType } from '../../../types/metrics';
-import { MeasureEnhanced } from '../../../types/types';
-import { MeasurementType, getMeasurementAfterMergeMetricKey } from '../utils';
-
-export interface AfterMergeEstimateProps {
-  className?: string;
-  measures: MeasureEnhanced[];
-  type: MeasurementType;
-}
-
-export function AfterMergeEstimate({ className, measures, type }: AfterMergeEstimateProps) {
-  const afterMergeMetric = getMeasurementAfterMergeMetricKey(type);
-
-  const measure = findMeasure(measures, afterMergeMetric);
-
-  if (!measure || measure.value === undefined) {
-    return null;
-  }
-
-  return (
-    <div className={classNames(className, 'sw-flex sw-items-center')}>
-      <LightPrimary className="sw-body-sm-highlight">
-        {formatMeasure(measure.value, MetricType.Percent)}
-      </LightPrimary>
-    </div>
-  );
-}
-
-export default React.memo(AfterMergeEstimate);
index 37a36ecb1e2002bbb1987f65eaf03067e4bed31c..b67e517ca9506c530616e3fb67be95cc4235450b 100644 (file)
@@ -87,6 +87,7 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>)
               ...getBranchLikeQuery(pullRequest),
             })}
             value={newSecurityHotspots}
+            metric={MetricKey.new_security_hotspots}
             conditions={conditions}
             conditionMetric={MetricKey.new_security_hotspots_reviewed}
             showRequired
index 30c9e557766c8fa2a35b718bd5b132de0e7ae964..4510c2641df00e45ac8393f5b327d2b54c66b474 100644 (file)
@@ -20,8 +20,6 @@
 import { memoize } from 'lodash';
 import React from 'react';
 import { IntlShape } from 'react-intl';
-import CoverageRating from '../../components/ui/CoverageRating';
-import DuplicationsRating from '../../components/ui/DuplicationsRating';
 import { ISSUETYPE_METRIC_KEYS_MAP } from '../../helpers/issues';
 import { translate } from '../../helpers/l10n';
 import { formatMeasure } from '../../helpers/measures';
@@ -156,24 +154,10 @@ const MEASUREMENTS_MAP = {
   [MeasurementType.Coverage]: {
     metric: MetricKey.coverage,
     newMetric: MetricKey.new_coverage,
-    linesMetric: MetricKey.lines_to_cover,
-    newLinesMetric: MetricKey.new_lines_to_cover,
-    afterMergeMetric: MetricKey.coverage,
-    labelKey: 'metric.coverage.name',
-    expandedLabelKey: 'overview.coverage_on_X_lines',
-    newLinesExpandedLabelKey: 'overview.coverage_on_X_new_lines',
-    iconClass: CoverageRating,
   },
   [MeasurementType.Duplication]: {
     metric: MetricKey.duplicated_lines_density,
     newMetric: MetricKey.new_duplicated_lines_density,
-    linesMetric: MetricKey.ncloc,
-    newLinesMetric: MetricKey.new_lines,
-    afterMergeMetric: MetricKey.duplicated_lines_density,
-    labelKey: 'metric.duplicated_lines_density.short_name',
-    expandedLabelKey: 'overview.duplications_on_X_lines',
-    newLinesExpandedLabelKey: 'overview.duplications_on_X_new_lines',
-    iconClass: DuplicationsRating,
   },
 };
 
@@ -223,10 +207,6 @@ export function getIssueRatingName(type: IssueType) {
   return translate('metric_domain', ISSUETYPE_METRIC_KEYS_MAP[type].ratingName);
 }
 
-export function getIssueIconClass(type: IssueType) {
-  return ISSUETYPE_METRIC_KEYS_MAP[type].iconClass;
-}
-
 export function getIssueMetricKey(type: IssueType, useDiffMetric: boolean) {
   return useDiffMetric
     ? ISSUETYPE_METRIC_KEYS_MAP[type].newMetric
@@ -239,31 +219,10 @@ export function getIssueRatingMetricKey(type: IssueType, useDiffMetric: boolean)
     : ISSUETYPE_METRIC_KEYS_MAP[type].rating;
 }
 
-export function getMeasurementIconClass(type: MeasurementType) {
-  return MEASUREMENTS_MAP[type].iconClass;
-}
-
 export function getMeasurementMetricKey(type: MeasurementType, useDiffMetric: boolean) {
   return useDiffMetric ? MEASUREMENTS_MAP[type].newMetric : MEASUREMENTS_MAP[type].metric;
 }
 
-export function getMeasurementAfterMergeMetricKey(type: MeasurementType) {
-  return MEASUREMENTS_MAP[type].afterMergeMetric;
-}
-
-export function getMeasurementLinesMetricKey(type: MeasurementType, useDiffMetric: boolean) {
-  return useDiffMetric ? MEASUREMENTS_MAP[type].newLinesMetric : MEASUREMENTS_MAP[type].linesMetric;
-}
-
-export function getMeasurementLabelKeys(type: MeasurementType, useDiffMetric: boolean) {
-  return {
-    expandedLabelKey: useDiffMetric
-      ? MEASUREMENTS_MAP[type].newLinesExpandedLabelKey
-      : MEASUREMENTS_MAP[type].expandedLabelKey,
-    labelKey: MEASUREMENTS_MAP[type].labelKey,
-  };
-}
-
 export const parseQuery = memoize((urlQuery: RawQuery): { codeScope: string } => {
   return {
     codeScope: parseAsString(urlQuery['code_scope']),
index bda28e1abe2abf851de1109c3396338b0425555a..f44e2396b24a996d0dcbf9860bd6910bb4bc13cd 100644 (file)
@@ -39,10 +39,12 @@ export function mockQualityGate(overrides: Partial<QualityGate> = {}): QualityGa
 export function mockQualityGateStatus(
   overrides: Partial<QualityGateStatus> = {},
 ): QualityGateStatus {
+  const condition = mockQualityGateStatusConditionEnhanced();
   return {
     ignoredConditions: false,
     caycStatus: CaycStatus.Compliant,
-    failedConditions: [mockQualityGateStatusConditionEnhanced()],
+    conditions: [condition],
+    failedConditions: [condition],
     key: 'foo',
     name: 'Foo',
     status: 'ERROR',
index e6084098fdad423b607e1553c146b2a83905bcfc..8a7701bec6fd53d5e1f5aba6c9fcae50363b5b47 100644 (file)
@@ -64,6 +64,7 @@ export interface QualityGateApplicationStatusChildProject {
 }
 
 export interface QualityGateStatus {
+  conditions: QualityGateStatusConditionEnhanced[];
   failedConditions: QualityGateStatusConditionEnhanced[];
   ignoredConditions?: boolean;
   caycStatus: CaycStatus;
index 45b83dc14cee76c88d76f6334865b84ace5f24e3..cfb318747f81ffdec1af7f35aa110f8d418be72e 100644 (file)
@@ -977,10 +977,10 @@ issue.type.CODE_SMELL=Code Smell
 issue.type.BUG=Bug
 issue.type.VULNERABILITY=Vulnerability
 issue.type.SECURITY_HOTSPOT=Security Hotspot
+issue.type.SECURITY_HOTSPOT.plural=Security Hotspots
 issue.type.CODE_SMELL.plural=Code Smells
 issue.type.BUG.plural=Bugs
 issue.type.VULNERABILITY.plural=Vulnerabilities
-issue.type.SECURITY_HOTSPOT.plural=Security Hotspots
 
 issue.type.deprecation.title=Issue types are deprecated and can no longer be modified.
 issue.type.deprecation.filter_by=You can now filter issues by:
@@ -3861,6 +3861,7 @@ overview.pull_request.fixed_issues.disclaimer=Only issues fixed on the files mod
 overview.pull_request.fixed_issues.disclaimer.2=When the pull request and the target branch are not synchronized, issues introduced on the target branch may be incorrectly considered fixed by the pull request. Rebasing the pull request would give an updated value.
 overview.accepted_issues=Accepted issues
 overview.accepted_issues.help=Valid issues that were not fixed
+overview.accepted_issues.help_verbose=Valid issues, but not fixed. They represent accepted technical debt.
 overview.quality_gate.status=Quality Gate Status
 overview.quality_gate=Quality Gate
 overview.quality_gate_x=Quality Gate: {0}