Browse Source

SONAR-19018 Migrating project quality gate section to MIUI

tags/10.1.0.73491
Revanshu Paliwal 1 year ago
parent
commit
c6dc867d4a
34 changed files with 889 additions and 574 deletions
  1. 3
    1
      server/sonar-web/design-system/src/components/Accordion.tsx
  2. 12
    0
      server/sonar-web/design-system/src/components/Link.tsx
  3. 40
    0
      server/sonar-web/design-system/src/components/Separator.tsx
  4. 25
    0
      server/sonar-web/design-system/src/components/Text.tsx
  5. 4
    0
      server/sonar-web/design-system/src/components/buttons.tsx
  6. 1
    0
      server/sonar-web/design-system/src/components/index.ts
  7. 4
    4
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
  8. 49
    81
      server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx
  9. 88
    78
      server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx
  10. 2
    2
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
  11. 42
    0
      server/sonar-web/src/main/js/apps/overview/components/IgnoredConditionWarning.tsx
  12. 69
    55
      server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx
  13. 16
    24
      server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx
  14. 54
    0
      server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusHeader.tsx
  15. 32
    0
      server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusPassedView.tsx
  16. 38
    0
      server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx
  17. 7
    5
      server/sonar-web/src/main/js/apps/overview/components/SonarLintPromotion.tsx
  18. 6
    2
      server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateCondition-test.tsx
  19. 1
    1
      server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateConditions-test.tsx
  20. 0
    76
      server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx
  21. 75
    52
      server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
  22. 3
    4
      server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx
  23. 0
    97
      server/sonar-web/src/main/js/apps/overview/styles.css
  24. 3
    1
      server/sonar-web/src/main/js/components/measure/Measure.tsx
  25. 73
    0
      server/sonar-web/src/main/js/components/measure/MeasureIndicator.tsx
  26. 30
    0
      server/sonar-web/src/main/js/components/measure/__tests__/MeasureIndicator-test.tsx
  27. 27
    0
      server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/MeasureIndicator-test.tsx.snap
  28. 33
    0
      server/sonar-web/src/main/js/components/measure/__tests__/utils-test.tsx
  29. 19
    0
      server/sonar-web/src/main/js/components/measure/utils.ts
  30. 8
    70
      server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx
  31. 0
    15
      server/sonar-web/src/main/js/components/shared/__tests__/DrilldownLink-test.tsx
  32. 35
    0
      server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts
  33. 86
    0
      server/sonar-web/src/main/js/components/shared/utils.ts
  34. 4
    6
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 3
- 1
server/sonar-web/design-system/src/components/Accordion.tsx View File

