]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21455 Implement software quality breakdown cards & Integrate them
author7PH <b.raymond@protonmail.com>
Wed, 24 Jan 2024 08:51:31 +0000 (09:51 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 31 Jan 2024 20:03:36 +0000 (20:03 +0000)
14 files changed:
server/sonar-web/design-system/src/components/Link.tsx
server/sonar-web/design-system/src/components/MetricsRatingBadge.tsx
server/sonar-web/design-system/src/components/Separator.tsx
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/src/main/js/apps/overview/branches/BranchOverallCodePanel.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureBreakdownCard.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
server/sonar-web/src/main/js/apps/overview/utils.tsx
server/sonar-web/src/main/js/components/measure/RatingTooltipContent.tsx
server/sonar-web/src/main/js/types/clean-code-taxonomy.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 402b99493b9441b4ec9ec526565a0a12a13522f8..38ad2a351abe236b81aa1000ab68a5c8299cec95 100644 (file)
@@ -150,6 +150,12 @@ const StyledBaseLink = styled(BaseLink)`
     `};
 `;
 
+export const NakedLink = styled(BaseLink)`
+  border-bottom: none;
+  font-weight: 600;
+  color: ${themeColor('linkNaked')};
+`;
+
 export const DrilldownLink = styled(StyledBaseLink)`
   ${tw`sw-heading-lg`}
   ${tw`sw-tracking-tight`}
index 564f28162e5e99eadbf80315b487b0250c9553e0..d82e9a8684333cc56267145e4e3adbc755f92fbb 100644 (file)
@@ -22,7 +22,7 @@ import tw from 'twin.macro';
 import { getProp, themeColor, themeContrast } from '../helpers/theme';
 import { MetricsLabel } from '../types/measures';
 
