]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21280 Branch overview shows accepted issues
authorstanislavh <stanislav.honcharov@sonarsource.com>
Mon, 18 Dec 2023 13:11:03 +0000 (14:11 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 17 Jan 2024 20:02:44 +0000 (20:02 +0000)
15 files changed:
server/sonar-web/design-system/src/components/icons/HighImpactCircleIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/index.ts
server/sonar-web/src/main/js/api/measures.ts
server/sonar-web/src/main/js/apps/issues/sidebar/PeriodFilter.tsx
server/sonar-web/src/main/js/apps/overview/branches/AcceptedIssuesPanel.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/MeasuresPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx
server/sonar-web/src/main/js/apps/overview/components/IssueMeasuresCardInner.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/IssueMeasuresCard.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/IssueMeasuresCardInner.tsx [deleted file]
server/sonar-web/src/main/js/apps/overview/utils.tsx
server/sonar-web/src/main/js/types/metrics.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/design-system/src/components/icons/HighImpactCircleIcon.tsx b/server/sonar-web/design-system/src/components/icons/HighImpactCircleIcon.tsx
new file mode 100644 (file)
index 0000000..d869ab5
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { useTheme } from '@emotion/react';
+import { themeColor, themeContrast } from '../../helpers';
+import { CustomIcon, IconProps } from './Icon';
+
+export function HighImpactCircleIcon(props: Readonly<IconProps>) {
+  const theme = useTheme();
+
+  const bgColor = themeColor('overviewCardErrorIcon')({
+    theme,
+  });
+  const iconColor = themeContrast('overviewCardErrorIcon')({
+    theme,
+  });
+
+  return (
+    <CustomIcon height="36" viewBox="0 0 36 36" width="36" {...props}>
+      <circle cx="18" cy="18" fill={bgColor} r="18" />
+      <path
+        clipRule="evenodd"
+        d="M17.875 25.75C22.2242 25.75 25.75 22.2242 25.75 17.875C25.75 13.5258 22.2242 10 17.875 10C13.5258 10 10 13.5258 10 17.875C10 22.2242 13.5258 25.75 17.875 25.75ZM14.6622 16.111C14.5628 16.1619 14.5 16.2661 14.5 16.38V20.9489C14.5 21.1589 14.7047 21.3043 14.8965 21.2306L17.772 20.1254C17.8384 20.0998 17.9116 20.0998 17.978 20.1254L20.8535 21.2306C21.0453 21.3043 21.25 21.1589 21.25 20.9489V16.38C21.25 16.2661 21.1872 16.1619 21.0878 16.111L18.0062 14.5318C17.9236 14.4894 17.8264 14.4894 17.7438 14.5318L14.6622 16.111Z"
+        fill={iconColor}
+        fillRule="evenodd"
+      />
+    </CustomIcon>
+  );
+}
index 0efa2dea06c2891bfdcdb94efd24151bd4b149b0..1a5e151422aaa54149b0b6cbd14bca281407cdf5 100644 (file)
@@ -39,6 +39,7 @@ export { FlagInfoIcon } from './FlagInfoIcon';
 export { FlagSuccessIcon } from './FlagSuccessIcon';
 export { FlagWarningIcon } from './FlagWarningIcon';
 export { HelperHintIcon } from './HelperHintIcon';
+export { HighImpactCircleIcon } from './HighImpactCircleIcon';
 export { HomeFillIcon } from './HomeFillIcon';
 export { HomeIcon } from './HomeIcon';
 export * from './Icon';
index 8df28f012e0be8e3c6e52c39880849ce75e641b8..98eb5d0539a64480de18a5bb6b95eefc8d157e0e 100644 (file)
@@ -108,17 +108,41 @@ export function getMeasuresWithPeriod(
   }).catch(throwGlobalError);
 }
 
