From c6dc867d4a257ece961d8552a073b931e8540fb9 Mon Sep 17 00:00:00 2001 From: Revanshu Paliwal Date: Wed, 19 Apr 2023 11:33:28 +0200 Subject: [PATCH] SONAR-19018 Migrating project quality gate section to MIUI --- .../src/components/Accordion.tsx | 4 +- .../design-system/src/components/Link.tsx | 12 ++ .../src/components/Separator.tsx | 40 +++++ .../design-system/src/components/Text.tsx | 25 +++ .../design-system/src/components/buttons.tsx | 4 + .../design-system/src/components/index.ts | 1 + .../branches/BranchOverviewRenderer.tsx | 8 +- .../overview/branches/QualityGatePanel.tsx | 130 ++++++-------- .../branches/QualityGatePanelSection.tsx | 166 ++++++++++-------- .../branches/__tests__/BranchOverview-it.tsx | 4 +- .../components/IgnoredConditionWarning.tsx | 42 +++++ .../components/QualityGateCondition.tsx | 124 +++++++------ .../components/QualityGateConditions.tsx | 40 ++--- .../components/QualityGateStatusHeader.tsx | 54 ++++++ .../QualityGateStatusPassedView.tsx | 32 ++++ .../components/QualityGateStatusTitle.tsx | 38 ++++ .../components/SonarLintPromotion.tsx | 12 +- .../__tests__/QualityGateCondition-test.tsx | 8 +- .../__tests__/QualityGateConditions-test.tsx | 2 +- .../pullRequests/LargeQualityGateBadge.tsx | 76 -------- .../pullRequests/PullRequestOverview.tsx | 127 ++++++++------ .../__tests__/PullRequestOverview-it.tsx | 7 +- .../src/main/js/apps/overview/styles.css | 97 ---------- .../main/js/components/measure/Measure.tsx | 4 +- .../components/measure/MeasureIndicator.tsx | 73 ++++++++ .../__tests__/MeasureIndicator-test.tsx | 30 ++++ .../MeasureIndicator-test.tsx.snap | 27 +++ .../measure/__tests__/utils-test.tsx | 33 ++++ .../src/main/js/components/measure/utils.ts | 19 ++ .../js/components/shared/DrilldownLink.tsx | 78 +------- .../shared/__tests__/DrilldownLink-test.tsx | 15 -- .../components/shared/__tests__/utils-test.ts | 35 ++++ .../src/main/js/components/shared/utils.ts | 86 +++++++++ .../resources/org/sonar/l10n/core.properties | 10 +- 34 files changed, 889 insertions(+), 574 deletions(-) create mode 100644 server/sonar-web/design-system/src/components/Separator.tsx create mode 100644 server/sonar-web/src/main/js/apps/overview/components/IgnoredConditionWarning.tsx create mode 100644 server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusHeader.tsx create mode 100644 server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusPassedView.tsx create mode 100644 server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx delete mode 100644 server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx create mode 100644 server/sonar-web/src/main/js/components/measure/MeasureIndicator.tsx create mode 100644 server/sonar-web/src/main/js/components/measure/__tests__/MeasureIndicator-test.tsx create mode 100644 server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/MeasureIndicator-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/measure/__tests__/utils-test.tsx create mode 100644 server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts create mode 100644 server/sonar-web/src/main/js/components/shared/utils.ts diff --git a/server/sonar-web/design-system/src/components/Accordion.tsx b/server/sonar-web/design-system/src/components/Accordion.tsx index d1af31d9096..9dcfb92ab46 100644 --- a/server/sonar-web/design-system/src/components/Accordion.tsx +++ b/server/sonar-web/design-system/src/components/Accordion.tsx @@ -24,6 +24,7 @@ import { BareButton } from './buttons'; import { OpenCloseIndicator } from './icons/OpenCloseIndicator'; interface AccordionProps { + ariaLabel?: string; children: React.ReactNode; className?: string; data?: string; @@ -33,7 +34,7 @@ interface AccordionProps { } export function Accordion(props: AccordionProps) { - const { className, open, header, data, onClick } = props; + const { ariaLabel, className, open, header, data, onClick } = props; const id = React.useMemo(() => uniqueId('accordion-'), []); const handleClick = React.useCallback(() => { @@ -50,6 +51,7 @@ export function Accordion(props: AccordionProps) { + {text} + + ); +} + +export function TextError({ text, className }: { className?: string; text: string }) { + return ( + + {text} + + ); +} + export const StyledText = styled.span` ${tw`sw-inline-block`}; ${tw`sw-truncate`}; @@ -68,3 +84,12 @@ const StyledMutedText = styled(StyledText)` ${tw`sw-font-regular`}; color: ${themeColor('dropdownMenuSubTitle')}; `; + +const StyledPageTitle = styled(StyledText)` + ${tw`sw-text-base`} + color: ${themeColor('facetHeader')}; +`; + +const StyledTextError = styled(StyledText)` + color: ${themeColor('danger')}; +`; diff --git a/server/sonar-web/design-system/src/components/buttons.tsx b/server/sonar-web/design-system/src/components/buttons.tsx index b939dbbe4c0..13304f9215c 100644 --- a/server/sonar-web/design-system/src/components/buttons.tsx +++ b/server/sonar-web/design-system/src/components/buttons.tsx @@ -216,4 +216,8 @@ const ThirdPartyButtonStyled: React.FC = styled(Button)` export const BareButton = styled.button` all: unset; cursor: pointer; + + &:focus-visible { + background-color: ${themeColor('dropdownMenuHover')}; + } `; diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index cda65984092..c4c52b9436a 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -42,6 +42,7 @@ export * from './MetricsRatingBadge'; export * from './NavBarTabs'; export * from './NewCodeLegend'; export { QualityGateIndicator } from './QualityGateIndicator'; +export * from './Separator'; export * from './SizeIndicator'; export * from './SonarQubeLogo'; export * from './Text'; 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 5cc97dbd2b6..3b194749b6a 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 @@ -92,8 +92,8 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp {projectIsEmpty ? ( ) : ( -
-
+
+
-
-
+
+
Boolean(p.ignoredConditions)); return ( -
-
-

- {translate('overview.quality_gate')}{' '} -

- - {translate('overview.quality_gate.help')} +
+ + +
+ {loading ? ( +
+
- } - /> -
- {showIgnoredConditionWarning && ( - - - {translate('overview.quality_gate.ignored_conditions')} - - - - )} + ) : ( + <> + + {success && } -
- {loading ? ( -
- -
- ) : ( - <> -
-
- {translate('metric.level', overallLevel)} -
+ {showIgnoredConditionWarning && } - - {overallFailedConditionsCount > 0 - ? translateWithParameters( - 'overview.X_conditions_failed', - overallFailedConditionsCount - ) - : translate('overview.quality_gate_all_conditions_passed')} - -
+ {!success && } - {(overallFailedConditionsCount > 0 || - qgStatuses.some(({ caycStatus }) => caycStatus !== CaycStatus.Compliant)) && ( -
- {qgStatuses.map((qgStatus) => ( - - ))} -
- )} + {(overallFailedConditionsCount > 0 || + qgStatuses.some(({ caycStatus }) => caycStatus !== CaycStatus.Compliant)) && ( +
+ {qgStatuses.map((qgStatus) => ( + + ))} +
+ )} + + )} +
+
- {nonCaycProjectsInApp.length > 0 && ( - - )} + {nonCaycProjectsInApp.length > 0 && ( + + )} - {overCompliantCaycProjectsInApp.length > 0 && ( - - )} - - )} -
+ {overCompliantCaycProjectsInApp.length > 0 && ( + + )} qgStatus.failedConditions)} /> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx index faedae61120..d4ceaf05679 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx @@ -17,11 +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 { Accordion, BasicSeparator, TextMuted } from 'design-system'; import * as React from 'react'; -import { ButtonPlain } from '../../../components/controls/buttons'; -import ChevronDownIcon from '../../../components/icons/ChevronDownIcon'; -import ChevronRightIcon from '../../../components/icons/ChevronRightIcon'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translateWithParameters } from '../../../helpers/l10n'; import { isDiffMetric } from '../../../helpers/measures'; import { BranchLike } from '../../../types/branch-like'; import { isApplication } from '../../../types/component'; @@ -57,19 +55,6 @@ function splitConditions( return [newCodeFailedConditions, overallFailedConditions]; } -function displayConditions(conditions: number) { - if (conditions === 0) { - return null; - } - - const text = - conditions === 1 - ? translate('overview.1_condition_failed') - : translateWithParameters('overview.X_conditions_failed', conditions); - - return {text}; -} - export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { const { component, qgStatus } = props; const [collapsed, setCollapsed] = React.useState(false); @@ -96,7 +81,7 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { qgStatus.failedConditions ); - const showName = isApplication(component.qualifier); + const collapsible = isApplication(component.qualifier); const showSectionTitles = isApplication(component.qualifier) || @@ -107,82 +92,107 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { ? translateWithParameters('overview.quality_gate.show_project_conditions_x', qgStatus.name) : translateWithParameters('overview.quality_gate.hide_project_conditions_x', qgStatus.name); - return ( -
- {showName && ( - -
-
- {collapsed ? : } - {qgStatus.name} -
- {collapsed && displayConditions(qgStatus.failedConditions.length)} -
-
- )} + const renderFailedConditions = () => { + return ( + <> + {newCodeFailedConditions.length > 0 && ( + <> + {showSectionTitles && ( + <> +

+ {translateWithParameters( + 'quality_gates.conditions.new_code_x', + newCodeFailedConditions.length.toString() + )} +

+ + + )} + + + )} + + {overallFailedConditions.length > 0 && ( + <> + {showSectionTitles && ( + <> +

+ {translateWithParameters( + 'quality_gates.conditions.overall_code_x', + overallFailedConditions.length.toString() + )} +

+ + + )} + + + )} + + ); + }; - {!collapsed && ( + return ( + <> + {collapsible ? ( + <> + + {qgStatus.name} + {collapsed && newCodeFailedConditions.length > 0 && ( + + )} + {collapsed && overallFailedConditions.length > 0 && ( + + )} +
+ } + > + + {renderFailedConditions()} + + + + ) : ( <> + {renderFailedConditions()} {qgStatus.caycStatus === CaycStatus.NonCompliant && !isApplication(component.qualifier) && (
)} - {qgStatus.caycStatus === CaycStatus.OverCompliant && !isApplication(component.qualifier) && (
)} - - {newCodeFailedConditions.length > 0 && ( - <> - {showSectionTitles && ( -
- {translateWithParameters( - 'quality_gates.conditions.new_code_x', - newCodeFailedConditions.length.toString() - )} -
- )} - - - )} - - {overallFailedConditions.length > 0 && ( - <> - {showSectionTitles && ( -
- {translateWithParameters( - 'quality_gates.conditions.overall_code_x', - overallFailedConditions.length.toString() - )} -
- )} - - - )} )} -
+ ); } 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 a96481a7a59..1aae810e9a1 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 @@ -205,7 +205,7 @@ describe('project overview', () => { // QG panel expect(await screen.findByText('metric.level.OK')).toBeInTheDocument(); - expect(screen.getByText('overview.quality_gate_all_conditions_passed')).toBeInTheDocument(); + expect(screen.getByText('overview.passed.clean_code')).toBeInTheDocument(); expect( screen.queryByText('overview.quality_gate.conditions.cayc.warning') ).not.toBeInTheDocument(); @@ -342,7 +342,7 @@ it.each([ renderBranchOverview(); // wait for loading - await screen.findByText('overview.quality_gate'); + await screen.findByText('overview.quality_gate.status'); expect(screen.queryByText('overview.project.next_steps.set_up_ci') === null).toBe(expected); } diff --git a/server/sonar-web/src/main/js/apps/overview/components/IgnoredConditionWarning.tsx b/server/sonar-web/src/main/js/apps/overview/components/IgnoredConditionWarning.tsx new file mode 100644 index 00000000000..a3055cf6e9f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/IgnoredConditionWarning.tsx @@ -0,0 +1,42 @@ +/* + * 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 { FlagMessage, HelperHintIcon } from 'design-system'; +import React from 'react'; +import HelpTooltip from '../../../components/controls/HelpTooltip'; +import { translate } from '../../../helpers/l10n'; + +export default function IgnoredConditionWarning() { + return ( + + {translate('overview.quality_gate.ignored_conditions')} + + + + + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx index 8e6f8bd1f5a..73476f95598 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx @@ -17,20 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; +import { LinkBox, TextMuted } from 'design-system'; import * as React from 'react'; import { Path } from 'react-router-dom'; -import Link from '../../../components/common/Link'; import IssueTypeIcon from '../../../components/icons/IssueTypeIcon'; -import Measure from '../../../components/measure/Measure'; -import DrilldownLink from '../../../components/shared/DrilldownLink'; +import MeasureIndicator from '../../../components/measure/MeasureIndicator'; +import { isIssueMeasure, propsToIssueParams } from '../../../components/shared/utils'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures'; -import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl } from '../../../helpers/urls'; +import { + getComponentDrilldownUrl, + getComponentIssuesUrl, + getComponentSecurityHotspotsUrl, +} from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; import { IssueType } from '../../../types/issues'; -import { MetricKey } from '../../../types/metrics'; +import { MetricKey, MetricType } from '../../../types/metrics'; import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; import { Component, Dict } from '../../../types/types'; @@ -87,11 +90,6 @@ export default class QualityGateCondition extends React.PureComponent { wrapWithLink(children: React.ReactNode) { const { branchLike, component, condition } = this.props; - const className = classNames( - 'overview-quality-gate-condition', - `overview-quality-gate-condition-${condition.level.toLowerCase()}` - ); - const metricKey = condition.measure.metric.key; const METRICS_TO_URL_MAPPING: Dict<() => Path> = { @@ -109,67 +107,83 @@ export default class QualityGateCondition extends React.PureComponent { [MetricKey.new_security_hotspots_reviewed]: () => this.getUrlForSecurityHotspot(true), }; - return ( -
  • - {METRICS_TO_URL_MAPPING[metricKey] ? ( - - {children} - - ) : ( - - {children} - - )} -
  • - ); + if (METRICS_TO_URL_MAPPING[metricKey]) { + return {children}; + } + + if (isIssueMeasure(condition.measure.metric.key)) { + const url = getComponentIssuesUrl(component.key, { + ...propsToIssueParams(condition.measure.metric.key, condition.period != null), + ...getBranchLikeQuery(branchLike), + }); + + return {children}; + } + + const url = getComponentDrilldownUrl({ + componentKey: component.key, + metric: condition.measure.metric.key, + branchLike, + listView: true, + }); + + return {children}; } - render() { + getPrimaryText = () => { const { condition } = this.props; const { measure } = condition; const { metric } = measure; - const isDiff = isDiffMetric(metric.key); + const subText = + !isDiff && condition.period != null + ? `${localizeMetric(metric.key)} ${translate('quality_gates.conditions.new_code')}` + : localizeMetric(metric.key); + + if (metric.type !== MetricType.Rating) { + const actual = (condition.period ? measure.period?.value : measure.value) as string; + const formattedValue = formatMeasure(actual, metric.type, { + decimal: 2, + omitExtraDecimalZeros: metric.type === MetricType.Percent, + }); + return `${formattedValue} ${subText}`; + } + + return subText; + }; + + render() { + const { condition } = this.props; + const { measure } = condition; + const { metric } = measure; + const threshold = (condition.level === 'ERROR' ? condition.error : condition.warning) as string; const actual = (condition.period ? measure.period?.value : measure.value) as string; let operator = translate('quality_gates.operator', condition.op); - if (metric.type === 'RATING') { + if (metric.type === MetricType.Rating) { operator = translate('quality_gates.operator', condition.op, 'rating'); } return this.wrapWithLink( -
    -
    - -
    - -
    - - - {localizeMetric(metric.key)} - - {!isDiff && condition.period != null && ( - - {translate('quality_gates.conditions.new_code')} +
    + +
    +
    + + + {this.getPrimaryText()} - )} - - {operator} {formatMeasure(threshold, metric.type)} - +
    +
    ); diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx index c9e52c9283c..0fe8c6ded69 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx @@ -17,11 +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 { BasicSeparator, Link } from 'design-system'; import { sortBy } from 'lodash'; import * as React from 'react'; -import { ButtonLink } from '../../../components/controls/buttons'; -import ChevronDownIcon from '../../../components/icons/ChevronDownIcon'; -import { translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; import { BranchLike } from '../../../types/branch-like'; import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; import { Component } from '../../../types/types'; @@ -50,6 +49,7 @@ export function QualityGateConditions(props: QualityGateConditionsProps) { let renderConditions; let renderCollapsed; + if (collapsed && sortedConditions.length > MAX_CONDITIONS) { renderConditions = sortedConditions.slice(0, MAX_CONDITIONS); renderCollapsed = true; @@ -59,30 +59,22 @@ export function QualityGateConditions(props: QualityGateConditionsProps) { } return ( -
      +
        {renderConditions.map((condition) => ( - +
        + + +
        ))} {renderCollapsed && ( -
      • - - {translateWithParameters( - 'overview.X_more_failed_conditions', - sortedConditions.length - MAX_CONDITIONS - )} - - +
      • + + {translate('show_more')} +
      • )}
      diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusHeader.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusHeader.tsx new file mode 100644 index 00000000000..bfe09a00d97 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusHeader.tsx @@ -0,0 +1,54 @@ +/* + * 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 { QualityGateIndicator, TextError, TextMuted } from 'design-system'; +import React from 'react'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { Status } from '../../../types/types'; + +interface Props { + status: Status; + failedConditionCount: number; +} + +export default function QualityGateStatusHeader(props: Props) { + const { status, failedConditionCount } = props; + + return ( +
      + +
      +
      + +
      +
      + {translate('metric.level', status)} +
      +
      +
      + {failedConditionCount > 0 && ( + + )} +
      +
      + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusPassedView.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusPassedView.tsx new file mode 100644 index 00000000000..bc8fe37a5da --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusPassedView.tsx @@ -0,0 +1,32 @@ +/* + * 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 { OverviewQGPassedIcon } from 'design-system'; +import React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default function QualityGateStatusPassedView() { + return ( +
      + +

      {translate('overview.passed.clean_code')}

      +
      + ); +} 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 new file mode 100644 index 00000000000..55010a5a83b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx @@ -0,0 +1,38 @@ +/* + * 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 { HelperHintIcon, PageTitle } from 'design-system'; +import React from 'react'; +import HelpTooltip from '../../../components/controls/HelpTooltip'; +import { translate } from '../../../helpers/l10n'; + +export function QualityGateStatusTitle() { + return ( +
      + + {translate('overview.quality_gate.help')}
      } + > + + +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/SonarLintPromotion.tsx b/server/sonar-web/src/main/js/apps/overview/components/SonarLintPromotion.tsx index b28c0324f0a..67a41dc2c34 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/SonarLintPromotion.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/SonarLintPromotion.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 { Card, DiscreetLink } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; @@ -55,26 +56,27 @@ export function SonarLintPromotion({ currentUser, qgConditions }: SonarLintPromo return null; } return ( -
    + - SonarLint - + ), }} /> -
    + ); } diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateCondition-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateCondition-test.tsx index e7d521b6a8c..979b58c8976 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateCondition-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateCondition-test.tsx @@ -23,12 +23,11 @@ import { mockBranch } from '../../../../helpers/mocks/branch-like'; import { mockQualityGateStatusConditionEnhanced } from '../../../../helpers/mocks/quality-gates'; import { mockMetric } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; -import { MetricKey } from '../../../../types/metrics'; +import { MetricKey, MetricType } from '../../../../types/metrics'; import { QualityGateStatusConditionEnhanced } from '../../../../types/quality-gates'; import QualityGateCondition from '../QualityGateCondition'; it.each([ - [quickMock(MetricKey.open_issues, 'INT')], [quickMock(MetricKey.reliability_rating)], [quickMock(MetricKey.security_rating)], [quickMock(MetricKey.sqale_rating)], @@ -51,6 +50,11 @@ it.each([ // } }); +it('should show the count when metric is not rating', async () => { + renderQualityGateCondition({ condition: quickMock(MetricKey.open_issues, MetricType.Integer) }); + expect(await screen.findByText('3 metric.open_issues.name')).toBeInTheDocument(); +}); + it('should work with branch', async () => { const condition = quickMock(MetricKey.new_maintainability_rating); renderQualityGateCondition({ branchLike: mockBranch(), condition }); diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateConditions-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateConditions-test.tsx index ef967ebf969..ed4fd8bb5b5 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateConditions-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateConditions-test.tsx @@ -48,7 +48,7 @@ it('should be collapsible', async () => { HALF_CONDITIONS ); - await user.click(screen.getByRole('button', { name: 'overview.X_more_failed_conditions.5' })); + await user.click(screen.getByRole('link', { name: 'show_more' })); expect(await screen.findAllByText(/.*metric..+.name.*/)).toHaveLength(ALL_CONDITIONS); expect(await screen.findAllByText('quality_gates.operator', { exact: false })).toHaveLength( diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx deleted file mode 100644 index 95802c86f0f..00000000000 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx +++ /dev/null @@ -1,76 +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 classNames from 'classnames'; -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { colors } from '../../../app/theme'; -import Link from '../../../components/common/Link'; -import HelpTooltip from '../../../components/controls/HelpTooltip'; -import HelpIcon from '../../../components/icons/HelpIcon'; -import { translate } from '../../../helpers/l10n'; -import { getQualityGatesUrl, getQualityGateUrl } from '../../../helpers/urls'; -import { Component, Status } from '../../../types/types'; - -interface Props { - component: Component; - level?: Status; -} - -export function LargeQualityGateBadge({ component, level }: Props) { - const success = level === 'OK'; - - const path = - component.qualityGate === undefined - ? getQualityGatesUrl() - : getQualityGateUrl(component.qualityGate.name); - - return ( -
    -
    - {translate('overview.on_new_code_long')} - - {translate('overview.quality_gate')}, - }} - /> - } - > - - -
    - {level !== undefined && ( -
    {translate('metric.level', level)}
    - )} -
    - ); -} - -export default React.memo(LargeQualityGateBadge); 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 1fe518c3493..03717947f1d 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 @@ -17,31 +17,43 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; +import { + BasicSeparator, + Card, + DeferredSpinner, + HelperHintIcon, + LargeCenteredLayout, + Link, + TextMuted, +} from 'design-system'; import { differenceBy, uniq } from 'lodash'; import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import { getMeasuresWithMetrics } from '../../../api/measures'; import { BranchStatusContextInterface } from '../../../app/components/branch-status/BranchStatusContext'; import withBranchStatus from '../../../app/components/branch-status/withBranchStatus'; import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; import HelpTooltip from '../../../components/controls/HelpTooltip'; -import { Alert } from '../../../components/ui/Alert'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures'; import { isDefined } from '../../../helpers/types'; +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 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'; +import { QualityGateStatusTitle } from '../components/QualityGateStatusTitle'; import SonarLintPromotion from '../components/SonarLintPromotion'; import '../styles.css'; import { MeasurementType, PR_METRICS } from '../utils'; import AfterMergeEstimate from './AfterMergeEstimate'; -import LargeQualityGateBadge from './LargeQualityGateBadge'; interface Props extends BranchStatusData, Pick { branchLike: PullRequest; @@ -140,9 +152,11 @@ export class PullRequestOverview extends React.PureComponent { if (loading) { return ( -
    - -
    + +
    + +
    +
    ); } @@ -150,61 +164,70 @@ export class PullRequestOverview extends React.PureComponent { return null; } + const path = + component.qualityGate === undefined + ? getQualityGatesUrl() + : getQualityGateUrl(component.qualityGate.name); + const failedConditions = conditions .filter((condition) => condition.level === 'ERROR') .map((c) => enhanceConditionWithMeasure(c, measures)) .filter(isDefined); return ( -
    -
    0, - })} - > - {ignoredConditions && ( - - - {translate('overview.quality_gate.ignored_conditions')} - - - - )} -
    -
    -

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

    - } - /> - - + +
    +
    +
    + + + {status && ( + + )} + +
    + + {translate('overview.quality_gate.status')}, + }} + /> + } + > + + +
    + {ignoredConditions && } + + {status === 'OK' && failedConditions.length === 0 && ( + + )} + + {status !== 'OK' && } + + {failedConditions.length > 0 && ( +
    + +
    + )} +
    - {failedConditions.length > 0 && ( -
    -

    - {translate('overview.failed_conditions')} -

    - -
    - )} -

    {translate('overview.measures')} @@ -264,7 +287,7 @@ export class PullRequestOverview extends React.PureComponent {

    -
    + ); } } diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx index 1be518dd553..aba05081a7f 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx @@ -115,7 +115,6 @@ it('should render correctly for a passed QG', async () => { renderPullRequestOverview({ status: 'OK', conditions: [] }); expect(await screen.findByText('metric.level.OK')).toBeInTheDocument(); - expect(screen.queryByText('overview.failed_conditions')).not.toBeInTheDocument(); }); it('should render correctly if conditions are ignored', async () => { @@ -148,12 +147,12 @@ it('should render correctly for a failed QG', async () => { expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument(); - expect(await screen.findByText('overview.failed_conditions')).toBeInTheDocument(); - expect(await screen.findByText('metric.new_coverage.name')).toBeInTheDocument(); expect(await screen.findByText('quality_gates.operator.GT 2.0%')).toBeInTheDocument(); - expect(await screen.findByText('metric.duplicated_lines.name')).toBeInTheDocument(); + expect( + await screen.findByText('metric.duplicated_lines.name quality_gates.conditions.new_code') + ).toBeInTheDocument(); expect(await screen.findByText('quality_gates.operator.GT 1.0%')).toBeInTheDocument(); expect(screen.getByText('quality_gates.operator.GT 3')).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css index 11cefef0cf9..fcb69fe9ab0 100644 --- a/server/sonar-web/src/main/js/apps/overview/styles.css +++ b/server/sonar-web/src/main/js/apps/overview/styles.css @@ -31,11 +31,6 @@ border: 1px solid var(--barBorderColor); } -.overview-quality-gate-sonar-lint-info { - padding: 8px 16px; - border: 1px solid var(--barBorderColor); -} - .overview-panel-title { text-transform: uppercase; font-weight: 600; @@ -93,49 +88,10 @@ background: var(--veryLightGreen); } -/* - * Quality Gate - */ - -.overview-quality-gate-badge-large { - padding: calc(2 * var(--gridSize)); - color: white; - box-sizing: border-box; -} - -.overview-quality-gate-badge-large.failed { - background: var(--error700); -} - -.overview-quality-gate-badge-large.success { - background: var(--success500); - height: 160px; -} - -.overview-quality-gate-badge-large .h3 { - color: white; -} - .overview-quality-gate-conditions-list { background-color: white; } -.overview-quality-gate-conditions-project-name { - padding: calc(2 * var(--gridSize)) 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize)); - font-size: var(--bigFontSize); -} - -.overview-quality-gate-conditions-section-title { - border-bottom: 1px solid var(--barBorderColor); - margin: 0; - font-size: var(--baseFontSize); - background: var(--barBorderColor); -} - -.overview-quality-gate-conditions-list-collapse { - margin: calc(2 * var(--gridSize)) 0; -} - .overview-quality-gate-condition, .overview-quality-gate-condition:hover { display: block; @@ -148,18 +104,6 @@ background-color: var(--rowHoverHighlight); } -.overview-quality-gate-condition-container { - padding: calc(1.5 * var(--gridSize)) var(--gridSize) calc(1.5 * var(--gridSize)) - calc(3 * var(--gridSize)); - border-bottom: 1px solid var(--barBorderColor); -} - -.overview-quality-gate-condition-value { - flex: 0 0 20%; - line-height: 1; - font-size: var(--bigFontSize); -} - /* * Animations */ @@ -187,10 +131,6 @@ max-width: 1260px; } -.pr-overview-failed-conditions { - flex: 0 0 240px; -} - .pr-overview .overview-quality-gate-condition:first-of-type { margin-top: 0; } @@ -211,43 +151,6 @@ border-color: var(--orange); } -.pr-overview .overview-quality-gate-condition:hover .overview-quality-gate-condition-container, -.pr-overview .overview-quality-gate-condition:focus .overview-quality-gate-condition-container { - border-color: inherit; -} - -.pr-overview .overview-quality-gate-condition-metric, -.pr-overview .overview-quality-gate-condition-period { - display: block; - max-width: 125px; - line-height: 16px; - font-size: var(--smallFontSize); -} - -.pr-overview .overview-quality-gate-condition-container { - min-width: 150px; - /* three lines by 16px and 4px margin */ - min-height: 52px; - padding: var(--gridSize); - border-top: 1px solid var(--barBorderColor); - border-right: 1px solid var(--barBorderColor); - transition: border-color 0.3s ease; -} - -.pr-overview .overview-quality-gate-condition-value { - font-size: var(--hugeFontSize); -} - -.pr-overview .overview-quality-gate-badge-large { - width: 240px; - min-height: 160px; - color: var(--transparentWhite); -} - -.pr-overview .overview-quality-gate-sonar-lint-info { - width: 207px; -} - .pr-pverview .overview-measures-row { min-height: 85px; } diff --git a/server/sonar-web/src/main/js/components/measure/Measure.tsx b/server/sonar-web/src/main/js/components/measure/Measure.tsx index bd14e142103..a980441cf9e 100644 --- a/server/sonar-web/src/main/js/components/measure/Measure.tsx +++ b/server/sonar-web/src/main/js/components/measure/Measure.tsx @@ -31,6 +31,7 @@ interface Props { metricType: string; small?: boolean; value: string | undefined; + ratingComponent?: JSX.Element; } export default function Measure({ @@ -40,6 +41,7 @@ export default function Measure({ metricType, small, value, + ratingComponent, }: Props) { if (value === undefined) { return –; @@ -58,7 +60,7 @@ export default function Measure({ } const tooltip = ; - const rating = ; + const rating = ratingComponent || ; if (tooltip) { return ( diff --git a/server/sonar-web/src/main/js/components/measure/MeasureIndicator.tsx b/server/sonar-web/src/main/js/components/measure/MeasureIndicator.tsx new file mode 100644 index 00000000000..945614d0db8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/measure/MeasureIndicator.tsx @@ -0,0 +1,73 @@ +/* + * 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 { CoverageIndicator, DuplicationsIndicator, MetricsRatingBadge } from 'design-system'; +import * as React from 'react'; +import { formatMeasure } from '../../helpers/measures'; +import { MetricKey, MetricType } from '../../types/metrics'; +import Measure from './Measure'; +import { duplicationRatingConverter } from './utils'; + +interface Props { + className?: string; + decimals?: number | null; + metricKey: string; + metricType: string; + small?: boolean; + value: string | undefined; +} + +enum MetricsEnum { + A = 'A', + B = 'B', + C = 'C', + D = 'D', + E = 'E', +} + +export default function MeasureIndicator(props: Props) { + const { className, metricKey, metricType, value } = props; + + if ( + metricType === MetricType.Percent && + (metricKey === MetricKey.duplicated_lines_density || + metricKey === MetricKey.new_duplicated_lines_density) + ) { + return ( +
    + +
    + ); + } + + if (metricType === MetricType.Percent) { + return ( +
    + +
    + ); + } + + const ratingFormatted = formatMeasure(value, MetricType.Rating); + const ratingComponent = ( + + ); + + return ; +} diff --git a/server/sonar-web/src/main/js/components/measure/__tests__/MeasureIndicator-test.tsx b/server/sonar-web/src/main/js/components/measure/__tests__/MeasureIndicator-test.tsx new file mode 100644 index 00000000000..e135ad32ffe --- /dev/null +++ b/server/sonar-web/src/main/js/components/measure/__tests__/MeasureIndicator-test.tsx @@ -0,0 +1,30 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import * as React from 'react'; +import { MetricKey, MetricType } from '../../../types/metrics'; +import MeasureIndicator from '../MeasureIndicator'; + +it('renders correctly for coverage', () => { + render( + + ); + expect(screen.getByRole('img')).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/MeasureIndicator-test.tsx.snap b/server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/MeasureIndicator-test.tsx.snap new file mode 100644 index 00000000000..41fcb0725e8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/MeasureIndicator-test.tsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly for coverage 1`] = ` + + + + + + + + +`; diff --git a/server/sonar-web/src/main/js/components/measure/__tests__/utils-test.tsx b/server/sonar-web/src/main/js/components/measure/__tests__/utils-test.tsx new file mode 100644 index 00000000000..0428864b305 --- /dev/null +++ b/server/sonar-web/src/main/js/components/measure/__tests__/utils-test.tsx @@ -0,0 +1,33 @@ +/* + * 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 { duplicationRatingConverter } from '../utils'; + +describe('duplicationRatingConverter', () => { + it('should work correctly for different use cases', () => { + expect(duplicationRatingConverter(-10)).toEqual('A'); + expect(duplicationRatingConverter(2)).toEqual('A'); + expect(duplicationRatingConverter(4)).toEqual('B'); + expect(duplicationRatingConverter(8)).toEqual('C'); + expect(duplicationRatingConverter(18)).toEqual('D'); + expect(duplicationRatingConverter(20)).toEqual('E'); + expect(duplicationRatingConverter(25)).toEqual('E'); + }); +}); diff --git a/server/sonar-web/src/main/js/components/measure/utils.ts b/server/sonar-web/src/main/js/components/measure/utils.ts index a8af1a0a311..190c79b7976 100644 --- a/server/sonar-web/src/main/js/components/measure/utils.ts +++ b/server/sonar-web/src/main/js/components/measure/utils.ts @@ -38,3 +38,22 @@ export function enhanceMeasure(measure: Measure, metrics: Dict): Measure export function getLeakValue(measure: MeasureIntern | undefined): string | undefined { return measure?.period?.value; } + +export function duplicationRatingConverter(val: number) { + const value = val || 0; + const THRESHOLD_A = 3; + const THRESHOLD_B = 5; + const THRESHOLD_C = 10; + const THRESHOLD_D = 20; + + if (value < THRESHOLD_A) { + return 'A'; + } else if (value < THRESHOLD_B) { + return 'B'; + } else if (value < THRESHOLD_C) { + return 'C'; + } else if (value < THRESHOLD_D) { + return 'D'; + } + return 'E'; +} diff --git a/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx b/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx index e20174318e0..4896bd63304 100644 --- a/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx +++ b/server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx @@ -21,57 +21,8 @@ import * as React from 'react'; import { getBranchLikeQuery } from '../../helpers/branch-like'; import { getComponentDrilldownUrl, getComponentIssuesUrl } from '../../helpers/urls'; import { BranchLike } from '../../types/branch-like'; -import { MetricKey } from '../../types/metrics'; -import { Dict } from '../../types/types'; import Link from '../common/Link'; - -const ISSUE_MEASURES = [ - MetricKey.violations, - MetricKey.new_violations, - MetricKey.blocker_violations, - MetricKey.critical_violations, - MetricKey.major_violations, - MetricKey.minor_violations, - MetricKey.info_violations, - MetricKey.new_blocker_violations, - MetricKey.new_critical_violations, - MetricKey.new_major_violations, - MetricKey.new_minor_violations, - MetricKey.new_info_violations, - MetricKey.open_issues, - MetricKey.reopened_issues, - MetricKey.confirmed_issues, - MetricKey.false_positive_issues, - MetricKey.code_smells, - MetricKey.new_code_smells, - MetricKey.bugs, - MetricKey.new_bugs, - MetricKey.vulnerabilities, - MetricKey.new_vulnerabilities, -]; - -const issueParamsPerMetric: Dict> = { - [MetricKey.blocker_violations]: { resolved: 'false', severities: 'BLOCKER' }, - [MetricKey.new_blocker_violations]: { resolved: 'false', severities: 'BLOCKER' }, - [MetricKey.critical_violations]: { resolved: 'false', severities: 'CRITICAL' }, - [MetricKey.new_critical_violations]: { resolved: 'false', severities: 'CRITICAL' }, - [MetricKey.major_violations]: { resolved: 'false', severities: 'MAJOR' }, - [MetricKey.new_major_violations]: { resolved: 'false', severities: 'MAJOR' }, - [MetricKey.minor_violations]: { resolved: 'false', severities: 'MINOR' }, - [MetricKey.new_minor_violations]: { resolved: 'false', severities: 'MINOR' }, - [MetricKey.info_violations]: { resolved: 'false', severities: 'INFO' }, - [MetricKey.new_info_violations]: { resolved: 'false', severities: 'INFO' }, - [MetricKey.open_issues]: { resolved: 'false', statuses: 'OPEN' }, - [MetricKey.reopened_issues]: { resolved: 'false', statuses: 'REOPENED' }, - [MetricKey.confirmed_issues]: { resolved: 'false', statuses: 'CONFIRMED' }, - [MetricKey.false_positive_issues]: { resolutions: 'FALSE-POSITIVE' }, - [MetricKey.code_smells]: { resolved: 'false', types: 'CODE_SMELL' }, - [MetricKey.new_code_smells]: { resolved: 'false', types: 'CODE_SMELL' }, - [MetricKey.bugs]: { resolved: 'false', types: 'BUG' }, - [MetricKey.new_bugs]: { resolved: 'false', types: 'BUG' }, - [MetricKey.vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' }, - [MetricKey.new_vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' }, -}; +import { isIssueMeasure, propsToIssueParams } from './utils'; interface Props { ariaLabel?: string; @@ -84,27 +35,12 @@ interface Props { } export default class DrilldownLink extends React.PureComponent { - isIssueMeasure = () => { - return ISSUE_MEASURES.indexOf(this.props.metric as MetricKey) !== -1; - }; - - propsToIssueParams = () => { - const params: Dict = { - ...(issueParamsPerMetric[this.props.metric] || { resolved: 'false' }), - }; - - if (this.props.inNewCodePeriod) { - params.inNewCodePeriod = true; - } - - return params; - }; - renderIssuesLink = () => { - const { ariaLabel, className, component, children, branchLike } = this.props; + const { ariaLabel, className, component, children, branchLike, metric, inNewCodePeriod } = + this.props; const url = getComponentIssuesUrl(component, { - ...this.propsToIssueParams(), + ...propsToIssueParams(metric, inNewCodePeriod), ...getBranchLikeQuery(branchLike), }); @@ -116,10 +52,11 @@ export default class DrilldownLink extends React.PureComponent { }; render() { - if (this.isIssueMeasure()) { + const { ariaLabel, className, metric, component, children, branchLike } = this.props; + + if (isIssueMeasure(metric)) { return this.renderIssuesLink(); } - const { ariaLabel, className, metric, component, children, branchLike } = this.props; const url = getComponentDrilldownUrl({ componentKey: component, @@ -127,6 +64,7 @@ export default class DrilldownLink extends React.PureComponent { branchLike, listView: true, }); + return ( {children} diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/DrilldownLink-test.tsx b/server/sonar-web/src/main/js/components/shared/__tests__/DrilldownLink-test.tsx index 871b0d9d820..d1b1ea0b787 100644 --- a/server/sonar-web/src/main/js/components/shared/__tests__/DrilldownLink-test.tsx +++ b/server/sonar-web/src/main/js/components/shared/__tests__/DrilldownLink-test.tsx @@ -30,21 +30,6 @@ it('should render issuesLink correctly', () => { expect(wrapper).toMatchSnapshot(); }); -describe('propsToIssueParams', () => { - it('should render correct default parameters', () => { - const wrapper = shallowRender(); - expect(wrapper.instance().propsToIssueParams()).toEqual({ resolved: 'false' }); - }); - - it(`should render correct params`, () => { - const wrapper = shallowRender({ metric: 'false_positive_issues', inNewCodePeriod: true }); - expect(wrapper.instance().propsToIssueParams()).toEqual({ - resolutions: 'FALSE-POSITIVE', - inNewCodePeriod: true, - }); - }); -}); - const shallowRender = (props: Partial = {}, label = 'label') => { return shallow( diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts b/server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts new file mode 100644 index 00000000000..495248657e8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts @@ -0,0 +1,35 @@ +/* + * 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 { MetricKey } from '../../../types/metrics'; +import { propsToIssueParams } from '../utils'; + +describe('propsToIssueParams', () => { + it('should render correct default parameters', () => { + expect(propsToIssueParams('other')).toEqual({ resolved: 'false' }); + }); + + it(`should render correct params`, () => { + expect(propsToIssueParams(MetricKey.false_positive_issues, true)).toEqual({ + resolutions: 'FALSE-POSITIVE', + inNewCodePeriod: true, + }); + }); +}); diff --git a/server/sonar-web/src/main/js/components/shared/utils.ts b/server/sonar-web/src/main/js/components/shared/utils.ts new file mode 100644 index 00000000000..a073929ddf3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/utils.ts @@ -0,0 +1,86 @@ +/* + * 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 { MetricKey } from '../../types/metrics'; +import { Dict } from '../../types/types'; + +const ISSUE_MEASURES = [ + MetricKey.violations, + MetricKey.new_violations, + MetricKey.blocker_violations, + MetricKey.critical_violations, + MetricKey.major_violations, + MetricKey.minor_violations, + MetricKey.info_violations, + MetricKey.new_blocker_violations, + MetricKey.new_critical_violations, + MetricKey.new_major_violations, + MetricKey.new_minor_violations, + MetricKey.new_info_violations, + MetricKey.open_issues, + MetricKey.reopened_issues, + MetricKey.confirmed_issues, + MetricKey.false_positive_issues, + MetricKey.code_smells, + MetricKey.new_code_smells, + MetricKey.bugs, + MetricKey.new_bugs, + MetricKey.vulnerabilities, + MetricKey.new_vulnerabilities, +]; + +const issueParamsPerMetric: Dict> = { + [MetricKey.blocker_violations]: { resolved: 'false', severities: 'BLOCKER' }, + [MetricKey.new_blocker_violations]: { resolved: 'false', severities: 'BLOCKER' }, + [MetricKey.critical_violations]: { resolved: 'false', severities: 'CRITICAL' }, + [MetricKey.new_critical_violations]: { resolved: 'false', severities: 'CRITICAL' }, + [MetricKey.major_violations]: { resolved: 'false', severities: 'MAJOR' }, + [MetricKey.new_major_violations]: { resolved: 'false', severities: 'MAJOR' }, + [MetricKey.minor_violations]: { resolved: 'false', severities: 'MINOR' }, + [MetricKey.new_minor_violations]: { resolved: 'false', severities: 'MINOR' }, + [MetricKey.info_violations]: { resolved: 'false', severities: 'INFO' }, + [MetricKey.new_info_violations]: { resolved: 'false', severities: 'INFO' }, + [MetricKey.open_issues]: { resolved: 'false', statuses: 'OPEN' }, + [MetricKey.reopened_issues]: { resolved: 'false', statuses: 'REOPENED' }, + [MetricKey.confirmed_issues]: { resolved: 'false', statuses: 'CONFIRMED' }, + [MetricKey.false_positive_issues]: { resolutions: 'FALSE-POSITIVE' }, + [MetricKey.code_smells]: { resolved: 'false', types: 'CODE_SMELL' }, + [MetricKey.new_code_smells]: { resolved: 'false', types: 'CODE_SMELL' }, + [MetricKey.bugs]: { resolved: 'false', types: 'BUG' }, + [MetricKey.new_bugs]: { resolved: 'false', types: 'BUG' }, + [MetricKey.vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' }, + [MetricKey.new_vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' }, +}; + +export function isIssueMeasure(metric: string) { + return ISSUE_MEASURES.indexOf(metric as MetricKey) !== -1; +} + +export function propsToIssueParams(metric: string, inNewCodePeriod = false) { + const params: Dict = { + ...(issueParamsPerMetric[metric] || { resolved: 'false' }), + }; + + if (inNewCodePeriod) { + params.inNewCodePeriod = true; + } + + return params; +} 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 d2e143c4e27..386ab30be8b 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3370,17 +3370,15 @@ system.version_is_availble={version} is available # OVERVIEW # #------------------------------------------------------------------------------ -overview.failed_conditions=Failed conditions -overview.X_more_failed_conditions={0} more failed conditions -overview.1_condition_failed=1 condition failed -overview.X_conditions_failed={0} conditions failed +overview.X_conditions_failed={0} failed condition(s) overview.fix_failed_conditions_with_sonarlint=Fix issues before they fail your Quality Gate with {link} in your IDE. Power up with connected mode! -overview.quality_gate=Quality Gate Status +overview.quality_gate.status=Quality Gate Status +overview.quality_gate=Quality Gate overview.quality_gate_x=Quality Gate: {0} overview.quality_gate.help=A Quality Gate is a set of measure-based Boolean conditions. It helps you know immediately whether your project is production-ready. If your current status is not Passed, you'll see which measures caused the problem and the values required to pass. overview.quality_gate_failed_with_x=with {0} errors overview.quality_gate_code_clean=Your code is clean! -overview.quality_gate_all_conditions_passed=All conditions passed. +overview.passed.clean_code=Enjoy your sparkling clean code! overview.you_should_define_quality_gate=You should define a quality gate on this project. overview.quality_gate.ignored_conditions=Some Quality Gate conditions on New Code were ignored because of the small number of New Lines overview.quality_gate.ignored_conditions.tooltip=At the start of a new code period, if very few lines have been added or modified, it might be difficult to reach the desired level of code coverage or duplications. To prevent Quality Gate failure when there's little that can be done about it, Quality Gate conditions about duplications in new code and coverage on new code are ignored until the number of new lines is at least 20. An administrator can disable this in the general settings. -- 2.39.5