@@ -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) {
<BareButton
aria-controls={`${id}-panel`}
aria-expanded={open}
aria-label={ariaLabel}
className="sw-flex sw-items-center sw-justify-between sw-px-2 sw-py-2 sw-box-border sw-w-full"
id={`${id}-header`}
onClick={handleClick}

+ 12
- 0
server/sonar-web/design-system/src/components/Link.tsx View File

@@ -161,6 +161,18 @@ export const HoverLink = styled(StyledBaseLink)`
`;
HoverLink.displayName = 'HoverLink';

export const LinkBox = styled(StyledBaseLink)`
text-decoration: none;

&:hover,
&:focus,
&:active {
background-color: ${themeColor('dropdownMenuHover')};
display: block;
}
`;
LinkBox.displayName = 'LinkBox';

export const DiscreetLink = styled(HoverLink)`
--border: ${themeBorder('default', 'linkDiscreet')};
`;

+ 40
- 0
server/sonar-web/design-system/src/components/Separator.tsx View File

@@ -0,0 +1,40 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import styled from '@emotion/styled';
import tw from 'twin.macro';
import { themeColor } from '../helpers/theme';

export const BasicSeparator = styled.hr`
height: 1px;
background-color: ${themeColor('border')};

${tw`sw-my-1`}
${tw`sw-overflow-hidden`};
${tw`sw-clear-both`}
`;

export const BlueGreySeparator = styled(BasicSeparator)`
background-color: ${themeColor('popupBorder')};
`;

export const GreySeparator = styled(BasicSeparator)`
background-color: ${themeColor('subnavigationBorder')};
`;

+ 25
- 0
server/sonar-web/design-system/src/components/Text.tsx View File

@@ -50,6 +50,22 @@ export function TextMuted({ text, className }: { className?: string; text: strin
);
}

export function PageTitle({ text, className }: { className?: string; text: string }) {
return (
<StyledPageTitle className={className} title={text}>
{text}
</StyledPageTitle>
);
}

export function TextError({ text, className }: { className?: string; text: string }) {
return (
<StyledTextError className={className} title={text}>
{text}
</StyledTextError>
);
}

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')};
`;

+ 4
- 0
server/sonar-web/design-system/src/components/buttons.tsx View File

@@ -216,4 +216,8 @@ const ThirdPartyButtonStyled: React.FC<ButtonProps> = styled(Button)`
export const BareButton = styled.button`
all: unset;
cursor: pointer;

&:focus-visible {
background-color: ${themeColor('dropdownMenuHover')};
}
`;

+ 1
- 0
server/sonar-web/design-system/src/components/index.ts View File

@@ -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';

+ 4
- 4
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx View File

@@ -92,8 +92,8 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
{projectIsEmpty ? (
<NoCodeWarning branchLike={branch} component={component} measures={measures} />
) : (
<div className="display-flex-row">
<div className="width-25 big-spacer-right">
<div className="sw-flex">
<div className="width-30 sw-mr-12">
<QualityGatePanel
component={component}
loading={loadingStatus}
@@ -101,8 +101,8 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
/>
</div>

<div className="flex-1">
<div className="display-flex-column">
<div className="sw-flex-1">
<div className="sw-flex sw-flex-col">
<MeasuresPanel
appLeak={appLeak}
branch={branch}

+ 49
- 81
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx View File

@@ -17,16 +17,16 @@
* 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 } from 'design-system';
import { flatMap } from 'lodash';
import * as React from 'react';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import { Alert } from '../../../components/ui/Alert';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { ComponentQualifier, isApplication } from '../../../types/component';
import { QualityGateStatus } from '../../../types/quality-gates';
import { CaycStatus, Component } from '../../../types/types';
import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
import QualityGateStatusHeader from '../components/QualityGateStatusHeader';
import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView';
import { QualityGateStatusTitle } from '../components/QualityGateStatusTitle';
import SonarLintPromotion from '../components/SonarLintPromotion';
import ApplicationNonCaycProjectWarning from './ApplicationNonCaycProjectWarning';
import QualityGatePanelSection from './QualityGatePanelSection';
@@ -69,88 +69,56 @@ export function QualityGatePanel(props: QualityGatePanelProps) {
qgStatuses.some((p) => Boolean(p.ignoredConditions));

return (
<div className="overview-panel" data-test="overview__quality-gate-panel">
<div className="display-flex-center spacer-bottom">
<h2 className="overview-panel-title null-spacer-bottom">
{translate('overview.quality_gate')}{' '}
</h2>
<HelpTooltip
className="little-spacer-left"
overlay={
<div className="big-padded-top big-padded-bottom">
{translate('overview.quality_gate.help')}
<div data-test="overview__quality-gate-panel">
<QualityGateStatusTitle />
<Card>
<div>
{loading ? (
<div className="sw-p-6">
<DeferredSpinner loading={loading} />
</div>
}
/>
</div>
{showIgnoredConditionWarning && (
<Alert className="big-spacer-bottom" display="inline" variant="info">
<span className="text-middle">
{translate('overview.quality_gate.ignored_conditions')}
</span>
<HelpTooltip
className="spacer-left"
overlay={translate('overview.quality_gate.ignored_conditions.tooltip')}
/>
</Alert>
)}
) : (
<>
<QualityGateStatusHeader
status={overallLevel}
failedConditionCount={overallFailedConditionsCount}
/>
{success && <QualityGateStatusPassedView />}

<div>
{loading ? (
<div className="overview-panel-big-padded">
<DeferredSpinner loading={loading} />
</div>
) : (
<>
<div
className={classNames('overview-quality-gate-badge-large', {
failed: !success,
success,
})}
>
<div className="big-spacer-bottom huge h3">
{translate('metric.level', overallLevel)}
</div>
{showIgnoredConditionWarning && <IgnoredConditionWarning />}

<span className="small">
{overallFailedConditionsCount > 0
? translateWithParameters(
'overview.X_conditions_failed',
overallFailedConditionsCount
)
: translate('overview.quality_gate_all_conditions_passed')}
</span>
</div>
{!success && <BasicSeparator />}

{(overallFailedConditionsCount > 0 ||
qgStatuses.some(({ caycStatus }) => caycStatus !== CaycStatus.Compliant)) && (
<div data-test="overview__quality-gate-conditions">
{qgStatuses.map((qgStatus) => (
<QualityGatePanelSection
component={component}
key={qgStatus.key}
qgStatus={qgStatus}
/>
))}
</div>
)}
{(overallFailedConditionsCount > 0 ||
qgStatuses.some(({ caycStatus }) => caycStatus !== CaycStatus.Compliant)) && (
<div data-test="overview__quality-gate-conditions">
{qgStatuses.map((qgStatus) => (
<QualityGatePanelSection
component={component}
key={qgStatus.key}
qgStatus={qgStatus}
/>
))}
</div>
)}
</>
)}
</div>
</Card>

{nonCaycProjectsInApp.length > 0 && (
<ApplicationNonCaycProjectWarning
projects={nonCaycProjectsInApp}
caycStatus={CaycStatus.NonCompliant}
/>
)}
{nonCaycProjectsInApp.length > 0 && (
<ApplicationNonCaycProjectWarning
projects={nonCaycProjectsInApp}
caycStatus={CaycStatus.NonCompliant}
/>
)}

{overCompliantCaycProjectsInApp.length > 0 && (
<ApplicationNonCaycProjectWarning
projects={overCompliantCaycProjectsInApp}
caycStatus={CaycStatus.OverCompliant}
/>
)}
</>
)}
</div>
{overCompliantCaycProjectsInApp.length > 0 && (
<ApplicationNonCaycProjectWarning
projects={overCompliantCaycProjectsInApp}
caycStatus={CaycStatus.OverCompliant}
/>
)}
<SonarLintPromotion
qgConditions={flatMap(qgStatuses, (qgStatus) => qgStatus.failedConditions)}
/>

+ 88
- 78
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx View File

@@ -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 <span className="text-muted big-spacer-left">{text}</span>;
}

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 (
<div className="overview-quality-gate-conditions">
{showName && (
<ButtonPlain
aria-label={toggleLabel}
aria-expanded={!collapsed}
className="width-100 text-left"
onClick={toggle}
>
<div className="display-flex-center">
<div
className="overview-quality-gate-conditions-project-name text-ellipsis h3"
title={qgStatus.name}
>
{collapsed ? <ChevronRightIcon /> : <ChevronDownIcon />}
<span className="spacer-left">{qgStatus.name}</span>
</div>
{collapsed && displayConditions(qgStatus.failedConditions.length)}
</div>
</ButtonPlain>
)}
const renderFailedConditions = () => {
return (
<>
{newCodeFailedConditions.length > 0 && (
<>
{showSectionTitles && (
<>
<p className="sw-px-2 sw-py-3">
{translateWithParameters(
'quality_gates.conditions.new_code_x',
newCodeFailedConditions.length.toString()
)}
</p>
<BasicSeparator />
</>
)}
<QualityGateConditions
component={qgStatus}
branchLike={qgStatus.branchLike}
failedConditions={newCodeFailedConditions}
/>
</>
)}

{overallFailedConditions.length > 0 && (
<>
{showSectionTitles && (
<>
<p className="sw-px-2 sw-py-3">
{translateWithParameters(
'quality_gates.conditions.overall_code_x',
overallFailedConditions.length.toString()
)}
</p>
<BasicSeparator />
</>
)}
<QualityGateConditions
component={qgStatus}
branchLike={qgStatus.branchLike}
failedConditions={overallFailedConditions}
/>
</>
)}
</>
);
};

{!collapsed && (
return (
<>
{collapsible ? (
<>
<Accordion
ariaLabel={toggleLabel}
onClick={toggle}
open={!collapsed}
header={
<div className="sw-flex sw-flex-col sw-text-sm">
<span className="sw-body-sm-highlight">{qgStatus.name}</span>
{collapsed && newCodeFailedConditions.length > 0 && (
<TextMuted
text={translateWithParameters(
'quality_gates.conditions.new_code_x',
newCodeFailedConditions.length
)}
/>
)}
{collapsed && overallFailedConditions.length > 0 && (
<TextMuted
text={translateWithParameters(
'quality_gates.conditions.overall_code_x',
overallFailedConditions.length
)}
/>
)}
</div>
}
>
<BasicSeparator />
{renderFailedConditions()}
</Accordion>
<BasicSeparator />
</>
) : (
<>
{renderFailedConditions()}
{qgStatus.caycStatus === CaycStatus.NonCompliant &&
!isApplication(component.qualifier) && (
<div className="big-padded bordered-bottom overview-quality-gate-conditions-list">
<CleanAsYouCodeWarning component={component} />
</div>
)}

{qgStatus.caycStatus === CaycStatus.OverCompliant &&
!isApplication(component.qualifier) && (
<div className="big-padded bordered-bottom overview-quality-gate-conditions-list">
<CleanAsYouCodeWarningOverCompliant component={component} />
</div>
)}

{newCodeFailedConditions.length > 0 && (
<>
{showSectionTitles && (
<div className="big-padded overview-quality-gate-conditions-section-title h4">
{translateWithParameters(
'quality_gates.conditions.new_code_x',
newCodeFailedConditions.length.toString()
)}
</div>
)}
<QualityGateConditions
component={qgStatus}
branchLike={qgStatus.branchLike}
failedConditions={newCodeFailedConditions}
/>
</>
)}

{overallFailedConditions.length > 0 && (
<>
{showSectionTitles && (
<div className="big-padded overview-quality-gate-conditions-section-title h4">
{translateWithParameters(
'quality_gates.conditions.overall_code_x',
overallFailedConditions.length.toString()
)}
</div>
)}
<QualityGateConditions
component={qgStatus}
branchLike={qgStatus.branchLike}
failedConditions={overallFailedConditions}
/>
</>
)}
</>
)}
</div>
</>
);
}


+ 2
- 2
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx View File

@@ -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);
}

+ 42
- 0
server/sonar-web/src/main/js/apps/overview/components/IgnoredConditionWarning.tsx View File

@@ -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 (
<FlagMessage
ariaLabel={translate('overview.quality_gate.ignored_conditions')}
className="sw-mb-4"
variant="info"
>
<span>{translate('overview.quality_gate.ignored_conditions')}</span>
<HelpTooltip
className="sw-ml-2"
overlay={translate('overview.quality_gate.ignored_conditions.tooltip')}
>
<HelperHintIcon aria-label="help-tooltip" />
</HelpTooltip>
</FlagMessage>
);
}

+ 69
- 55
server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx View File

@@ -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<Props> {
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<Props> {
[MetricKey.new_security_hotspots_reviewed]: () => this.getUrlForSecurityHotspot(true),
};

return (
<li>
{METRICS_TO_URL_MAPPING[metricKey] ? (
<Link className={className} to={METRICS_TO_URL_MAPPING[metricKey]()}>
{children}
</Link>
) : (
<DrilldownLink
branchLike={branchLike}
className={className}
component={component.key}
metric={condition.measure.metric.key}
inNewCodePeriod={condition.period != null}
>
{children}
</DrilldownLink>
)}
</li>
);
if (METRICS_TO_URL_MAPPING[metricKey]) {
return <LinkBox to={METRICS_TO_URL_MAPPING[metricKey]()}>{children}</LinkBox>;
}

if (isIssueMeasure(condition.measure.metric.key)) {
const url = getComponentIssuesUrl(component.key, {
...propsToIssueParams(condition.measure.metric.key, condition.period != null),
...getBranchLikeQuery(branchLike),
});

return <LinkBox to={url}>{children}</LinkBox>;
}

const url = getComponentDrilldownUrl({
componentKey: component.key,
metric: condition.measure.metric.key,
branchLike,
listView: true,
});

return <LinkBox to={url}>{children}</LinkBox>;
}

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(
<div className="overview-quality-gate-condition-container display-flex-center">
<div className="overview-quality-gate-condition-value text-center spacer-right">
<Measure
decimals={2}
metricKey={measure.metric.key}
metricType={measure.metric.type}
value={actual}
/>
</div>

<div>
<span className="overview-quality-gate-condition-metric little-spacer-right">
<IssueTypeIcon className="little-spacer-right" query={metric.key} />
{localizeMetric(metric.key)}
</span>
{!isDiff && condition.period != null && (
<span className="overview-quality-gate-condition-period text-ellipsis little-spacer-right">
{translate('quality_gates.conditions.new_code')}
<div className="sw-flex sw-items-center sw-p-2">
<MeasureIndicator
className="sw-flex sw-justify-center sw-w-6 sw-mx-4"
decimals={2}
metricKey={measure.metric.key}
metricType={measure.metric.type}
value={actual}
/>
<div className="sw-flex sw-flex-col sw-text-sm">
<div className="sw-flex sw-items-center">
<IssueTypeIcon className="sw-mr-2" query={metric.key} />
<span className="sw-body-sm-highlight sw-text-ellipsis sw-max-w-abs-300">
{this.getPrimaryText()}
</span>
)}
<span className="little-spacer-top small text-muted">
{operator} {formatMeasure(threshold, metric.type)}
</span>
</div>
<TextMuted text={`${operator} ${formatMeasure(threshold, metric.type)}`} />
</div>
</div>
);

+ 16
- 24
server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx View File

@@ -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 (
<ul
className="overview-quality-gate-conditions-list"
id="overview-quality-gate-conditions-list"
>
<ul id="overview-quality-gate-conditions-list" className="sw-mb-2">
{renderConditions.map((condition) => (
<QualityGateCondition
branchLike={branchLike}
component={component}
condition={condition}
key={condition.measure.metric.key}
/>
<div key={condition.measure.metric.key}>
<QualityGateCondition
branchLike={branchLike}
component={component}
condition={condition}
/>
<BasicSeparator />
</div>
))}
{renderCollapsed && (
<li>
<ButtonLink
className="overview-quality-gate-conditions-list-collapse"
onClick={handleToggleCollapsed}
>
{translateWithParameters(
'overview.X_more_failed_conditions',
sortedConditions.length - MAX_CONDITIONS
)}
<ChevronDownIcon className="little-spacer-left" />
</ButtonLink>
<li className="sw-flex sw-justify-center sw-my-3">
<Link onClick={handleToggleCollapsed} to={{}} preventDefault={true}>
<span className="sw-font-semibold sw-text-sm">{translate('show_more')}</span>
</Link>
</li>
)}
</ul>

+ 54
- 0
server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusHeader.tsx View File

@@ -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 (
<div className="sw-flex sw-items-center sw-mb-4">
<QualityGateIndicator status={status} className="sw-mr-2" size="xl" />
<div className="sw-flex sw-flex-col">
<div>
<TextMuted text={translate('overview.quality_gate')} />
</div>
<div>
<span className="sw-heading-lg">{translate('metric.level', status)}</span>
</div>
</div>
<div className="sw-flex sw-flex-1 sw-justify-end">
{failedConditionCount > 0 && (
<TextError
text={translateWithParameters('overview.X_conditions_failed', failedConditionCount)}
/>
)}
</div>
</div>
);
}

+ 32
- 0
server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusPassedView.tsx View File

@@ -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 (
<div className="sw-flex sw-items-center sw-justify-center sw-flex-col">
<OverviewQGPassedIcon className="sw-my-12" />
<p className="sw-mb-8">{translate('overview.passed.clean_code')}</p>
</div>
);
}

+ 38
- 0
server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx View File

@@ -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 (
<div className="sw-flex sw-items-center sw-mb-4">
<PageTitle text={translate('overview.quality_gate.status')} />
<HelpTooltip
className="sw-ml-2"
overlay={<div className="sw-my-4">{translate('overview.quality_gate.help')}</div>}
>
<HelperHintIcon aria-label="help-tooltip" />
</HelpTooltip>
</div>
);
}

