@@ -18,7 +18,8 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { ChevronRightIcon, DangerButtonSecondary } from 'design-system'; | |||
import styled from '@emotion/styled'; | |||
import { Badge, ButtonSecondary, themeColor } from 'design-system'; | |||
import React from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import { | |||
@@ -27,7 +28,7 @@ import { | |||
propsToIssueParams, | |||
} from '../../../components/shared/utils'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { getLocalizedMetricName } from '../../../helpers/l10n'; | |||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | |||
import { formatMeasure, getShortType, isDiffMetric } from '../../../helpers/measures'; | |||
import { | |||
getComponentDrilldownUrl, | |||
@@ -78,10 +79,14 @@ function FailedQGCondition( | |||
const url = getQGConditionUrl(component.key, condition, branchLike); | |||
return ( | |||
<DangerButtonSecondary className="sw-px-2 sw-py-1 sw-rounded-1/2 sw-body-sm" to={url}> | |||
<FailedMetric condition={condition} /> | |||
<ChevronRightIcon className="sw-ml-1" /> | |||
</DangerButtonSecondary> | |||
<ButtonSecondary className="sw-px-2 sw-py-1 sw-rounded-1/2 sw-body-sm" to={url}> | |||
<Badge className="sw-mr-2" variant="deleted"> | |||
{translate('overview.measures.failed_badge')} | |||
</Badge> | |||
<SpanDanger> | |||
<FailedMetric condition={condition} /> | |||
</SpanDanger> | |||
</ButtonSecondary> | |||
); | |||
} | |||
@@ -209,3 +214,7 @@ function getQGConditionUrl( | |||
listView: true, | |||
}); | |||
} | |||
const SpanDanger = styled.span` | |||
color: ${themeColor('danger')}; | |||
`; |
@@ -34,7 +34,7 @@ it('renders failed QG', () => { | |||
// Maintainability rating condition | |||
const maintainabilityRatingLink = byRole('link', { | |||
name: 'overview.failed_condition.x_rating_requiredmetric_domain.Maintainability metric.type.ratingE A', | |||
name: 'overview.measures.failed_badge overview.failed_condition.x_rating_requiredmetric_domain.Maintainability metric.type.ratingE A', | |||
}).get(); | |||
expect(maintainabilityRatingLink).toBeInTheDocument(); | |||
expect(maintainabilityRatingLink).toHaveAttribute( | |||
@@ -44,7 +44,7 @@ it('renders failed QG', () => { | |||
// Security Hotspots rating condition | |||
const securityHotspotsRatingLink = byRole('link', { | |||
name: 'overview.failed_condition.x_rating_requiredmetric_domain.Security Review metric.type.ratingE A', | |||
name: 'overview.measures.failed_badge overview.failed_condition.x_rating_requiredmetric_domain.Security Review metric.type.ratingE A', | |||
}).get(); | |||
expect(securityHotspotsRatingLink).toBeInTheDocument(); | |||
expect(securityHotspotsRatingLink).toHaveAttribute( | |||
@@ -54,7 +54,7 @@ it('renders failed QG', () => { | |||
// New code smells | |||
const codeSmellsLink = byRole('link', { | |||
name: 'overview.failed_condition.x_required 5 Code Smells ≤ 1', | |||
name: 'overview.measures.failed_badge overview.failed_condition.x_required 5 Code Smells ≤ 1', | |||
}).get(); | |||
expect(codeSmellsLink).toBeInTheDocument(); | |||
expect(codeSmellsLink).toHaveAttribute( | |||
@@ -64,7 +64,7 @@ it('renders failed QG', () => { | |||
// Conditions to cover | |||
const conditionToCoverLink = byRole('link', { | |||
name: 'overview.failed_condition.x_required 5 Conditions to cover ≥ 10', | |||
name: 'overview.measures.failed_badge overview.failed_condition.x_required 5 Conditions to cover ≥ 10', | |||
}).get(); | |||
expect(conditionToCoverLink).toBeInTheDocument(); | |||
expect(conditionToCoverLink).toHaveAttribute( |
@@ -18,8 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import styled from '@emotion/styled'; | |||
import classNames from 'classnames'; | |||
import { Card, ContentLink, PageContentFontWrapper, themeColor } from 'design-system'; | |||
import { Badge, Card, ContentLink, themeColor } from 'design-system'; | |||
import * as React from 'react'; | |||
import { To } from 'react-router-dom'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
@@ -41,16 +40,11 @@ export default function MeasuresCard( | |||
const { failed, children, metric, icon, value, url, label, ...rest } = props; | |||
return ( | |||
<StyledCard | |||
className={classNames( | |||
'sw-h-fit sw-p-8 sw-rounded-2 sw-flex sw-justify-between sw-items-center sw-text-base', | |||
{ | |||
failed, | |||
}, | |||
)} | |||
<Card | |||
className="sw-h-fit sw-p-8 sw-rounded-2 sw-flex sw-justify-between sw-items-center sw-text-base" | |||
{...rest} | |||
> | |||
<PageContentFontWrapper className="sw-flex sw-flex-col sw-gap-1 sw-justify-between"> | |||
<div className="sw-flex sw-flex-col sw-gap-1 sw-justify-between"> | |||
<div className="sw-flex sw-items-center sw-gap-2 sw-font-semibold"> | |||
{value ? ( | |||
<ContentLink | |||
@@ -68,21 +62,20 @@ export default function MeasuresCard( | |||
<StyledNoValue> — </StyledNoValue> | |||
)} | |||
{translate(label)} | |||
{failed && ( | |||
<Badge className="sw-mt-1/2" variant="deleted"> | |||
{translate('overview.measures.failed_badge')} | |||
</Badge> | |||
)} | |||
</div> | |||
{children && <div className="sw-flex sw-flex-col">{children}</div>} | |||
</PageContentFontWrapper> | |||
</div> | |||
{icon && <div>{icon}</div>} | |||
</StyledCard> | |||
</Card> | |||
); | |||
} | |||
const StyledNoValue = styled.span` | |||
color: ${themeColor('pageTitle')}; | |||
`; | |||
export const StyledCard = styled(Card)` | |||
&.failed { | |||
border-color: ${themeColor('qgCardFailed')}; | |||
} | |||
`; |
@@ -17,53 +17,65 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { TextError } from 'design-system'; | |||
import { LightLabel, TextError } from 'design-system'; | |||
import * as React from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import { To } from 'react-router-dom'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { MetricKey, MetricType } from '../../../types/metrics'; | |||
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; | |||
import { Status } from '../utils'; | |||
import MeasuresCard from './MeasuresCard'; | |||
interface Props { | |||
failedConditions: QualityGateStatusConditionEnhanced[]; | |||
conditions: QualityGateStatusConditionEnhanced[]; | |||
label: string; | |||
url: To; | |||
value: string; | |||
failingConditionMetric: MetricKey; | |||
requireLabel: string; | |||
conditionMetric: MetricKey; | |||
guidingKeyOnError?: string; | |||
} | |||
export default function MeasuresCardNumber( | |||
props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>, | |||
) { | |||
const { | |||
label, | |||
value, | |||
failedConditions, | |||
url, | |||
failingConditionMetric, | |||
requireLabel, | |||
guidingKeyOnError, | |||
...rest | |||
} = props; | |||
const { label, value, conditions, url, conditionMetric, guidingKeyOnError, ...rest } = props; | |||
const failed = Boolean( | |||
failedConditions.find((condition) => condition.metric === failingConditionMetric), | |||
); | |||
const intl = useIntl(); | |||
const condition = conditions.find((condition) => condition.metric === conditionMetric); | |||
const conditionFailed = condition?.level === Status.ERROR; | |||
const requireLabel = | |||
condition && | |||
intl.formatMessage( | |||
{ id: 'overview.quality_gate.required_x' }, | |||
{ | |||
operator: condition.op === 'GT' ? '≤' : '≥', | |||
value: formatMeasure(condition.error, MetricType.Percent, { | |||
decimals: 2, | |||
omitExtraDecimalZeros: true, | |||
}), | |||
}, | |||
); | |||
return ( | |||
<MeasuresCard | |||
url={url} | |||
value={formatMeasure(value, MetricType.ShortInteger)} | |||
metric={failingConditionMetric} | |||
metric={conditionMetric} | |||
label={label} | |||
failed={failed} | |||
data-guiding-id={failed ? guidingKeyOnError : undefined} | |||
failed={conditionFailed} | |||
data-guiding-id={conditionFailed ? guidingKeyOnError : undefined} | |||
{...rest} | |||
> | |||
{failed && <TextError className="sw-font-regular sw-mt-2" text={requireLabel} />} | |||
{requireLabel && | |||
(conditionFailed ? ( | |||
<TextError className="sw-mt-2 sw-font-regular" text={requireLabel} /> | |||
) : ( | |||
<LightLabel className="sw-mt-2">{requireLabel}</LightLabel> | |||
))} | |||
</MeasuresCard> | |||
); | |||
} |
@@ -19,7 +19,6 @@ | |||
*/ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import { getLeakValue } from '../../../components/measure/utils'; | |||
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
@@ -42,13 +41,11 @@ interface Props { | |||
branchLike?: BranchLike; | |||
component: Component; | |||
measures: MeasureEnhanced[]; | |||
failedConditions: QualityGateStatusConditionEnhanced[]; | |||
conditions: QualityGateStatusConditionEnhanced[]; | |||
} | |||
export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>) { | |||
const { branchLike, component, measures, failedConditions, className } = props; | |||
const intl = useIntl(); | |||
const { branchLike, component, measures, conditions, className } = props; | |||
const newViolations = getLeakValue(findMeasure(measures, MetricKey.new_violations)) as string; | |||
const newSecurityHotspots = getLeakValue( | |||
@@ -66,14 +63,8 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>) | |||
...DEFAULT_ISSUES_QUERY, | |||
})} | |||
value={newViolations} | |||
failedConditions={failedConditions} | |||
failingConditionMetric={MetricKey.new_violations} | |||
requireLabel={intl.formatMessage( | |||
{ id: 'overview.quality_gate.require_fixing' }, | |||
{ | |||
count: newViolations, | |||
}, | |||
)} | |||
conditions={conditions} | |||
conditionMetric={MetricKey.new_violations} | |||
guidingKeyOnError="overviewZeroNewIssuesSimplification" | |||
/> | |||
@@ -88,8 +79,8 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>) | |||
branchLike, | |||
listView: true, | |||
})} | |||
failedConditions={failedConditions} | |||
failingConditionMetric={MetricKey.new_coverage} | |||
conditions={conditions} | |||
conditionMetric={MetricKey.new_coverage} | |||
newLinesMetric={MetricKey.new_lines_to_cover} | |||
afterMergeMetric={MetricKey.coverage} | |||
measures={measures} | |||
@@ -107,14 +98,8 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>) | |||
...getBranchLikeQuery(branchLike), | |||
})} | |||
value={newSecurityHotspots} | |||
failedConditions={failedConditions} | |||
failingConditionMetric={MetricKey.new_security_hotspots_reviewed} | |||
requireLabel={intl.formatMessage( | |||
{ id: 'overview.quality_gate.require_reviewing' }, | |||
{ | |||
count: newSecurityHotspots, | |||
}, | |||
)} | |||
conditions={conditions} | |||
conditionMetric={MetricKey.new_security_hotspots_reviewed} | |||
/> | |||
<MeasuresCardPercent | |||
@@ -128,8 +113,8 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>) | |||
branchLike, | |||
listView: true, | |||
})} | |||
failedConditions={failedConditions} | |||
failingConditionMetric={MetricKey.new_duplicated_lines_density} | |||
conditions={conditions} | |||
conditionMetric={MetricKey.new_duplicated_lines_density} | |||
newLinesMetric={MetricKey.new_lines} | |||
afterMergeMetric={MetricKey.duplicated_lines_density} | |||
measures={measures} |
@@ -35,7 +35,7 @@ import { BranchLike } from '../../../types/branch-like'; | |||
import { MetricKey, MetricType } from '../../../types/metrics'; | |||
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; | |||
import { MeasureEnhanced } from '../../../types/types'; | |||
import { MeasurementType, getMeasurementMetricKey } from '../utils'; | |||
import { MeasurementType, Status, getMeasurementMetricKey } from '../utils'; | |||
import MeasuresCard from './MeasuresCard'; | |||
interface Props { | |||
@@ -45,8 +45,8 @@ interface Props { | |||
label: string; | |||
url: To; | |||
measures: MeasureEnhanced[]; | |||
failedConditions: QualityGateStatusConditionEnhanced[]; | |||
failingConditionMetric: MetricKey; | |||
conditions: QualityGateStatusConditionEnhanced[]; | |||
conditionMetric: MetricKey; | |||
newLinesMetric: MetricKey; | |||
afterMergeMetric: MetricKey; | |||
} | |||
@@ -61,8 +61,8 @@ export default function MeasuresCardPercent( | |||
label, | |||
url, | |||
measures, | |||
failedConditions, | |||
failingConditionMetric, | |||
conditions, | |||
conditionMetric, | |||
newLinesMetric, | |||
afterMergeMetric, | |||
} = props; | |||
@@ -87,27 +87,21 @@ export default function MeasuresCardPercent( | |||
const afterMergeValue = findMeasure(measures, afterMergeMetric)?.value; | |||
const failedCondition = failedConditions.find( | |||
(condition) => condition.metric === failingConditionMetric, | |||
); | |||
const condition = conditions.find((c) => c.metric === conditionMetric); | |||
const conditionFailed = condition?.level === Status.ERROR; | |||
let errorRequireLabel = ''; | |||
if (failedCondition) { | |||
errorRequireLabel = intl.formatMessage( | |||
const requireLabel = | |||
condition && | |||
intl.formatMessage( | |||
{ id: 'overview.quality_gate.required_x' }, | |||
{ | |||
operator: failedCondition.op === 'GT' ? '≤' : '≥', | |||
value: formatMeasure( | |||
failedCondition.level === 'ERROR' ? failedCondition.error : failedCondition.warning, | |||
MetricType.Percent, | |||
{ | |||
decimals: 2, | |||
omitExtraDecimalZeros: true, | |||
}, | |||
), | |||
operator: condition.op === 'GT' ? '≤' : '≥', | |||
value: formatMeasure(condition.error, MetricType.Percent, { | |||
decimals: 2, | |||
omitExtraDecimalZeros: true, | |||
}), | |||
}, | |||
); | |||
} | |||
return ( | |||
<MeasuresCard | |||
@@ -115,11 +109,18 @@ export default function MeasuresCardPercent( | |||
metric={metricKey} | |||
url={url} | |||
label={label} | |||
failed={Boolean(failedCondition)} | |||
failed={conditionFailed} | |||
icon={renderIcon(measurementType, value)} | |||
> | |||
<div className="sw-flex sw-flex-col"> | |||
<LightLabel className="sw-flex sw-items-center sw-gap-1"> | |||
<> | |||
{requireLabel && | |||
(conditionFailed ? ( | |||
<TextError className="sw-mt-2 sw-font-regular" text={requireLabel} /> | |||
) : ( | |||
<LightLabel className="sw-mt-2">{requireLabel}</LightLabel> | |||
))} | |||
<LightLabel className="sw-flex sw-items-center sw-gap-1 sw-mt-4"> | |||
<FormattedMessage | |||
defaultMessage={translate(newLinesLabel)} | |||
id={newLinesLabel} | |||
@@ -152,11 +153,7 @@ export default function MeasuresCardPercent( | |||
/> | |||
</LightLabel> | |||
)} | |||
{failedCondition && ( | |||
<TextError className="sw-mt-2 sw-font-regular" text={errorRequireLabel} /> | |||
)} | |||
</div> | |||
</> | |||
</MeasuresCard> | |||
); | |||
} |
@@ -17,7 +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 { BasicSeparator, CenteredLayout, Spinner } from 'design-system'; | |||
import { BasicSeparator, CenteredLayout, PageContentFontWrapper, Spinner } from 'design-system'; | |||
import { uniq } from 'lodash'; | |||
import * as React from 'react'; | |||
import { useEffect, useState } from 'react'; | |||
@@ -29,13 +29,13 @@ import { isDefined } from '../../../helpers/types'; | |||
import { useBranchStatusQuery } from '../../../queries/branch'; | |||
import { PullRequest } from '../../../types/branch-like'; | |||
import { Component, MeasureEnhanced, QualityGate } from '../../../types/types'; | |||
import MeasuresCardPanel from '../branches/MeasuresCardPanel'; | |||
import BranchQualityGate from '../components/BranchQualityGate'; | |||
import IgnoredConditionWarning from '../components/IgnoredConditionWarning'; | |||
import MetaTopBar from '../components/MetaTopBar'; | |||
import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide'; | |||
import '../styles.css'; | |||
import { PR_METRICS, Status } from '../utils'; | |||
import MeasuresCardPanel from './MeasuresCardPanel'; | |||
import SonarLintAd from './SonarLintAd'; | |||
interface Props { | |||
@@ -60,11 +60,8 @@ export default function PullRequestOverview(props: Props) { | |||
const metricKeys = | |||
conditions !== undefined | |||
? // Also load metrics that apply to failing QG conditions. | |||
uniq([ | |||
...PR_METRICS, | |||
...conditions.filter((c) => c.level !== Status.OK).map((c) => c.metric), | |||
]) | |||
? // Also load metrics that apply to QG conditions. | |||
uniq([...PR_METRICS, ...conditions.map((c) => c.metric)]) | |||
: PR_METRICS; | |||
getMeasuresWithMetrics(component.key, metricKeys, getBranchLikeQuery(branchLike)).then( | |||
@@ -108,14 +105,17 @@ export default function PullRequestOverview(props: Props) { | |||
return null; | |||
} | |||
const failedConditions = conditions | |||
.filter((condition) => condition.level === Status.ERROR) | |||
const enhancedConditions = conditions | |||
.map((c) => enhanceConditionWithMeasure(c, measures)) | |||
.filter(isDefined); | |||
const failedConditions = enhancedConditions.filter( | |||
(condition) => condition.level === Status.ERROR, | |||
); | |||
return ( | |||
<CenteredLayout> | |||
<div className="it__pr-overview sw-mt-12 sw-mb-8 sw-grid sw-grid-cols-12"> | |||
<PageContentFontWrapper className="it__pr-overview sw-mt-12 sw-mb-8 sw-grid sw-grid-cols-12 sw-body-sm"> | |||
<div className="sw-col-start-2 sw-col-span-10"> | |||
<MetaTopBar branchLike={branchLike} measures={measures} /> | |||
<BasicSeparator className="sw-my-4" /> | |||
@@ -135,7 +135,7 @@ export default function PullRequestOverview(props: Props) { | |||
className="sw-flex-1" | |||
branchLike={branchLike} | |||
component={component} | |||
failedConditions={failedConditions} | |||
conditions={enhancedConditions} | |||
measures={measures} | |||
/> | |||
@@ -143,7 +143,7 @@ export default function PullRequestOverview(props: Props) { | |||
<SonarLintAd status={status} /> | |||
</div> | |||
</div> | |||
</PageContentFontWrapper> | |||
</CenteredLayout> | |||
); | |||
} |
@@ -180,12 +180,12 @@ it('should render correctly for a failed QG', async () => { | |||
expect( | |||
byRole('link', { | |||
name: 'overview.failed_condition.x_required 10.0% duplicated_lines ≤ 1.0%', | |||
name: 'overview.measures.failed_badge overview.failed_condition.x_required 10.0% duplicated_lines ≤ 1.0%', | |||
}).get(), | |||
).toBeInTheDocument(); | |||
expect( | |||
byRole('link', { | |||
name: 'overview.failed_condition.x_required 10 new_bugs ≤ 3', | |||
name: 'overview.measures.failed_badge overview.failed_condition.x_required 10 new_bugs ≤ 3', | |||
}).get(), | |||
).toBeInTheDocument(); | |||
}); |
@@ -3802,8 +3802,8 @@ system.version_is_availble={version} is available | |||
#------------------------------------------------------------------------------ | |||
overview.1_condition_failed=1 failed condition | |||
overview.X_conditions_failed={0} failed conditions | |||
overview.failed_condition.x_rating_required={rating} is {value} required {threshold} | |||
overview.failed_condition.x_required={metric} required {threshold} | |||
overview.failed_condition.x_rating_required={rating} is {value}. Required {threshold} | |||
overview.failed_condition.x_required={metric}. Required {threshold} | |||
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.status=Quality Gate Status | |||
overview.quality_gate=Quality Gate | |||
@@ -3835,7 +3835,7 @@ overview.quality_gate.on_x_new_lines=On {link} New Lines | |||
overview.quality_gate.x_estimated_after_merge={value} Estimated after merge | |||
overview.quality_gate.require_fixing={count, plural, one {requires} other {require}} fixing | |||
overview.quality_gate.require_reviewing={count, plural, one {requires} other {require}} reviewing | |||
overview.quality_gate.required_x=required {operator} {value} | |||
overview.quality_gate.required_x=Required {operator} {value} | |||
overview.quality_profiles=Quality Profiles used | |||
overview.new_code_period_x=New Code: {0} | |||
overview.max_new_code_period_from_x=Max New Code from: {0} | |||
@@ -3920,6 +3920,8 @@ overview.gate.view.errors=The view failed the quality gate on the following cond | |||
overview.measurement_type.DUPLICATION=Duplications | |||
overview.measurement_type.COVERAGE=Coverage | |||
overview.measures.failed_badge=Failed | |||
overview.complexity_tooltip.function={0} functions have complexity around {1} | |||
overview.complexity_tooltip.file={0} files have complexity around {1} | |||