import { OpenCloseIndicator } from './icons/OpenCloseIndicator';
interface AccordionProps {
+ ariaLabel?: string;
children: React.ReactNode;
className?: string;
data?: string;
}
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(() => {
<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}
`;
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')};
`;
--- /dev/null
+/*
+ * 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')};
+`;
);
}
+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`};
${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')};
+`;
export const BareButton = styled.button`
all: unset;
cursor: pointer;
+
+ &:focus-visible {
+ background-color: ${themeColor('dropdownMenuHover')};
+ }
`;
export * from './NavBarTabs';
export * from './NewCodeLegend';
export { QualityGateIndicator } from './QualityGateIndicator';
+export * from './Separator';
export * from './SizeIndicator';
export * from './SonarQubeLogo';
export * from './Text';
{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}
/>
</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}
* 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';
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)}
/>
* 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';
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);
qgStatus.failedConditions
);
- const showName = isApplication(component.qualifier);
+ const collapsible = isApplication(component.qualifier);
const showSectionTitles =
isApplication(component.qualifier) ||
? 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>
+ </>
);
}
// 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();
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);
}
--- /dev/null
+/*
+ * 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>
+ );
+}
* 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';
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> = {
[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>
);
* 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';
let renderConditions;
let renderCollapsed;
+
if (collapsed && sortedConditions.length > MAX_CONDITIONS) {
renderConditions = sortedConditions.slice(0, MAX_CONDITIONS);
renderCollapsed = true;
}
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>
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
* 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';
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>
);
}
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)],
// }
});
+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 });
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(
+++ /dev/null
-/*
- * 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);
* 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;
if (loading) {
return (
- <div className="page page-limited">
- <i className="spinner" />
- </div>
+ <LargeCenteredLayout>
+ <div className="sw-p-6">
+ <DeferredSpinner loading={true} />
+ </div>
+ </LargeCenteredLayout>
);
}
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')}
</div>
</div>
</div>
- </div>
+ </LargeCenteredLayout>
);
}
}
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 () => {
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();
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;
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;
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
*/
max-width: 1260px;
}
-.pr-overview-failed-conditions {
- flex: 0 0 240px;
-}
-
.pr-overview .overview-quality-gate-condition:first-of-type {
margin-top: 0;
}
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;
}
metricType: string;
small?: boolean;
value: string | undefined;
+ ratingComponent?: JSX.Element;
}
export default function Measure({
metricType,
small,
value,
+ ratingComponent,
}: Props) {
if (value === undefined) {
return <span className={className}>–</span>;
}
const tooltip = <RatingTooltipContent metricKey={metricKey} value={value} />;
- const rating = <Rating value={value} />;
+ const rating = ratingComponent || <Rating value={value} />;
if (tooltip) {
return (
--- /dev/null
+/*
+ * 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} />;
+}
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+// 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>
+`;
--- /dev/null
+/*
+ * 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');
+ });
+});
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';
+}
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;
}
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),
});
};
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,
branchLike,
listView: true,
});
+
return (
<Link aria-label={ariaLabel} className={className} to={url}>
{children}
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}>
--- /dev/null
+/*
+ * 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,
+ });
+ });
+});
--- /dev/null
+/*
+ * 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;
+}
# 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.