+ 7
- 5
server/sonar-web/src/main/js/apps/overview/components/SonarLintPromotion.tsx View File

@@ -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 (
<div className="it__overview__sonarlint-promotion big-spacer-top overview-quality-gate-sonar-lint-info">
<Card className="it__overview__sonarlint-promotion sw-my-4 sw-body-sm">
<FormattedMessage
id="overview.fix_failed_conditions_with_sonarlint"
defaultMessage={translate('overview.fix_failed_conditions_with_sonarlint')}
values={{
link: (
<>
<a
href="https://www.sonarqube.org/sonarlint/?referrer=sonarqube"
<DiscreetLink
to="https://www.sonarqube.org/sonarlint/?referrer=sonarqube"
rel="noopener noreferrer"
target="_blank"
showExternalIcon={false}
>
SonarLint
</a>
</DiscreetLink>
<SonarLintIcon size={16} />
</>
),
}}
/>
</div>
</Card>
);
}


+ 6
- 2
server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateCondition-test.tsx View File

@@ -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 });

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateConditions-test.tsx View File

@@ -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(

+ 0
- 76
server/sonar-web/src/main/js/apps/overview/pullRequests/LargeQualityGateBadge.tsx View File

@@ -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 (
<div
className={classNames('overview-quality-gate-badge-large small', {
failed: !success,
success,
})}
>
<div className="display-flex-center">
<span>{translate('overview.on_new_code_long')}</span>

<HelpTooltip
className="little-spacer-left"
overlay={
<FormattedMessage
defaultMessage={translate('overview.quality_gate.conditions_on_new_code')}
id="overview.quality_gate.conditions_on_new_code"
values={{
link: <Link to={path}>{translate('overview.quality_gate')}</Link>,
}}
/>
}
>
<HelpIcon fill={colors.transparentWhite} size={12} />
</HelpTooltip>
</div>
{level !== undefined && (
<div className="huge-spacer-top huge h3">{translate('metric.level', level)}</div>
)}
</div>
);
}