-export function getMeasuresWithPeriodAndMetrics(
+export async function getMeasuresWithPeriodAndMetrics(
   component: string,
   metrics: string[],
   branchParameters?: BranchParameters,
 ): Promise<MeasuresAndMetaWithPeriod & MeasuresAndMetaWithMetrics> {
-  return getJSON(COMPONENT_URL, {
+  // TODO: Remove this mock (SONAR-21323&SONAR-21275)
+  const mockedMetrics = metrics.filter(
+    (metric) =>
+      metric !== MetricKey.new_accepted_issues && metric !== MetricKey.high_impact_accepted_issues,
+  );
+  const result = await getJSON(COMPONENT_URL, {
     additionalFields: 'period,metrics',
     component,
-    metricKeys: metrics.join(','),
+    metricKeys: mockedMetrics.join(','),
     ...branchParameters,
   }).catch(throwGlobalError);
+  if (metrics.includes(MetricKey.high_impact_accepted_issues)) {
+    result.metrics.push({
+      key: MetricKey.high_impact_accepted_issues,
+      name: 'Accepted Issues with high impact',
+      description: 'Accepted Issues with high impact',
+      domain: 'Reliability',
+      type: MetricType.Integer,
+      higherValuesAreBetter: false,
+      qualitative: true,
+      hidden: false,
+      bestValue: '0',
+    });
+    result.component.measures?.push({
+      metric: MetricKey.high_impact_accepted_issues,
+      value: '3',
+    });
+  }
+
+  return result;
 }
 
 export function getMeasuresForProjects(
index 7d5bf439d970ebd33b8fd2dae487ad3be5e78ba4..4768f867260871975640cbed9fdd0ec209273ebe 100644 (file)
@@ -20,7 +20,7 @@
 import { BasicSeparator, FacetItem } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
-import { MeasuresPanelTabs } from '../../overview/branches/MeasuresPanel';
+import { MeasuresTabs } from '../../overview/utils';
 import { Query } from '../utils';
 import { FacetItemsList } from './FacetItemsList';
 
@@ -56,7 +56,7 @@ export function PeriodFilter(props: PeriodFilterProps) {
         className="it__search-navigator-facet"
         name={translate('issues.new_code')}
         onClick={handleClick}
-        value={newCodeSelected ? MeasuresPanelTabs.New : MeasuresPanelTabs.Overall}
+        value={newCodeSelected ? MeasuresTabs.New : MeasuresTabs.Overall}
       />
 
       <BasicSeparator className="sw-mb-5 sw-mt-4" />
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
new file mode 100644 (file)
index 0000000..a03ea74
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import classNames from 'classnames';
+import {
+  Card,
+  HighImpactCircleIcon,
+  LightLabel,
+  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
+              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 className="sw--translate-y-3" neutral={acceptedCount === '0'} />
+              }
+            />
+            {!isNewCode && (
+              <>
+                <StyledCardSeparator />
+                <IssueMeasuresCardInner
+                  className="sw-w-1/2"
+                  metric={MetricKey.high_impact_accepted_issues}
+                  value={formatMeasure(acceptedWithHighImpactCount, MetricType.ShortInteger)}
+                  header={intl.formatMessage({
+                    id: `metric.${MetricKey.high_impact_accepted_issues}.name`,
+                  })}
+                  url={acceptedIssuesWithHighImpactUrl}
+                  icon={<HighImpactCircleIcon className="sw--translate-y-3" />}
+                />
+              </>
+            )}
+          </Card>
+        </div>
+      </Spinner>
+    </div>
+  );
+}
+
+const StyledCardSeparator = styled.div`
+  width: 1px;
+  background-color: ${themeColor('projectCardBorder')};
+`;
+
+export default React.memo(AcceptedIssuesPanel);
index 469dfc60a99955959d1ec6e7f39c342f72e784ac..6fe2a9dca435c84c0e8fead059c4cbae963856fc 100644 (file)
 import { LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
 import * as React from 'react';
 import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
+import { useLocation } from '../../../components/hoc/withRouter';
 import { parseDate } from '../../../helpers/dates';
+import { isDiffMetric } from '../../../helpers/measures';
+import { CodeScope } from '../../../helpers/urls';
 import { ApplicationPeriod } from '../../../types/application';
 import { Branch } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
 import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity';
 import { QualityGateStatus } from '../../../types/quality-gates';
 import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
+import { MeasuresTabs } from '../utils';
+import AcceptedIssuesPanel from './AcceptedIssuesPanel';
 import ActivityPanel from './ActivityPanel';
 import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif';
 import MeasuresPanel from './MeasuresPanel';
+import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode';
 import NoCodeWarning from './NoCodeWarning';
 import QualityGatePanel from './QualityGatePanel';
 
@@ -64,7 +70,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
     graph,
     loadingHistory,
     loadingStatus,
-    measures,
+    measures = [],
     measuresHistory = [],
     metrics = [],
     onGraphChange,
@@ -74,7 +80,24 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
     qualityGate,
   } = props;
 
+  const { query } = useLocation();
+  const [tab, selectTab] = React.useState(() => {
+    return query.codeScope === CodeScope.Overall ? MeasuresTabs.Overall : MeasuresTabs.New;
+  });
+
   const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period;
+  const isNewCodeTab = tab === MeasuresTabs.New;
+  const hasNewCodeMeasures = measures.some((m) => isDiffMetric(m.metric.key));
+
+  React.useEffect(() => {
+    // Open Overall tab by default if there are no new measures.
+    if (loadingStatus === false && !hasNewCodeMeasures && isNewCodeTab) {
+      selectTab(MeasuresTabs.Overall);
+    }
+    // In this case, we explicitly do NOT want to mark tab as a dependency, as
+    // it would prevent the user from selecting it, even if it's empty.
+    /* eslint-disable-next-line react-hooks/exhaustive-deps */
+  }, [loadingStatus, hasNewCodeMeasures]);
 
   return (
     <>
@@ -103,16 +126,36 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
 
                 <div className="sw-flex-1">
                   <div className="sw-flex sw-flex-col sw-pt-6">
-                    <MeasuresPanel
-                      analyses={analyses}
-                      appLeak={appLeak}
-                      branch={branch}
-                      component={component}
-                      loading={loadingStatus}
-                      measures={measures}
-                      period={period}
-                      qgStatuses={qgStatuses}
-                    />
+                    {!hasNewCodeMeasures && isNewCodeTab && !loadingStatus ? (
+                      <MeasuresPanelNoNewCode
+                        branch={branch}
+                        component={component}
+                        period={period}
+                      />
+                    ) : (
+                      <>
+                        <MeasuresPanel
+                          analyses={analyses}
+                          appLeak={appLeak}
+                          branch={branch}
+                          component={component}
+                          loading={loadingStatus}
+                          measures={measures}
+                          period={period}
+                          qgStatuses={qgStatuses}
+                          isNewCode={isNewCodeTab}
+                          onTabSelect={selectTab}
+                        />
+
+                        <AcceptedIssuesPanel
+                          branch={branch}
+                          component={component}
+                          measures={measures}
+                          isNewCode={isNewCodeTab}
+                          loading={loadingStatus}
+                        />
+                      </>
+                    )}
 
                     <ActivityPanel
                       analyses={analyses}
index 6d97dffe91998f3b4ab47d45a55596af2c608cc5..06adbd842810b18d18ab3182b2cc6a7a888c78fa 100644 (file)
@@ -33,11 +33,9 @@ import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import DocLink from '../../../components/common/DocLink';
 import ComponentReportActions from '../../../components/controls/ComponentReportActions';
-import { Location, withRouter } from '../../../components/hoc/withRouter';
 import { duplicationRatingConverter } from '../../../components/measure/utils';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { findMeasure, isDiffMetric } from '../../../helpers/measures';
-import { CodeScope } from '../../../helpers/urls';
 import { ApplicationPeriod } from '../../../types/application';
 import { Branch } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
@@ -46,11 +44,10 @@ import { MetricKey } from '../../../types/metrics';
 import { Analysis, ProjectAnalysisEventCategory } from '../../../types/project-activity';
 import { QualityGateStatus } from '../../../types/quality-gates';
 import { Component, MeasureEnhanced, Period } from '../../../types/types';
-import { MeasurementType, parseQuery } from '../utils';
+import { MeasurementType, MeasuresTabs } from '../utils';
 import { MAX_ANALYSES_NB } from './ActivityPanel';
 import { LeakPeriodInfo } from './LeakPeriodInfo';
 import MeasuresPanelIssueMeasure from './MeasuresPanelIssueMeasure';
-import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode';
 import MeasuresPanelPercentMeasure from './MeasuresPanelPercentMeasure';
 
 export interface MeasuresPanelProps {
@@ -59,15 +56,11 @@ export interface MeasuresPanelProps {
   branch?: Branch;
   component: Component;
   loading?: boolean;
-  measures?: MeasureEnhanced[];
+  measures: MeasureEnhanced[];
   period?: Period;
-  location: Location;
   qgStatuses?: QualityGateStatus[];
-}
-
-export enum MeasuresPanelTabs {
-  New = 'new',
-  Overall = 'overall',
+  isNewCode: boolean;
+  onTabSelect: (tab: MeasuresTabs) => void;
 }
 
 const SQ_UPGRADE_NOTIFICATION_TIMEOUT = { weeks: 3 };
@@ -79,29 +72,19 @@ export function MeasuresPanel(props: MeasuresPanelProps) {
     branch,
     component,
     loading,
-    measures = [],
+    measures,
     period,
     qgStatuses = [],
-    location,
+    isNewCode,
   } = props;
 
-  const hasDiffMeasures = measures.some((m) => isDiffMetric(m.metric.key));
   const isApp = component.qualifier === ComponentQualifier.Application;
   const leakPeriod = isApp ? appLeak : period;
-  const query = parseQuery(location.query);
 
   const { failingConditionsOnNewCode, failingConditionsOnOverallCode } =
     countFailingConditions(qgStatuses);
   const failingConditions = failingConditionsOnNewCode + failingConditionsOnOverallCode;
 
-  const [tab, selectTab] = React.useState(() => {
-    return query.codeScope === CodeScope.Overall
-      ? MeasuresPanelTabs.Overall
-      : MeasuresPanelTabs.New;
-  });
-
-  const isNewCodeTab = tab === MeasuresPanelTabs.New;
-
   const recentSqUpgradeEvent = React.useMemo(() => {
     if (!analyses || analyses.length === 0) {
       return undefined;
@@ -139,24 +122,14 @@ export function MeasuresPanel(props: MeasuresPanelProps) {
     });
   };
 
-  React.useEffect(() => {
-    // Open Overall tab by default if there are no new measures.
-    if (loading === false && !hasDiffMeasures && isNewCodeTab) {
-      selectTab(MeasuresPanelTabs.Overall);
-    }
-    // In this case, we explicitly do NOT want to mark tab as a dependency, as
-    // it would prevent the user from selecting it, even if it's empty.
-    /* eslint-disable-next-line react-hooks/exhaustive-deps */
-  }, [loading, hasDiffMeasures]);
-
   const tabs = [
     {
-      value: MeasuresPanelTabs.New,
+      value: MeasuresTabs.New,
       label: translate('overview.new_code'),
       counter: failingConditionsOnNewCode,
     },
     {
-      value: MeasuresPanelTabs.Overall,
+      value: MeasuresTabs.Overall,
       label: translate('overview.overall_code'),
       counter: failingConditionsOnOverallCode,
     },
@@ -196,7 +169,11 @@ export function MeasuresPanel(props: MeasuresPanelProps) {
             </div>
           )}
           <div className="sw-flex sw-items-center">
-            <ToggleButton onChange={(key) => selectTab(key)} options={tabs} value={tab} />
+            <ToggleButton
+              onChange={props.onTabSelect}
+              options={tabs}
+              value={isNewCode ? MeasuresTabs.New : MeasuresTabs.Overall}
+            />
             {failingConditions > 0 && (
               <LightLabel className="sw-body-sm-highlight sw-ml-8">
                 {failingConditions === 1
@@ -205,7 +182,7 @@ export function MeasuresPanel(props: MeasuresPanelProps) {
               </LightLabel>
             )}
           </div>
-          {tab === MeasuresPanelTabs.New && leakPeriod ? (
+          {isNewCode && leakPeriod ? (
             <LightLabel className="sw-body-sm sw-flex sw-items-center sw-mt-4">
               <span className="sw-mr-1">{translate('overview.new_code')}:</span>
               <LeakPeriodInfo leakPeriod={leakPeriod} />
@@ -230,62 +207,58 @@ export function MeasuresPanel(props: MeasuresPanelProps) {
             </FlagMessage>
           )}
 
-          {!hasDiffMeasures && isNewCodeTab ? (
-            <MeasuresPanelNoNewCode branch={branch} component={component} period={period} />
-          ) : (
-            <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
-              {[
-                IssueType.Bug,
-                IssueType.CodeSmell,
-                IssueType.Vulnerability,
-                IssueType.SecurityHotspot,
-              ].map((type: IssueType) => (
-                <Card key={type} className="sw-p-8">
-                  <MeasuresPanelIssueMeasure
-                    branchLike={branch}
-                    component={component}
-                    isNewCodeTab={isNewCodeTab}
-                    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={isNewCodeTab}
-                  />
-                </Card>
-              )}
+          <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
+            {[
+              IssueType.Bug,
+              IssueType.CodeSmell,
+              IssueType.Vulnerability,
+              IssueType.SecurityHotspot,
+            ].map((type: IssueType) => (
+              <Card key={type} className="sw-p-8">
+                <MeasuresPanelIssueMeasure
+                  branchLike={branch}
+                  component={component}
+                  isNewCodeTab={isNewCode}
+                  measures={measures}
+                  type={type}
+                />
+              </Card>
+            ))}
 
-              <Card className="sw-p-8">
+            {(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={renderDuplicationIcon}
-                  secondaryMetricKey={MetricKey.duplicated_blocks}
-                  type={MeasurementType.Duplication}
-                  useDiffMetric={isNewCodeTab}
+                  ratingIcon={renderCoverageIcon}
+                  secondaryMetricKey={MetricKey.tests}
+                  type={MeasurementType.Coverage}
+                  useDiffMetric={isNewCode}
                 />
               </Card>
-            </div>
-          )}
+            )}
+
+            <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>
         </>
       )}
     </div>
   );
 }
 
-export default withRouter(React.memo(MeasuresPanel));
+export default React.memo(MeasuresPanel);
 
 function renderCoverageIcon(value?: string) {
   return <CoverageIndicator value={value} size="md" />;
index dd2737d434c492a09b54320fa4e3b37553691e74..b074b53a63b54db754cdbb9081d04d7d7da4548f 100644 (file)
@@ -37,6 +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 { ComponentQualifier } from '../../../../types/component';
 import { MetricKey, MetricType } from '../../../../types/metrics';
 import {
@@ -218,11 +219,21 @@ describe('project overview', () => {
 
     //Measures panel
     expect(screen.getByText('metric.new_vulnerabilities.name')).toBeInTheDocument();
+    expect(
+      byRole('link', {
+        name: 'overview.see_more_details_on_x_of_y.1.metric.accepted_issues.name',
+      }).get(),
+    ).toBeInTheDocument();
 
     // go to overall
     await user.click(screen.getByText('overview.overall_code'));
 
     expect(screen.getByText('metric.vulnerabilities.name')).toBeInTheDocument();
+    expect(
+      byRole('link', {
+        name: 'overview.see_more_details_on_x_of_y.1.metric.high_impact_accepted_issues.name',
+      }).get(),
+    ).toBeInTheDocument();
   });
 
   it('should show a successful non-compliant QG', async () => {
index cac37b8347a3086f46d33ef80f76369f68677bae..d3bfaa0bdad7ca72d2946e5fbe8af49912b8265b 100644 (file)
@@ -59,7 +59,7 @@ export default function BranchQualityGateConditions(props: Readonly<Props>) {
   );
 
   return (
-    <ul className="sw-flex sw-items-center sw-gap-2 sw-flex-wrap">
+    <ul className="sw-flex sw-items-center sw-gap-2 sw-flex-wrap sw-mb-4">
       {filteredFailedConditions.map((condition) => (
         <li key={condition.metric}>
           <FailedQGCondition branchLike={branchLike} component={component} condition={condition} />
diff --git a/server/sonar-web/src/main/js/apps/overview/components/IssueMeasuresCardInner.tsx b/server/sonar-web/src/main/js/apps/overview/components/IssueMeasuresCardInner.tsx
new file mode 100644 (file)
index 0000000..3c6e7ef
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import styled from '@emotion/styled';
+import classNames from 'classnames';
+import { Badge, ContentLink, themeColor } from 'design-system';
+import * as React from 'react';
+import { Path } from 'react-router-dom';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { localizeMetric } from '../../../helpers/measures';
+import { MetricKey } from '../../../types/metrics';
+
+interface IssueMeasuresCardInnerProps extends React.HTMLAttributes<HTMLDivElement> {
+  metric: MetricKey;
+  value?: string;
+  header: React.ReactNode;
+  url: Path;
+  failed?: boolean;
+  icon?: React.ReactNode;
+  footer?: React.ReactNode;
+}
+
+export function IssueMeasuresCardInner(props: Readonly<IssueMeasuresCardInnerProps>) {
+  const { header, metric, icon, value, url, failed, footer, className, ...rest } = props;
+
+  return (
+    <div className={classNames('sw-flex sw-flex-col sw-gap-3', className)} {...rest}>
+      <div className="sw-flex sw-flex-col sw-gap-2 sw-font-semibold">
+        <ColorBold className="sw-flex sw-items-center sw-gap-2 sw-body-sm-highlight">
+          {header}
+
+          {failed && (
+            <Badge className="sw-h-fit" variant="deleted">
+              {translate('overview.measures.failed_badge')}
+            </Badge>
+          )}
+        </ColorBold>
+        <div className="sw-flex sw-justify-between sw-items-center sw-h-9">
+          <div className="sw-h-fit">
+            <ContentLink
+              aria-label={translateWithParameters(
+                'overview.see_more_details_on_x_of_y',
+                value || '0',
+                localizeMetric(metric),
+              )}
+              className="it__overview-measures-value sw-w-fit sw-text-lg"
+              to={url}
+            >
+              {value || '0'}
+            </ContentLink>
+          </div>
+
+          {icon}
+        </div>
+      </div>
+      {footer}
+    </div>
+  );
+}
+
+const ColorBold = styled.div`
+  color: ${themeColor('pageTitle')};
+`;
index 99225368fcf235185565f48348d2cdd547ac997a..12efb13cf8747d4644de6cece58dd158ed24d427 100644 (file)
@@ -42,8 +42,8 @@ import { PullRequest } from '../../../types/branch-like';
 import { MetricKey, MetricType } from '../../../types/metrics';
 import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
 import { Component, MeasureEnhanced } from '../../../types/types';
+import { IssueMeasuresCardInner } from '../components/IssueMeasuresCardInner';
 import { getConditionRequiredLabel, Status } from '../utils';
-import { IssueMeasuresCardInner } from './IssueMeasuresCardInner';
 
 interface Props {
   conditions: QualityGateStatusConditionEnhanced[];
@@ -82,6 +82,7 @@ export default function IssueMeasuresCard(
   return (
     <Card className="sw-p-8 sw-rounded-2 sw-flex sw-text-base sw-gap-4" {...rest}>
       <IssueMeasuresCardInner
+        className="sw-w-1/3"
         header={intl.formatMessage({ id: 'overview.pull_request.new_issues' })}
         data-test="overview__measures-new-violations"
         data-guiding-id={issuesConditionFailed ? 'overviewZeroNewIssuesSimplification' : undefined}
@@ -106,6 +107,7 @@ export default function IssueMeasuresCard(
       />
       <StyledCardSeparator />
       <IssueMeasuresCardInner
+        className="sw-w-1/3"
         header={intl.formatMessage({ id: 'overview.pull_request.accepted_issues' })}
         metric={MetricKey.new_accepted_issues}
         value={formatMeasure(acceptedCount, MetricType.ShortInteger)}
@@ -119,6 +121,7 @@ export default function IssueMeasuresCard(
       />
       <StyledCardSeparator />
       <IssueMeasuresCardInner
+        className="sw-w-1/3"
         header={
           <>
             {intl.formatMessage({ id: 'overview.pull_request.fixed_issues' })}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/IssueMeasuresCardInner.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/IssueMeasuresCardInner.tsx
deleted file mode 100644 (file)
index ccdf8c9..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { Badge, ContentLink } from 'design-system';
-import * as React from 'react';
-import { Path } from 'react-router-dom';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { localizeMetric } from '../../../helpers/measures';
-import { MetricKey } from '../../../types/metrics';
-
-interface IssueMeasuresCardInnerProps extends React.HTMLAttributes<HTMLDivElement> {
-  metric: MetricKey;
-  value?: string;
-  header: React.ReactNode;
-  url: Path;
-  failed?: boolean;
-  icon?: React.ReactNode;
-  footer?: React.ReactNode;
-}
-
-export function IssueMeasuresCardInner(props: Readonly<IssueMeasuresCardInnerProps>) {
-  const { header, metric, icon, value, url, failed, footer, ...rest } = props;
-
-  return (
-    <div className="sw-w-1/3 sw-flex sw-flex-col sw-gap-3" {...rest}>
-      <div className="sw-flex sw-flex-col sw-gap-2 sw-font-semibold">
-        <div className="sw-flex sw-items-center sw-gap-2">
-          {header}
-
-          {failed && (
-            <Badge className="sw-h-fit" variant="deleted">
-              {translate('overview.measures.failed_badge')}
-            </Badge>
-          )}
-        </div>
-        <div className="sw-flex sw-justify-between sw-items-center sw-h-9">
-          <div className="sw-h-fit">
-            <ContentLink
-              aria-label={translateWithParameters(
-                'overview.see_more_details_on_x_of_y',
-                value ?? '0',
-                localizeMetric(metric),
-              )}
-              className="it__overview-measures-value sw-w-fit sw-text-lg"
-              to={url}
-            >
-              {value ?? '0'}
-            </ContentLink>
-          </div>
-
-          {icon}
-        </div>
-      </div>
-      {footer}
-    </div>
-  );
-}
index a3816df71a4fc2693e55a9675d75113056e4b824..47021a519c5bc4291c3c955028e4053a9c5cb533 100644 (file)
@@ -37,6 +37,11 @@ export const METRICS: string[] = [
   MetricKey.alert_status,
   MetricKey.quality_gate_details, // TODO: still relevant?
 
+  // issues
+  MetricKey.accepted_issues,
+  MetricKey.new_accepted_issues,
+  MetricKey.high_impact_accepted_issues,
+
   // bugs
   MetricKey.bugs,
   MetricKey.new_bugs,
@@ -125,6 +130,11 @@ const MEASURES_VARIATIONS_METRICS = [
   MetricKey.vulnerabilities,
 ];
 
+export enum MeasuresTabs {
+  New = 'new',
+  Overall = 'overall',
+}
+
 export enum MeasurementType {
   Coverage = 'COVERAGE',
   Duplication = 'DUPLICATION',
index f1b6edd1f0d720f930313d3b67c2b969a392ed87..043b54fd979041b9a2279cd7e83d54d174fc5737 100644 (file)
@@ -155,6 +155,7 @@ export enum MetricKey {
   violations = 'violations',
   vulnerabilities = 'vulnerabilities',
   accepted_issues = 'accepted_issues',
+  high_impact_accepted_issues = 'high_impact_accepted_issues',
   wont_fix_issues = 'wont_fix_issues',
 }
 
index f43ef32e6700cdd7bceaa19b8db2b7359ae4ceae..cbd79aa6161365ae5e701831b8a9e91f60936946 100644 (file)
@@ -3268,7 +3268,8 @@ metric.pull_request_fixed_issues.name=Fixed issues
 metric.pull_request_fixed_issues.description=Fixed issues
 metric.new_accepted_issues.name=Accepted issues
 metric.new_accepted_issues.description=Accepted issues
-
+metric.high_impact_accepted_issues.name=High impact accepted issues
+metric.high_impact_accepted_issues.description=High impact accepted issues
 #------------------------------------------------------------------------------
 #
 # PERMISSIONS
@@ -3915,6 +3916,9 @@ overview.project_key.click_to_copy=Click to copy the key to your clipboard
 overview.activity=Activity
 overview.activity.graph_shows_data_for_x=This graph shows historical data for {0}. Click on the "Activity" link below to see more information.
 overview.recent_activity=Recent Activity
+overview.accepted_issues=Accepted issues
+overview.accepted_issues.description=Issues that are valid, but were not fixed and represent accepted technical debt.
+overview.accepted_issues.total=Total accepted issues
 overview.measures=Measures
 overview.measures.empty_explanation=Measures on New Code will appear after the second analysis of this branch.
 overview.measures.empty_link={learn_more_link} about the Clean as You Code approach.