From d611d5a0c20b5d6d9193ae7caedabdd51121d741 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Thu, 20 Apr 2023 11:55:19 +0200 Subject: [PATCH] SONAR-19018 New UI for the Measures section of Project Overview --- .../config/jest/SetupReactTestingLibrary.ts | 4 +- .../config/jest/SetupTestEnvironment.ts | 4 + .../design-system/src/components/Link.tsx | 12 + .../design-system/src/components/Text.tsx | 8 + .../branches/ApplicationLeakPeriodInfo.tsx | 11 +- .../branches/BranchOverviewRenderer.tsx | 5 +- .../branches/DrilldownMeasureValue.tsx | 50 ++-- .../apps/overview/branches/MeasuresPanel.tsx | 231 ++++++++++-------- .../overview/branches/MeasuresPanelCard.tsx | 43 ++++ .../branches/MeasuresPanelIssueMeasure.tsx | 85 +++++++ .../branches/MeasuresPanelIssueMeasureRow.tsx | 103 -------- .../branches/MeasuresPanelPercentMeasure.tsx | 142 +++++++++++ .../MeasuresPanelPercentMeasureLabel.tsx | 74 ++++++ .../branches/ProjectLeakPeriodInfo.tsx | 23 +- .../branches/__tests__/BranchOverview-it.tsx | 2 +- .../apps/overview/components/IssueLabel.tsx | 29 ++- .../apps/overview/components/IssueRating.tsx | 68 +++--- .../components/QualityGateStatusTitle.tsx | 16 +- .../components/__tests__/IssueLabel-test.tsx | 4 +- .../components/__tests__/IssueRating-test.tsx | 11 +- .../pullRequests/AfterMergeEstimate.tsx | 14 +- .../pullRequests/PullRequestOverview.tsx | 82 +++---- .../sonar-web/src/main/js/helpers/issues.ts | 5 +- .../sonar-web/src/main/js/helpers/measures.ts | 20 +- .../resources/org/sonar/l10n/core.properties | 4 +- 25 files changed, 677 insertions(+), 373 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelCard.tsx create mode 100644 server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasure.tsx delete mode 100644 server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasureRow.tsx create mode 100644 server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasure.tsx create mode 100644 server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasureLabel.tsx diff --git a/server/sonar-web/config/jest/SetupReactTestingLibrary.ts b/server/sonar-web/config/jest/SetupReactTestingLibrary.ts index f7d03248c53..afaa0a4fcfb 100644 --- a/server/sonar-web/config/jest/SetupReactTestingLibrary.ts +++ b/server/sonar-web/config/jest/SetupReactTestingLibrary.ts @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { configure } from '@testing-library/dom'; import '@testing-library/jest-dom'; +import { configure } from '@testing-library/react'; configure({ - asyncUtilTimeout: 3000 + asyncUtilTimeout: 3000, }); diff --git a/server/sonar-web/config/jest/SetupTestEnvironment.ts b/server/sonar-web/config/jest/SetupTestEnvironment.ts index 3e393db4fb6..54350855106 100644 --- a/server/sonar-web/config/jest/SetupTestEnvironment.ts +++ b/server/sonar-web/config/jest/SetupTestEnvironment.ts @@ -17,6 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import React from 'react'; + +(window as any).React = React; + const content = document.createElement('div'); content.id = 'content'; document.documentElement.appendChild(content); diff --git a/server/sonar-web/design-system/src/components/Link.tsx b/server/sonar-web/design-system/src/components/Link.tsx index 237b9bf3223..8f1bb78e43f 100644 --- a/server/sonar-web/design-system/src/components/Link.tsx +++ b/server/sonar-web/design-system/src/components/Link.tsx @@ -173,6 +173,18 @@ export const LinkBox = styled(StyledBaseLink)` `; LinkBox.displayName = 'LinkBox'; +export const DiscreetLinkBox = styled(StyledBaseLink)` + text-decoration: none; + + &:hover, + &:focus, + &:active { + background-color: none; + display: block; + } +`; +LinkBox.displayName = 'DiscreetLinkBox'; + export const DiscreetLink = styled(HoverLink)` --border: ${themeBorder('default', 'linkDiscreet')}; `; diff --git a/server/sonar-web/design-system/src/components/Text.tsx b/server/sonar-web/design-system/src/components/Text.tsx index 540074b8bc8..775091713c5 100644 --- a/server/sonar-web/design-system/src/components/Text.tsx +++ b/server/sonar-web/design-system/src/components/Text.tsx @@ -93,3 +93,11 @@ const StyledPageTitle = styled(StyledText)` const StyledTextError = styled(StyledText)` color: ${themeColor('danger')}; `; + +export const LightLabel = styled.span` + color: ${themeColor('pageContentLight')}; +`; + +export const LightPrimary = styled.span` + color: ${themeContrast('primaryLight')}; +`; diff --git a/server/sonar-web/src/main/js/apps/overview/branches/ApplicationLeakPeriodInfo.tsx b/server/sonar-web/src/main/js/apps/overview/branches/ApplicationLeakPeriodInfo.tsx index b103b681c98..e33d5ea5e67 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/ApplicationLeakPeriodInfo.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/ApplicationLeakPeriodInfo.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { HelperHintIcon } from 'design-system'; import * as React from 'react'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import DateFromNow from '../../../components/intl/DateFromNow'; @@ -29,18 +30,20 @@ export interface ApplicationLeakPeriodInfoProps { export function ApplicationLeakPeriodInfo({ leakPeriod }: ApplicationLeakPeriodInfoProps) { return ( -
+ <> {(fromNow) => translateWithParameters('overview.started_x', fromNow)} -
+ > + + + ); } diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx index 3b194749b6a..7593b4ae651 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx @@ -93,7 +93,7 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp ) : (
-
+
-
+
-; - } else { - content = ( - - - {formatMeasure(measure.value, 'SHORT_INT')} - - - ); + return –; } + const url = getComponentDrilldownUrl({ + branchLike, + componentKey: component.key, + metric, + }); + return ( -
- {content} - {getLocalizedMetricName({ key: metric })} -
+ + + {formatMeasure(measure.value, MetricType.ShortInteger)} + + ); } 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 index ea5733ae563..1cbfcaafef2 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx @@ -17,13 +17,20 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { + Card, + CoverageIndicator, + DeferredSpinner, + DuplicationsIndicator, + LightLabel, + PageTitle, + ToggleButton, +} from 'design-system'; import * as React from 'react'; -import { rawSizes } from '../../../app/theme'; -import BoxedTabs, { getTabId, getTabPanelId } from '../../../components/controls/BoxedTabs'; import ComponentReportActions from '../../../components/controls/ComponentReportActions'; import { Location, withRouter } from '../../../components/hoc/withRouter'; -import DeferredSpinner from '../../../components/ui/DeferredSpinner'; -import { translate } from '../../../helpers/l10n'; +import { duplicationRatingConverter } from '../../../components/measure/utils'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; import { findMeasure, isDiffMetric } from '../../../helpers/measures'; import { CodeScope } from '../../../helpers/urls'; import { ApplicationPeriod } from '../../../types/application'; @@ -31,13 +38,13 @@ import { Branch } from '../../../types/branch-like'; import { ComponentQualifier } from '../../../types/component'; import { IssueType } from '../../../types/issues'; import { MetricKey } from '../../../types/metrics'; +import { QualityGateStatus } from '../../../types/quality-gates'; import { Component, MeasureEnhanced, Period } from '../../../types/types'; -import MeasurementLabel from '../components/MeasurementLabel'; import { MeasurementType, parseQuery } from '../utils'; -import { DrilldownMeasureValue } from './DrilldownMeasureValue'; import { LeakPeriodInfo } from './LeakPeriodInfo'; -import MeasuresPanelIssueMeasureRow from './MeasuresPanelIssueMeasureRow'; +import MeasuresPanelIssueMeasure from './MeasuresPanelIssueMeasure'; import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode'; +import MeasuresPanelPercentMeasure from './MeasuresPanelPercentMeasure'; export interface MeasuresPanelProps { appLeak?: ApplicationPeriod; @@ -47,6 +54,7 @@ export interface MeasuresPanelProps { measures?: MeasureEnhanced[]; period?: Period; location: Location; + qgStatuses?: QualityGateStatus[]; } export enum MeasuresPanelTabs { @@ -55,13 +63,26 @@ export enum MeasuresPanelTabs { } export function MeasuresPanel(props: MeasuresPanelProps) { - const { appLeak, branch, component, loading, measures = [], period, location } = props; + const { + appLeak, + branch, + component, + loading, + measures = [], + period, + qgStatuses = [], + location, + } = props; const hasDiffMeasures = measures.some((m) => isDiffMetric(m.metric.key)); const isApp = component.qualifier === ComponentQualifier.Application; const leakPeriod = isApp ? appLeak : period; const query = parseQuery(location.query); + const { failingConditionsOnNewCode, failingConditionsOnOverallCode } = + countFailingConditions(qgStatuses); + const failingConditions = failingConditionsOnNewCode + failingConditionsOnOverallCode; + const [tab, selectTab] = React.useState(() => { return query.codeScope === CodeScope.Overall ? MeasuresPanelTabs.Overall @@ -82,120 +103,99 @@ export function MeasuresPanel(props: MeasuresPanelProps) { const tabs = [ { - key: MeasuresPanelTabs.New, - label: ( -
- {translate('overview.new_code')} - {leakPeriod && } -
- ), + value: MeasuresPanelTabs.New, + label: translate('overview.new_code'), + counter: failingConditionsOnNewCode, }, { - key: MeasuresPanelTabs.Overall, - label: ( -
- - {translate('overview.overall_code')} - -
- ), + value: MeasuresPanelTabs.Overall, + label: translate('overview.overall_code'), + counter: failingConditionsOnOverallCode, }, ]; return ( -
-
-

{translate('overview.measures')}

+
+
+

+ +

{loading ? ( -
+
) : ( <> - selectTab(key)} selected={tab} tabs={tabs} /> - -
- {!hasDiffMeasures && isNewCodeTab ? ( - - ) : ( - <> - {[ - IssueType.Bug, - IssueType.Vulnerability, - IssueType.SecurityHotspot, - IssueType.CodeSmell, - ].map((type: IssueType) => ( - + selectTab(key)} options={tabs} value={tab} /> + {failingConditions > 0 && ( + + {translateWithParameters('overview.X_conditions_failed', failingConditions)} + + )} +
+ + {tab === MeasuresPanelTabs.New && leakPeriod ? ( + + {translate('overview.new_code')}: + + + ) : ( +
+ )} + + {!hasDiffMeasures && isNewCodeTab ? ( + + ) : ( +
+ {[ + IssueType.Bug, + IssueType.CodeSmell, + IssueType.Vulnerability, + IssueType.SecurityHotspot, + ].map((type: IssueType) => ( + + - ))} - -
- {(findMeasure(measures, MetricKey.coverage) || - findMeasure(measures, MetricKey.new_coverage)) && ( -
- - - {tab === MeasuresPanelTabs.Overall && ( -
- -
- )} -
- )} -
- - - {tab === MeasuresPanelTabs.Overall && ( -
- -
- )} -
-
- - )} -
+ + ))} + + {(findMeasure(measures, MetricKey.coverage) || + findMeasure(measures, MetricKey.new_coverage)) && ( + + + + )} + + + + +
+ )} )}
@@ -203,3 +203,30 @@ export function MeasuresPanel(props: MeasuresPanelProps) { } export default withRouter(React.memo(MeasuresPanel)); + +function renderCoverageIcon(value?: string) { + return ; +} + +function renderDuplicationIcon(value?: string) { + const rating = value !== undefined ? duplicationRatingConverter(Number(value)) : undefined; + + return ; +} + +function countFailingConditions(qgStatuses: QualityGateStatus[]) { + let failingConditionsOnNewCode = 0; + let failingConditionsOnOverallCode = 0; + + qgStatuses.forEach(({ failedConditions }) => { + failedConditions.forEach((condition) => { + if (isDiffMetric(condition.metric)) { + failingConditionsOnNewCode += 1; + } else { + failingConditionsOnOverallCode += 1; + } + }); + }); + + return { failingConditionsOnNewCode, failingConditionsOnOverallCode }; +} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelCard.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelCard.tsx new file mode 100644 index 00000000000..ab42b7aa5fa --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelCard.tsx @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; + +interface Props { + category: React.ReactElement; + rating: React.ReactElement | null; +} + +export default function MeasuresPanelCard( + props: React.PropsWithChildren> +) { + const { category, children, rating, ...attributes } = props; + + return ( +
+
+
{category}
+ +
{children}
+
+ +
{rating}
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasure.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasure.tsx new file mode 100644 index 00000000000..0a2d6e11db0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasure.tsx @@ -0,0 +1,85 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { LightPrimary, ThemeColors } from 'design-system'; +import * as React from 'react'; +import { translate } from '../../../helpers/l10n'; +import { BranchLike } from '../../../types/branch-like'; +import { ComponentQualifier } from '../../../types/component'; +import { IssueType } from '../../../types/issues'; +import { Component, MeasureEnhanced } from '../../../types/types'; +import IssueLabel from '../components/IssueLabel'; +import IssueRating from '../components/IssueRating'; +import { getIssueIconClass, getIssueRatingName } from '../utils'; +import MeasuresPanelCard from './MeasuresPanelCard'; + +interface Props { + branchLike?: BranchLike; + component: Component; + isNewCodeTab: boolean; + measures: MeasureEnhanced[]; + type: IssueType; +} + +export default function MeasuresPanelIssueMeasure(props: Props) { + const { branchLike, component, isNewCodeTab, measures, type } = props; + + const isApp = component.qualifier === ComponentQualifier.Application; + + const IconClass = getIssueIconClass(type) as (args: { + className?: string; + fill?: ThemeColors; + }) => JSX.Element; + + return ( + + + {getIssueRatingName(type)} +
+ } + rating={ + !isApp || !isNewCodeTab ? ( + + ) : null + } + > + + + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasureRow.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasureRow.tsx deleted file mode 100644 index 4f6da6dd17e..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasureRow.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { translate } from '../../../helpers/l10n'; -import { BranchLike } from '../../../types/branch-like'; -import { ComponentQualifier } from '../../../types/component'; -import { IssueType } from '../../../types/issues'; -import { Component, MeasureEnhanced } from '../../../types/types'; -import IssueLabel from '../components/IssueLabel'; -import IssueRating from '../components/IssueRating'; -import DebtValue from './DebtValue'; -import SecurityHotspotsReviewed from './SecurityHotspotsReviewed'; - -export interface MeasuresPanelIssueMeasureRowProps { - branchLike?: BranchLike; - component: Component; - isNewCodeTab: boolean; - measures: MeasureEnhanced[]; - type: IssueType; -} - -export default function MeasuresPanelIssueMeasureRow(props: MeasuresPanelIssueMeasureRowProps) { - const { branchLike, component, isNewCodeTab, measures, type } = props; - - const isApp = component.qualifier === ComponentQualifier.Application; - - return ( -
- {type === IssueType.CodeSmell ? ( - <> -
- -
-
- -
- - ) : ( -
- -
- )} - {type === IssueType.SecurityHotspot && ( -
- -
- )} - {(!isApp || !isNewCodeTab) && ( -
- -
- )} -
- ); -} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasure.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasure.tsx new file mode 100644 index 00000000000..cf7c7a1e2f1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasure.tsx @@ -0,0 +1,142 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { DrilldownLink, GreySeparator, LightLabel, LightPrimary } from 'design-system'; +import * as React from 'react'; +import { getLeakValue } from '../../../components/measure/utils'; +import { isPullRequest } from '../../../helpers/branch-like'; +import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; +import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures'; +import { getComponentDrilldownUrl } from '../../../helpers/urls'; +import { BranchLike } from '../../../types/branch-like'; +import { MetricKey, MetricType } from '../../../types/metrics'; +import { Component, MeasureEnhanced } from '../../../types/types'; +import AfterMergeEstimate from '../pullRequests/AfterMergeEstimate'; +import { MeasurementType, getMeasurementMetricKey } from '../utils'; +import DrilldownMeasureValue from './DrilldownMeasureValue'; +import MeasuresPanelCard from './MeasuresPanelCard'; +import MeasuresPanelPercentMeasureLabel from './MeasuresPanelPercentMeasureLabel'; + +interface Props { + branchLike?: BranchLike; + component: Component; + useDiffMetric: boolean; + measures: MeasureEnhanced[]; + ratingIcon: (value: string | undefined) => React.ReactElement; + secondaryMetricKey?: MetricKey; + type: MeasurementType; +} + +export default function MeasuresPanelPercentMeasure(props: Props) { + const { + branchLike, + component, + measures, + ratingIcon, + secondaryMetricKey, + type, + useDiffMetric = false, + } = props; + const metricKey = getMeasurementMetricKey(type, useDiffMetric); + const measure = findMeasure(measures, metricKey); + + let value; + if (measure) { + value = useDiffMetric ? getLeakValue(measure) : measure.value; + } + + const url = getComponentDrilldownUrl({ + componentKey: component.key, + metric: metricKey, + branchLike, + listView: true, + }); + + const formattedValue = formatMeasure(value, MetricType.Percent, { + decimals: 2, + omitExtraDecimalZeros: true, + }); + + return ( + {translate('overview.measurement_type', type)}} + rating={ratingIcon(value)} + > + <> +
+ {value === undefined ? ( + — + ) : ( + + {formattedValue} + + )} + + + {translate('overview.measurement_type', type)} + +
+ + + {!useDiffMetric && secondaryMetricKey && ( + <> + +
+ + + {getLocalizedMetricName({ key: secondaryMetricKey })} + +
+ + )} + + {isPullRequest(branchLike) && ( + <> + +
+ + + {translate('component_measures.facet_category.overall_category.estimated')} + +
+ + )} + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasureLabel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasureLabel.tsx new file mode 100644 index 00000000000..e65e832c454 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasureLabel.tsx @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { DrilldownLink, LightLabel } from 'design-system'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { getLeakValue } from '../../../components/measure/utils'; +import { translate } from '../../../helpers/l10n'; +import { findMeasure, formatMeasure } from '../../../helpers/measures'; +import { getComponentDrilldownUrl } from '../../../helpers/urls'; +import { BranchLike } from '../../../types/branch-like'; +import { MetricType } from '../../../types/metrics'; +import { Component, MeasureEnhanced } from '../../../types/types'; +import { MeasurementType, getMeasurementLabelKeys, getMeasurementLinesMetricKey } from '../utils'; + +interface Props { + branchLike?: BranchLike; + component: Component; + useDiffMetric: boolean; + measures: MeasureEnhanced[]; + type: MeasurementType; +} + +export default function MeasuresPanelPercentMeasureLabel(props: Props) { + const { branchLike, component, measures, type, useDiffMetric = false } = props; + const { expandedLabelKey, labelKey } = getMeasurementLabelKeys(type, useDiffMetric); + const linesMetric = getMeasurementLinesMetricKey(type, useDiffMetric); + const measure = findMeasure(measures, linesMetric); + + if (!measure) { + return {translate(labelKey)}; + } + + const value = useDiffMetric ? getLeakValue(measure) : measure.value; + + const url = getComponentDrilldownUrl({ + componentKey: component.key, + metric: linesMetric, + branchLike, + listView: true, + }); + + return ( + + + {formatMeasure(value, MetricType.ShortInteger)} + + ), + }} + /> + + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx b/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx index ec76ddfafde..4a9025ba54c 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx @@ -52,7 +52,7 @@ export function ProjectLeakPeriodInfo(props: ProjectLeakPeriodInfoProps) { leakPeriod.mode === NewCodePeriodSettingType.NUMBER_OF_DAYS || leakPeriod.mode === NewCodePeriodSettingType.REFERENCE_BRANCH ) { - return
{leakPeriodLabel}
; + return
{leakPeriodLabel}
; } const leakPeriodDate = getPeriodDate(leakPeriod); @@ -63,20 +63,19 @@ export function ProjectLeakPeriodInfo(props: ProjectLeakPeriodInfoProps) { return ( <> -
+
{leakPeriodLabel}
+ - {(fromNow) => ( -
- {translateWithParameters( - leakPeriod.mode === 'previous_analysis' - ? 'overview.previous_analysis_x' - : 'overview.started_x', - fromNow - )} -
- )} + {(fromNow) => + translateWithParameters( + leakPeriod.mode === 'previous_analysis' + ? 'overview.previous_analysis_x' + : 'overview.started_x', + fromNow + ) + }
); diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx index 1aae810e9a1..93f8b78d687 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx @@ -236,7 +236,7 @@ describe('project overview', () => { renderBranchOverview(); expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument(); - expect(screen.getByText('overview.X_conditions_failed.2')).toBeInTheDocument(); + expect(screen.getAllByText('overview.X_conditions_failed.2')).toHaveLength(2); }); it('should correctly show a project as empty', async () => { diff --git a/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx b/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx index 90a3c876f1d..511cf069cb2 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { DrilldownLink, HelperHintIcon, LightLabel } from 'design-system'; import * as React from 'react'; -import Link from '../../../components/common/Link'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { getLeakValue } from '../../../components/measure/utils'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; @@ -27,8 +27,9 @@ import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/mea import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; import { IssueType } from '../../../types/issues'; +import { MetricType } from '../../../types/metrics'; import { Component, MeasureEnhanced } from '../../../types/types'; -import { getIssueIconClass, getIssueMetricKey } from '../utils'; +import { getIssueMetricKey } from '../utils'; export interface IssueLabelProps { branchLike?: BranchLike; @@ -43,7 +44,6 @@ export function IssueLabel(props: IssueLabelProps) { const { branchLike, component, helpTooltip, measures, type, useDiffMetric = false } = props; const metricKey = getIssueMetricKey(type, useDiffMetric); const measure = findMeasure(measures, metricKey); - const iconClass = getIssueIconClass(type); let value; if (measure) { @@ -63,26 +63,29 @@ export function IssueLabel(props: IssueLabelProps) { : getComponentIssuesUrl(component.key, params); return ( - <> +
{value === undefined ? ( - + — ) : ( - - {formatMeasure(value, 'SHORT_INT')} - + {formatMeasure(value, MetricType.ShortInteger)} + )} - {React.createElement(iconClass, { className: 'big-spacer-left little-spacer-right' })} - {localizeMetric(metricKey)} - {helpTooltip && } - + {localizeMetric(metricKey)} + {helpTooltip && ( + + + + )} +
); } diff --git a/server/sonar-web/src/main/js/apps/overview/components/IssueRating.tsx b/server/sonar-web/src/main/js/apps/overview/components/IssueRating.tsx index c1dbf140e59..971237adb02 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/IssueRating.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/IssueRating.tsx @@ -19,17 +19,18 @@ */ /* eslint-disable react/no-unused-prop-types */ +import { DiscreetLinkBox, MetricsRatingBadge } from 'design-system'; import * as React from 'react'; import Tooltip from '../../../components/controls/Tooltip'; import RatingTooltipContent from '../../../components/measure/RatingTooltipContent'; import { getLeakValue } from '../../../components/measure/utils'; -import DrilldownLink from '../../../components/shared/DrilldownLink'; -import Rating from '../../../components/ui/Rating'; -import { findMeasure } from '../../../helpers/measures'; +import { translateWithParameters } from '../../../helpers/l10n'; +import { findMeasure, formatRating } from '../../../helpers/measures'; +import { getComponentDrilldownUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; import { IssueType } from '../../../types/issues'; import { Component, MeasureEnhanced } from '../../../types/types'; -import { getIssueRatingMetricKey, getIssueRatingName } from '../utils'; +import { getIssueRatingMetricKey } from '../utils'; export interface IssueRatingProps { branchLike?: BranchLike; @@ -39,46 +40,45 @@ export interface IssueRatingProps { useDiffMetric?: boolean; } -function renderRatingLink(props: IssueRatingProps) { +export function IssueRating(props: IssueRatingProps) { const { branchLike, component, useDiffMetric = false, measures, type } = props; - const rating = getIssueRatingMetricKey(type, useDiffMetric); - const measure = findMeasure(measures, rating); + const ratingKey = getIssueRatingMetricKey(type, useDiffMetric); + const measure = findMeasure(measures, ratingKey); + const rawValue = measure && (useDiffMetric ? getLeakValue(measure) : measure.value); + const value = formatRating(rawValue); - if (!rating || !measure) { - return ( -
- -
- ); + if (!ratingKey || !measure) { + return ; } - const value = measure && (useDiffMetric ? getLeakValue(measure) : measure.value); - return ( - }> + }> - - - + {value ? ( + + + + ) : ( + + )} ); } -export function IssueRating(props: IssueRatingProps) { - const { type } = props; +export default IssueRating; - return ( - <> - {getIssueRatingName(type)} - {renderRatingLink(props)} - - ); +function NoRating() { + return
–
; } - -export default React.memo(IssueRating); diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx index 55010a5a83b..c5078fe5e4d 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx @@ -26,13 +26,15 @@ import { translate } from '../../../helpers/l10n'; export function QualityGateStatusTitle() { return (
- - {translate('overview.quality_gate.help')}
} - > - - +

+ + {translate('overview.quality_gate.help')}

} + > + + +
); } diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueLabel-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueLabel-test.tsx index 8b349a7ae4d..6d2de23747f 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueLabel-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueLabel-test.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../../helpers/mocks/component'; import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks'; -import { findTooltipWithContent, renderComponent } from '../../../../helpers/testReactTestingUtils'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { IssueType } from '../../../../types/issues'; import { MetricKey } from '../../../../types/metrics'; import { IssueLabel, IssueLabelProps } from '../IssueLabel'; @@ -71,7 +71,7 @@ it('should render correctly for hotspots with tooltip', async () => { }) ).toBeInTheDocument(); - expect(findTooltipWithContent('tooltip text')).toBeInTheDocument(); + expect(screen.getByText('tooltip text')).toBeInTheDocument(); }); function renderIssueLabel(props: Partial = {}) { diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueRating-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueRating-test.tsx index 31a12a7fcfc..c10092ce098 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueRating-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueRating-test.tsx @@ -28,21 +28,16 @@ import { MetricKey } from '../../../../types/metrics'; import { IssueRating, IssueRatingProps } from '../IssueRating'; it('should render correctly for vulnerabilities', async () => { - renderIssueRating({ type: IssueType.Vulnerability }); - expect(await screen.findByText('metric_domain.Security')).toBeInTheDocument(); - renderIssueRating({ type: IssueType.Vulnerability, useDiffMetric: true }); - const labels = await screen.findAllByText('metric_domain.Security'); - expect(labels).toHaveLength(2); - const tooltips = await screen.findAllByText('metric.security_rating.tooltip.A'); - expect(tooltips).toHaveLength(2); + expect(await screen.findByLabelText('metric.has_rating_X.A')).toBeInTheDocument(); + expect(await screen.findByText('metric.security_rating.tooltip.A')).toBeInTheDocument(); }); it('should render correctly if no values are present', async () => { renderIssueRating({ measures: [mockMeasureEnhanced({ metric: mockMetric({ key: 'NONE' }) })], }); - expect(await screen.findByText('metric_domain.Reliability')).toBeInTheDocument(); + expect(await screen.findByText('–')).toBeInTheDocument(); }); function renderIssueRating(props: Partial = {}) { 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 index c2051bc1b80..fa91067a93d 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx @@ -18,11 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { LightPrimary } from 'design-system'; import * as React from 'react'; -import { translate } from '../../../helpers/l10n'; import { findMeasure, formatMeasure } from '../../../helpers/measures'; +import { MetricType } from '../../../types/metrics'; import { MeasureEnhanced } from '../../../types/types'; -import { getMeasurementAfterMergeMetricKey, MeasurementType } from '../utils'; +import { MeasurementType, getMeasurementAfterMergeMetricKey } from '../utils'; export interface AfterMergeEstimateProps { className?: string; @@ -40,11 +41,10 @@ export function AfterMergeEstimate({ className, measures, type }: AfterMergeEsti } return ( -
- {formatMeasure(measure.value, 'PERCENT')} - - {translate('component_measures.facet_category.overall_category.estimated')} - +
+ + {formatMeasure(measure.value, MetricType.Percent)} +
); } diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx index 03717947f1d..2a2ce4b34d0 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx @@ -20,10 +20,13 @@ import { BasicSeparator, Card, + CoverageIndicator, DeferredSpinner, + DuplicationsIndicator, HelperHintIcon, LargeCenteredLayout, Link, + PageTitle, TextMuted, } from 'design-system'; import { differenceBy, uniq } from 'lodash'; @@ -34,6 +37,7 @@ import { BranchStatusContextInterface } from '../../../app/components/branch-sta import withBranchStatus from '../../../app/components/branch-status/withBranchStatus'; import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; import HelpTooltip from '../../../components/controls/HelpTooltip'; +import { duplicationRatingConverter } from '../../../components/measure/utils'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures'; @@ -42,10 +46,9 @@ import { getQualityGateUrl, getQualityGatesUrl } from '../../../helpers/urls'; import { BranchStatusData, PullRequest } from '../../../types/branch-like'; import { IssueType } from '../../../types/issues'; import { Component, MeasureEnhanced } from '../../../types/types'; +import MeasuresPanelIssueMeasure from '../branches/MeasuresPanelIssueMeasure'; +import MeasuresPanelPercentMeasure from '../branches/MeasuresPanelPercentMeasure'; import IgnoredConditionWarning from '../components/IgnoredConditionWarning'; -import IssueLabel from '../components/IssueLabel'; -import IssueRating from '../components/IssueRating'; -import MeasurementLabel from '../components/MeasurementLabel'; import QualityGateConditions from '../components/QualityGateConditions'; import QualityGateStatusHeader from '../components/QualityGateStatusHeader'; import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView'; @@ -53,7 +56,6 @@ import { QualityGateStatusTitle } from '../components/QualityGateStatusTitle'; import SonarLintPromotion from '../components/SonarLintPromotion'; import '../styles.css'; import { MeasurementType, PR_METRICS } from '../utils'; -import AfterMergeEstimate from './AfterMergeEstimate'; interface Props extends BranchStatusData, Pick { branchLike: PullRequest; @@ -228,59 +230,41 @@ export class PullRequestOverview extends React.PureComponent {
-
-

- {translate('overview.measures')} +
+

+

-
+
{[ IssueType.Bug, IssueType.Vulnerability, IssueType.SecurityHotspot, IssueType.CodeSmell, ].map((type: IssueType) => ( -
-
- -
-
- -
-
+ + + ))} {[MeasurementType.Coverage, MeasurementType.Duplication].map( (type: MeasurementType) => ( -
-
- -
- - + -
+ ) )}
@@ -293,3 +277,17 @@ export class PullRequestOverview extends React.PureComponent { } export default withBranchStatus(withBranchStatusActions(PullRequestOverview)); + +function renderMeasureIcon(type: MeasurementType) { + if (type === MeasurementType.Coverage) { + return function CoverageIndicatorRenderer(value?: string) { + return ; + }; + } + + return function renderDuplicationIcon(value?: string) { + const rating = duplicationRatingConverter(Number(value)); + + return ; + }; +} diff --git a/server/sonar-web/src/main/js/helpers/issues.ts b/server/sonar-web/src/main/js/helpers/issues.ts index ca963e3ebd1..2e970d123da 100644 --- a/server/sonar-web/src/main/js/helpers/issues.ts +++ b/server/sonar-web/src/main/js/helpers/issues.ts @@ -17,11 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { BugIcon, CodeSmellIcon, SecurityHotspotIcon, VulnerabilityIcon } from 'design-system'; import { flatten, sortBy } from 'lodash'; -import BugIcon from '../components/icons/BugIcon'; -import CodeSmellIcon from '../components/icons/CodeSmellIcon'; -import SecurityHotspotIcon from '../components/icons/SecurityHotspotIcon'; -import VulnerabilityIcon from '../components/icons/VulnerabilityIcon'; import { IssueType, RawIssue } from '../types/issues'; import { MetricKey } from '../types/metrics'; import { Dict, Flow, FlowLocation, Issue, TextRange } from '../types/types'; diff --git a/server/sonar-web/src/main/js/helpers/measures.ts b/server/sonar-web/src/main/js/helpers/measures.ts index 87fdab13db1..63ed58f4863 100644 --- a/server/sonar-web/src/main/js/helpers/measures.ts +++ b/server/sonar-web/src/main/js/helpers/measures.ts @@ -78,7 +78,10 @@ interface Formatter { (value: string | number, options?: any): string; } -/** Format a measure value for a given type */ +/** + * Format a measure value for a given type + * ! For Ratings, use formatRating instead + */ export function formatMeasure( value: string | number | undefined, type: string, @@ -89,6 +92,21 @@ export function formatMeasure( return useFormatter(value, formatter, options); } +type RatingValue = 'A' | 'B' | 'C' | 'D' | 'E'; +const RATING_VALUES: RatingValue[] = ['A', 'B', 'C', 'D', 'E']; +export function formatRating(value: string | number | undefined): RatingValue | undefined { + if (!value) { + return undefined; + } + + if (typeof value === 'string') { + value = parseInt(value, 10); + } + + // rating is 1-5, adjust for 0-based indexing + return RATING_VALUES[value - 1]; +} + /** Return a localized metric name */ export function localizeMetric(metricKey: string): string { return translate('metric', metricKey, 'name'); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 386ab30be8b..f55eae17eaa 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3476,8 +3476,8 @@ overview.gate.view.no_alert=The view has passed the quality gate. overview.gate.view.warnings=The view has warnings on the following quality gate conditions: {0}. overview.gate.view.errors=The view failed the quality gate on the following conditions: {0}. -overview.domain.duplications=Duplications -overview.domain.size=Size +overview.measurement_type.DUPLICATION=Duplications +overview.measurement_type.COVERAGE=Coverage overview.complexity_tooltip.function={0} functions have complexity around {1} overview.complexity_tooltip.file={0} files have complexity around {1} -- 2.39.5