export default React.memo(LargeQualityGateBadge);

+ 75
- 52
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx View File

@@ -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<BranchStatusContextInterface, 'fetchBranchStatus'> {
branchLike: PullRequest;
@@ -140,9 +152,11 @@ export class PullRequestOverview extends React.PureComponent<Props, State> {

if (loading) {
return (
<div className="page page-limited">
<i className="spinner" />
</div>
<LargeCenteredLayout>
<div className="sw-p-6">
<DeferredSpinner loading={true} />
</div>
</LargeCenteredLayout>
);
}

@@ -150,61 +164,70 @@ export class PullRequestOverview extends React.PureComponent<Props, State> {
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 (
<div className="page page-limited">
<div
className={classNames('pr-overview', {
'has-conditions': failedConditions.length > 0,
})}
>
{ignoredConditions && (
<Alert className="big-spacer-bottom" display="inline" variant="info">
<span className="text-middle">
{translate('overview.quality_gate.ignored_conditions')}
</span>
<HelpTooltip
className="spacer-left"
overlay={translate('overview.quality_gate.ignored_conditions.tooltip')}
/>
</Alert>
)}
<div className="display-flex-row">
<div className="big-spacer-right">
<h2 className="overview-panel-title spacer-bottom small display-inline-flex-center">
{translate('overview.quality_gate')}
<HelpTooltip
className="little-spacer-left"
overlay={
<div className="big-padded-top big-padded-bottom">
{translate('overview.quality_gate.help')}
</div>
}
/>
</h2>
<LargeQualityGateBadge component={component} level={status} />
<LargeCenteredLayout>
<div className="it__pr-overview sw-mt-12">
<div className="sw-flex">
<div className="sw-flex sw-flex-col sw-mr-12 width-30">
<QualityGateStatusTitle />
<Card>
{status && (
<QualityGateStatusHeader
status={status}
failedConditionCount={failedConditions.length}
/>
)}

<div className="sw-flex sw-items-center sw-mb-4">
<TextMuted text={translate('overview.on_new_code_long')} />
<HelpTooltip
className="sw-ml-2"
overlay={
<FormattedMessage
defaultMessage={translate('overview.quality_gate.conditions_on_new_code')}
id="overview.quality_gate.conditions_on_new_code"
values={{
link: <Link to={path}>{translate('overview.quality_gate.status')}</Link>,
}}
/>
}
>
<HelperHintIcon aria-label="help-tooltip" />
</HelpTooltip>
</div>

{ignoredConditions && <IgnoredConditionWarning />}

{status === 'OK' && failedConditions.length === 0 && (
<QualityGateStatusPassedView />
)}

{status !== 'OK' && <BasicSeparator />}

{failedConditions.length > 0 && (
<div>
<QualityGateConditions
branchLike={branchLike}
collapsible={true}
component={component}
failedConditions={failedConditions}
/>
</div>
)}
</Card>
<SonarLintPromotion qgConditions={conditions} />
</div>

{failedConditions.length > 0 && (
<div className="pr-overview-failed-conditions big-spacer-right">
<h2 className="overview-panel-title spacer-bottom small">
{translate('overview.failed_conditions')}
</h2>
<QualityGateConditions
branchLike={branchLike}
collapsible={true}
component={component}
failedConditions={failedConditions}
/>
</div>
)}

<div className="flex-1">
<h2 className="overview-panel-title spacer-bottom small">
{translate('overview.measures')}
@@ -264,7 +287,7 @@ export class PullRequestOverview extends React.PureComponent<Props, State> {
</div>
</div>
</div>
</div>
</LargeCenteredLayout>
);
}
}

+ 3
- 4
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx View File

@@ -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();

+ 0
- 97
server/sonar-web/src/main/js/apps/overview/styles.css View File

@@ -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;
}

+ 3
- 1
server/sonar-web/src/main/js/components/measure/Measure.tsx View File

@@ -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 <span className={className}>–</span>;
@@ -58,7 +60,7 @@ export default function Measure({
}

const tooltip = <RatingTooltipContent metricKey={metricKey} value={value} />;
const rating = <Rating value={value} />;
const rating = ratingComponent || <Rating value={value} />;

if (tooltip) {
return (

+ 73
- 0
server/sonar-web/src/main/js/components/measure/MeasureIndicator.tsx View File

@@ -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 (
<div className={className}>
<DuplicationsIndicator rating={duplicationRatingConverter(Number(value))} />
</div>
);
}

if (metricType === MetricType.Percent) {
return (
<div className={className}>
<CoverageIndicator value={value} />
</div>
);
}

const ratingFormatted = formatMeasure(value, MetricType.Rating);
const ratingComponent = (
<MetricsRatingBadge rating={ratingFormatted as MetricsEnum} label={ratingFormatted} />
);

return <Measure {...props} ratingComponent={ratingComponent} />;
}

+ 30
- 0
server/sonar-web/src/main/js/components/measure/__tests__/MeasureIndicator-test.tsx View File

@@ -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(
<MeasureIndicator metricKey={MetricKey.coverage} metricType={MetricType.Percent} value="73.0" />
);
expect(screen.getByRole('img')).toMatchSnapshot();
});