-type sizeType = 'xs' | 'sm' | 'md' | 'xl';
+type sizeType = keyof typeof SIZE_MAPPING;
 interface Props extends React.AriaAttributes {
   className?: string;
   label: string;
@@ -34,6 +34,7 @@ const SIZE_MAPPING = {
   xs: '1rem',
   sm: '1.5rem',
   md: '2rem',
+  lg: '2.8rem',
   xl: '4rem',
 };
 
@@ -95,6 +96,7 @@ const MetricsRatingBadgeStyled = styled.div<{ rating: MetricsLabel; size: string
   color: ${({ rating }) => themeContrast(`rating.${rating}`)};
   font-size: ${({ size }) => getFontSize(size)};
   background-color: ${({ rating }) => themeColor(`rating.${rating}`)};
+  user-select: none;
 
   display: inline-flex;
   align-items: center;
index d70677f340082f8674b46e4e79c3f7170ab56cd1..d02b45072254e60dfcd5530b8efc4ce8bc14b873 100644 (file)
@@ -34,6 +34,10 @@ export const BlueGreySeparator = styled(BasicSeparator)`
   background-color: ${themeColor('popupBorder')};
 `;
 
+export const CardSeparator = styled(BasicSeparator)`
+  background-color: ${themeColor('projectCardBorder')};
+`;
+
 export const GreySeparator = styled(BasicSeparator)`
   background-color: ${themeColor('subnavigationBorder')};
 `;
index 1eb37c7c559c91080c745b0400997d803ff04101..79a6cab6ea98be6f845a8e9686391142942e8e0f 100644 (file)
@@ -338,6 +338,7 @@ export const lightTheme = {
 
     // links
     linkDefault: primary.default,
+    linkNaked: COLORS.blueGrey[700],
     linkActive: COLORS.indigo[600],
     linkDiscreet: 'currentColor',
     linkTooltipDefault: COLORS.indigo[200],
@@ -529,6 +530,12 @@ export const lightTheme = {
     overviewCardErrorIcon: COLORS.red[100],
     overviewCardSuccessIcon: COLORS.green[200],
 
+    // overview software impact breakdown
+    overviewSoftwareImpactSeverityNeutral: COLORS.blueGrey[50],
+    overviewSoftwareImpactSeverityHigh: COLORS.red[100],
+    overviewSoftwareImpactSeverityMedium: COLORS.yellow[100],
+    overviewSoftwareImpactSeverityLow: COLORS.blue[100],
+
     // graph - chart
     graphPointCircleColor: COLORS.white,
     'graphLineColor.0': COLORS.blue[500],
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
new file mode 100644 (file)
index 0000000..e483b2d
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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">
+      <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 289d9131bf08cffa139fe3f4bc288029c21e9652..3aee0a122bfcd2cadbb26e888d3e36737f3866ac 100644 (file)
@@ -41,6 +41,7 @@ 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';
@@ -179,6 +180,10 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
 
                         {!isNewCodeTab && (
                           <>
+                            {!isNewCodeTab && (
+                              <BranchOverallCodePanel component={component} measures={measures} />
+                            )}
+
                             <MeasuresPanel
                               branch={branch}
                               component={component}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureBreakdownCard.tsx b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureBreakdownCard.tsx
new file mode 100644 (file)
index 0000000..472a56f
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * 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 { DiscreetLinkBox, Tooltip, themeColor } from 'design-system';
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
+import { formatMeasure } from '../../../helpers/measures';
+import { getComponentIssuesUrl } from '../../../helpers/urls';
+import { SoftwareImpactSeverity, SoftwareQuality } from '../../../types/clean-code-taxonomy';
+import { MetricType } from '../../../types/metrics';
+import { Component } from '../../../types/types';
+
+export interface SoftwareImpactMeasureBreakdownCardProps {
+  softwareQuality: SoftwareQuality;
+  component: Component;
+  value: string;
+  severity: SoftwareImpactSeverity;
+  active?: boolean;
+}
+
+export function SoftwareImpactMeasureBreakdownCard(
+  props: Readonly<SoftwareImpactMeasureBreakdownCardProps>,
+) {
+  const { softwareQuality, component, value, severity, active } = props;
+
+  const intl = useIntl();
+
+  const url = getComponentIssuesUrl(component.key, {
+    ...DEFAULT_ISSUES_QUERY,
+    impactSoftwareQualities: softwareQuality,
+    impactSeverities: severity,
+  });
+
+  return (
+    <Tooltip
+      overlay={intl.formatMessage({
+        id: `overview.measures.software_impact.severity.${severity}.tooltip`,
+      })}
+    >
+      <StyledBreakdownCard
+        aria-label={intl.formatMessage(
+          {
+            id: 'overview.measures.software_impact.severity.see_x_open_issues',
+          },
+          {
+            count: formatMeasure(value, MetricType.ShortInteger),
+            softwareQuality: intl.formatMessage({
+              id: `software_quality.${softwareQuality}`,
+            }),
+            severity: intl.formatMessage({
+              id: `overview.measures.software_impact.severity.${severity}.tooltip`,
+            }),
+          },
+        )}
+        to={url}
+        className={classNames(
+          'sw-w-1/3 sw-p-2 sw-rounded-1 sw-text-xs sw-font-semibold sw-select-none sw-flex sw-gap-1 sw-justify-center sw-items-center',
+          severity,
+          {
+            active,
+          },
+        )}
+      >
+        <span>{formatMeasure(value, MetricType.ShortInteger)}</span>
+        <span>
+          {intl.formatMessage({
+            id: `overview.measures.software_impact.severity.${severity}`,
+          })}
+        </span>
+      </StyledBreakdownCard>
+    </Tooltip>
+  );
+}
+
+const StyledBreakdownCard = styled(DiscreetLinkBox)`
+  background-color: ${themeColor('overviewSoftwareImpactSeverityNeutral')};
+
+  &.active.HIGH {
+    background-color: ${themeColor('overviewSoftwareImpactSeverityHigh')};
+  }
+  &.active.MEDIUM {
+    background-color: ${themeColor('overviewSoftwareImpactSeverityMedium')};
+  }
+  &.active.LOW {
+    background-color: ${themeColor('overviewSoftwareImpactSeverityLow')};
+  }
+`;
+
+export default SoftwareImpactMeasureBreakdownCard;
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx
new file mode 100644 (file)
index 0000000..c7a5de8
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * 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 { Card, CardSeparator, NakedLink, TextBold, TextSubdued, themeBorder } from 'design-system';
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
+import { formatMeasure, formatRating } from '../../../helpers/measures';
+import { getComponentIssuesUrl } from '../../../helpers/urls';
+import {
+  SoftwareImpactMeasureData,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
+} from '../../../types/clean-code-taxonomy';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { Component, MeasureEnhanced } from '../../../types/types';
+import { getSoftwareImpactSeverityValue, softwareQualityToMeasure } from '../utils';
+import SoftwareImpactMeasureBreakdownCard from './SoftwareImpactMeasureBreakdownCard';
+import SoftwareImpactMeasureRating from './SoftwareImpactMeasureRating';
+
+export interface SoftwareImpactBreakdownCardProps {
+  component: Component;
+  softwareQuality: SoftwareQuality;
+  ratingMetricKey: MetricKey;
+  measures: MeasureEnhanced[];
+}
+
+export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdownCardProps>) {
+  const { component, softwareQuality, ratingMetricKey, measures } = props;
+
+  const intl = useIntl();
+
+  // Find measure for this software quality
+  const metricKey = softwareQualityToMeasure(softwareQuality);
+  const measureRaw = measures.find((m) => m.metric.key === metricKey);
+  const measure = JSON.parse(measureRaw?.value ?? 'null') as SoftwareImpactMeasureData | null;
+
+  // Hide this card if there is no measure
+  if (!measure) {
+    return null;
+  }
+
+  // Find rating measure
+  const ratingMeasure = measures.find((m) => m.metric.key === ratingMetricKey);
+  const ratingLabel = ratingMeasure?.value ? formatRating(ratingMeasure.value) : undefined;
+
+  const totalLinkHref = getComponentIssuesUrl(component.key, {
+    ...DEFAULT_ISSUES_QUERY,
+    impactSoftwareQualities: softwareQuality,
+  });
+
+  // We highlight the highest severity breakdown card with non-zero count if the rating is not A
+  const issuesBySeverity = {
+    [SoftwareImpactSeverity.High]: measure.high,
+    [SoftwareImpactSeverity.Medium]: measure.medium,
+    [SoftwareImpactSeverity.Low]: measure.low,
+  };
+  const shouldHighlightSeverity = measure && (!ratingLabel || ratingLabel !== 'A');
+  const highlightedSeverity = shouldHighlightSeverity
+    ? Object.entries(issuesBySeverity).find(([_, issuesCount]) => issuesCount > 0)?.[0]
+    : null;
+
+  return (
+    <StyledCard className="sw-w-1/3 sw-rounded-2 sw-p-4 sw-flex-col">
+      <TextBold name={intl.formatMessage({ id: `software_quality.${softwareQuality}` })} />
+      <CardSeparator className="sw--mx-4" />
+      <div className="sw-flex sw-flex-col sw-gap-3">
+        <div className="sw-flex sw-gap-1 sw-items-end">
+          <NakedLink
+            aria-label={intl.formatMessage(
+              {
+                id: `overview.measures.software_impact.see_list_of_x_open_issues`,
+              },
+              {
+                count: measure.total,
+                softwareQuality: intl.formatMessage({
+                  id: `software_quality.${softwareQuality}`,
+                }),
+              },
+            )}
+            className="sw-text-xl"
+            to={totalLinkHref}
+          >
+            {formatMeasure(measure.total, MetricType.ShortInteger)}
+          </NakedLink>
+          <TextSubdued className="sw-body-sm sw-mb-2">
+            {intl.formatMessage({ id: 'overview.measures.software_impact.total_open_issues' })}
+          </TextSubdued>
+          <div className="sw-flex-grow sw-flex sw-justify-end">
+            <SoftwareImpactMeasureRating
+              softwareQuality={softwareQuality}
+              value={ratingMeasure?.value}
+            />
+          </div>
+        </div>
+        <div className="sw-flex sw-gap-2">
+          {[
+            SoftwareImpactSeverity.High,
+            SoftwareImpactSeverity.Medium,
+            SoftwareImpactSeverity.Low,
+          ].map((severity) => (
+            <SoftwareImpactMeasureBreakdownCard
+              key={severity}
+              component={component}
+              softwareQuality={softwareQuality}
+              value={getSoftwareImpactSeverityValue(severity, measure)}
+              severity={severity}
+              active={highlightedSeverity === severity}
+            />
+          ))}
+        </div>
+      </div>
+    </StyledCard>
+  );
+}
+
+const StyledCard = styled(Card)`
+  border: ${themeBorder('default')};
+`;
+
+export default SoftwareImpactMeasureCard;
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx b/server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx
new file mode 100644 (file)
index 0000000..7099e26
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * 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 { BasicSeparator, MetricsRatingBadge, Tooltip, themeColor } from 'design-system';
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { formatRating } from '../../../helpers/measures';
+import { SoftwareImpactSeverity, SoftwareQuality } from '../../../types/clean-code-taxonomy';
+
+export interface SoftwareImpactMeasureRatingProps {
+  softwareQuality: SoftwareQuality;
+  value?: string;
+}
+
+export function SoftwareImpactMeasureRating(props: Readonly<SoftwareImpactMeasureRatingProps>) {
+  const { softwareQuality, value } = props;
+
+  const intl = useIntl();
+
+  const rating = formatRating(value);
+
+  function ratingToWorseSeverity(rating: string): SoftwareImpactSeverity {
+    switch (rating) {
+      case 'B':
+        return SoftwareImpactSeverity.Low;
+      case 'C':
+        return SoftwareImpactSeverity.Medium;
+      case 'D':
+      case 'E':
+        return SoftwareImpactSeverity.High;
+      default:
+        return SoftwareImpactSeverity.Low;
+    }
+  }
+
+  function getTooltipContent() {
+    if (!rating || rating === 'A') {
+      return null;
+    }
+
+    const softwareQualityLabel = intl.formatMessage({
+      id: `software_quality.${softwareQuality}`,
+    });
+    const severityLabel = intl.formatMessage({
+      id: `overview.measures.software_impact.severity.${ratingToWorseSeverity(
+        rating,
+      )}.improve_tooltip`,
+    });
+
+    return (
+      <div className="sw-flex sw-flex-col sw-gap-1">
+        <span className="sw-font-semibold">
+          {intl.formatMessage({
+            id: 'overview.measures.software_impact.improve_rating_tooltip.title',
+          })}
+        </span>
+
+        <span>
+          {intl.formatMessage(
+            {
+              id: 'overview.measures.software_impact.improve_rating_tooltip.content.1',
+            },
+            {
+              softwareQuality: softwareQualityLabel,
+              ratingLabel: rating,
+              severity: severityLabel,
+            },
+          )}
+        </span>
+
+        <span className="sw-mt-4">
+          {intl.formatMessage({
+            id: 'overview.measures.software_impact.improve_rating_tooltip.content.2',
+          })}
+        </span>
+      </div>
+    );
+  }
+
+  return (
+    <Tooltip overlay={getTooltipContent()}>
+      <MetricsRatingBadge
+        size="lg"
+        className="sw-text-sm"
+        rating={rating}
+        label={intl.formatMessage(
+          {
+            id: 'metric.has_rating_X',
+          },
+          { 0: value },
+        )}
+      />
+    </Tooltip>
+  );
+}
+
+export const StyledSeparator = styled(BasicSeparator)`
+  background-color: ${themeColor('projectCardBorder')};
+`;
+
+export default SoftwareImpactMeasureRating;
index 3f54500b1c012b6ec448e3a80c46cfe8c8f11f1b..cd3ba100250d1d66645c913498b7c8d97d8102d9 100644 (file)
@@ -37,7 +37,7 @@ import {
 } from '../../../../helpers/mocks/quality-gates';
 import { mockLoggedInUser, mockPeriod } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { byRole } from '../../../../helpers/testSelector';
+import { byLabelText, byRole } from '../../../../helpers/testSelector';
 import { ComponentQualifier } from '../../../../types/component';
 import { MetricKey, MetricType } from '../../../../types/metrics';
 import {
@@ -64,16 +64,58 @@ jest.mock('../../../../api/measures', () => {
           type = MetricType.Percent;
         } else if (/_rating$/.test(key)) {
           type = MetricType.Rating;
+        } else if (
+          [
+            MetricKey.reliability_issues,
+            MetricKey.security_issues,
+            MetricKey.maintainability_issues,
+          ].includes(key as MetricKey)
+        ) {
+          type = MetricType.Data;
         } else {
           type = MetricType.Integer;
         }
         metrics.push(mockMetric({ key, id: key, name: key, type }));
-        measures.push(
-          mockMeasure({
-            metric: key,
-            ...(isDiffMetric(key) ? { leak: '1' } : { period: undefined }),
-          }),
-        );
+
+        const measure = mockMeasure({
+          metric: key,
+          ...(isDiffMetric(key) ? { leak: '1' } : { period: undefined }),
+        });
+
+        // Mock software quality measures
+        if (type === MetricType.Data) {
+          if (key === MetricKey.reliability_issues) {
+            measure.value = JSON.stringify({
+              total: 3,
+              high: 0,
+              medium: 2,
+              low: 1,
+            });
+          } else if (key === MetricKey.maintainability_issues) {
+            measure.value = JSON.stringify({
+              total: 2,
+              high: 0,
+              medium: 0,
+              low: 1,
+            });
+          } else {
+            measure.value = JSON.stringify({
+              total: 0,
+              high: 0,
+              medium: 0,
+              low: 0,
+            });
+          }
+        }
+        // Mock software quality rating
+        if (key === MetricKey.reliability_rating) {
+          measure.value = '3';
+        } else if (key === MetricKey.security_rating) {
+          measure.value = '2';
+        } else if (key.endsWith('_rating') || key === MetricKey.sqale_rating) {
+          measure.value = '1';
+        }
+        measures.push(measure);
       });
       return Promise.resolve({
         component: {
@@ -221,7 +263,7 @@ describe('project overview', () => {
       screen.queryByText('overview.quality_gate.conditions.cayc.warning'),
     ).not.toBeInTheDocument();
 
-    //Measures panel
+    // Measures panel
     expect(screen.getByText('overview.new_issues')).toBeInTheDocument();
     expect(
       byRole('link', {
@@ -229,7 +271,7 @@ describe('project overview', () => {
       }).get(),
     ).toBeInTheDocument();
 
-    // go to overall
+    // Go to overall
     await user.click(screen.getByText('overview.overall_code'));
 
     expect(screen.getByText('metric.vulnerabilities.name')).toBeInTheDocument();
@@ -238,6 +280,36 @@ describe('project overview', () => {
         name: 'overview.see_more_details_on_x_of_y.1.metric.high_impact_accepted_issues.name',
       }).get(),
     ).toBeInTheDocument();
+
+    // Software breakdown links should be correct
+    expect(
+      byRole('link', {
+        name: 'overview.measures.software_impact.see_list_of_x_open_issues.3.software_quality.RELIABILITY',
+      }).get(),
+    ).toHaveAttribute(
+      'href',
+      '/project/issues?issueStatuses=OPEN%2CCONFIRMED&impactSoftwareQualities=RELIABILITY&id=foo',
+    );
+    expect(
+      byRole('link', {
+        name: 'overview.measures.software_impact.severity.see_x_open_issues.2.software_quality.RELIABILITY.overview.measures.software_impact.severity.MEDIUM.tooltip',
+      }).get(),
+    ).toHaveAttribute(
+      'href',
+      '/project/issues?issueStatuses=OPEN%2CCONFIRMED&impactSoftwareQualities=RELIABILITY&impactSeverities=MEDIUM&id=foo',
+    );
+
+    // Active severity card should be the one with the highest severity
+    expect(
+      byRole('link', {
+        name: 'overview.measures.software_impact.severity.see_x_open_issues.2.software_quality.RELIABILITY.overview.measures.software_impact.severity.MEDIUM.tooltip',
+      }).get(),
+    ).toHaveClass('active', 'MEDIUM');
+
+    // Ratings should be correct and rendered
+    expect(byLabelText('metric.has_rating_X.C').get()).toBeInTheDocument();
+    expect(byLabelText('metric.has_rating_X.B').get()).toBeInTheDocument();
+    expect(byLabelText('metric.has_rating_X.A').getAll()).toHaveLength(2);
   });
 
   it('should show a successful non-compliant QG', async () => {
index a5002a83a74e50628b98131b7e382c4e059ec63a..30c9e557766c8fa2a35b718bd5b132de0e7ae964 100644 (file)
@@ -26,6 +26,11 @@ import { ISSUETYPE_METRIC_KEYS_MAP } from '../../helpers/issues';
 import { translate } from '../../helpers/l10n';
 import { formatMeasure } from '../../helpers/measures';
 import { parseAsString } from '../../helpers/query';
+import {
+  SoftwareImpactMeasureData,
+  SoftwareImpactSeverity,
+  SoftwareQuality,
+} from '../../types/clean-code-taxonomy';
 import { IssueType } from '../../types/issues';
 import { MetricKey } from '../../types/metrics';
 import { AnalysisMeasuresVariations, MeasureHistory } from '../../types/project-activity';
@@ -201,6 +206,19 @@ export const METRICS_REPORTED_IN_OVERVIEW_CARDS = [
   MetricKey.duplicated_lines_density,
 ];
 
+export function softwareQualityToMeasure(softwareQuality: SoftwareQuality): MetricKey {
+  return (softwareQuality.toLowerCase() + '_issues') as MetricKey;
+}
+
+// Extract the number of issues for a given severity in the software impact measure
+export function getSoftwareImpactSeverityValue(
+  severity: SoftwareImpactSeverity,
+  softwareImpactMeasure?: SoftwareImpactMeasureData,
+) {
+  const key = severity.toLowerCase() as keyof SoftwareImpactMeasureData;
+  return softwareImpactMeasure ? softwareImpactMeasure[key]?.toString() : '';
+}
+
 export function getIssueRatingName(type: IssueType) {
   return translate('metric_domain', ISSUETYPE_METRIC_KEYS_MAP[type].ratingName);
 }
index f27496ac46498aaf4a1646b6f40e6498fa17b11b..26dd0f4b542a9e5eedf644dd8086cdc223398061 100644 (file)
@@ -38,7 +38,7 @@ export interface RatingTooltipContentProps {
   value: number | string;
 }
 
-export function RatingTooltipContent(props: RatingTooltipContentProps) {
+export function RatingTooltipContent(props: Readonly<RatingTooltipContentProps>) {
   const {
     appState: { settings },
     metricKey,
@@ -56,7 +56,7 @@ export function RatingTooltipContent(props: RatingTooltipContentProps) {
   const rating = Number(value);
   const ratingLetter = formatMeasure(value, MetricType.Rating);
 
-  if (finalMetricKey !== 'sqale_rating' && finalMetricKey !== 'maintainability_rating') {
+  if (finalMetricKey !== MetricKey.sqale_rating && finalMetricKey !== 'maintainability_rating') {
     return <>{translate('metric', finalMetricKey, 'tooltip', ratingLetter)}</>;
   }
 
index 4992bc2f520910c421971a1cfe26957201bb9e3a..ea51bfbe928e7e1a8e80e426edec4127022fd448 100644 (file)
@@ -57,3 +57,10 @@ export interface SoftwareImpact {
   softwareQuality: SoftwareQuality;
   severity: SoftwareImpactSeverity;
 }
+
+export interface SoftwareImpactMeasureData {
+  total: number;
+  high: number;
+  medium: number;
+  low: number;
+}
index 07a721c42c27f6ce461cdaab66aac52a34b7c487..1548fd4e4615e855511e85cd276f43fd4638acff 100644 (file)
@@ -161,6 +161,7 @@ on=on
 or=Or
 open=Open
 open_in_ide=Open in IDE
+open_issues=Open issues
 optional=Optional
 order=Order
 owner=Owner
@@ -3928,6 +3929,21 @@ overview.measures.same_reference.explanation=This branch is configured to use it
 overview.measures.bad_reference.explanation=This branch could not be compared to its reference branch. See the SCM or analysis report for more details.
 overview.measures.bad_setting.link=This can be fixed in the {setting_link} setting.
 overview.measures.security_hotspots_reviewed=Reviewed
+overview.measures.software_impact.total_open_issues=Open issues
+overview.measures.software_impact.see_list_of_x_open_issues=See the list of {count} open {softwareQuality} issues
+overview.measures.software_impact.severity.see_x_open_issues=See {count} open {softwareQuality} issues with {severity} severity
+overview.measures.software_impact.severity.HIGH=H
+overview.measures.software_impact.severity.HIGH.tooltip=High Impact
+overview.measures.software_impact.severity.HIGH.improve_tooltip=high
+overview.measures.software_impact.severity.MEDIUM=M
+overview.measures.software_impact.severity.MEDIUM.tooltip=Medium Impact
+overview.measures.software_impact.severity.MEDIUM.improve_tooltip=medium
+overview.measures.software_impact.severity.LOW=L
+overview.measures.software_impact.severity.LOW.tooltip=Low Impact
+overview.measures.software_impact.severity.LOW.improve_tooltip=low
+overview.measures.software_impact.improve_rating_tooltip.title=Improve rating by fixing the highest severity issues first
+overview.measures.software_impact.improve_rating_tooltip.content.1={softwareQuality} rating is a {ratingLabel} when there is at least one {severity} impact vulnerability.
+overview.measures.software_impact.improve_rating_tooltip.content.2=To improve your rating, start fixing the issues with highest severity first.
 
 overview.project.no_lines_of_code=This project has no lines of code.
 overview.project.empty=This project is empty.