+ 27
- 0
server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/MeasureIndicator-test.tsx.snap View File

@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly for coverage 1`] = `
<svg
class="donut-chart"
height="24"
role="img"
width="24"
>
<g
transform="translate(0, 0)"
>
<g
transform="translate(12, 12)"
>
<path
d="M0.75,-11.977A12,12,0,1,1,-11.672,2.785L-8.709,2.271A9,9,0,1,0,0.75,-8.969Z"
style="fill: rgb(18,183,106);"
/>
<path
d="M-11.929,1.307A12,12,0,0,1,-0.75,-11.977L-0.75,-8.969A9,9,0,0,0,-8.965,0.793Z"
style="fill: rgb(180,35,24);"
/>
</g>
</g>
</svg>
`;

+ 33
- 0
server/sonar-web/src/main/js/components/measure/__tests__/utils-test.tsx View File

@@ -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');
});
});

+ 19
- 0
server/sonar-web/src/main/js/components/measure/utils.ts View File

@@ -38,3 +38,22 @@ export function enhanceMeasure(measure: Measure, metrics: Dict<Metric>): 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';
}

+ 8
- 70
server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx View File

@@ -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<Dict<string>> = {
[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<Props> {
isIssueMeasure = () => {
return ISSUE_MEASURES.indexOf(this.props.metric as MetricKey) !== -1;
};

propsToIssueParams = () => {
const params: Dict<string | boolean> = {
...(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<Props> {
};

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<Props> {
branchLike,
listView: true,
});

return (
<Link aria-label={ariaLabel} className={className} to={url}>
{children}

+ 0
- 15
server/sonar-web/src/main/js/components/shared/__tests__/DrilldownLink-test.tsx View File

@@ -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<DrilldownLink['props']> = {}, label = 'label') => {
return shallow<DrilldownLink>(
<DrilldownLink component="project123" metric="other" {...props}>

+ 35
- 0
server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts View File

@@ -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,
});
});
});

+ 86
- 0
server/sonar-web/src/main/js/components/shared/utils.ts View File

@@ -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<Dict<string>> = {
[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<string | boolean> = {
...(issueParamsPerMetric[metric] || { resolved: 'false' }),
};

if (inNewCodePeriod) {
params.inNewCodePeriod = true;
}

return params;
}

+ 4
- 6
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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.

Loading…
Cancel
Save