@@ -17,12 +17,25 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { isObject, some } from 'lodash'; | |||
import * as React from 'react'; | |||
module.exports = { | |||
...jest.requireActual('react-intl'), | |||
useIntl: () => ({ | |||
formatMessage: ({ id }, values = {}) => [id, ...Object.values(values)].join('.'), | |||
formatMessage: ({ id }, values = {}) => { | |||
if (some(values, isObject)) { | |||
return ( | |||
<> | |||
{id} | |||
{Object.entries(values).map(([key, value]) => ( | |||
<React.Fragment key={key}>{value}</React.Fragment> | |||
))} | |||
</> | |||
); | |||
} | |||
return [id, ...Object.values(values)].join('.'); | |||
}, | |||
}), | |||
FormattedMessage: ({ id, values }: { id: string; values?: { [x: string]: React.ReactNode } }) => { | |||
return ( |
@@ -26,7 +26,7 @@ describe('CenteredLayout', () => { | |||
expect(screen.getByText('content')).toHaveStyle({ | |||
'min-width': '1280px', | |||
'max-width': '1400px', | |||
'max-width': '1280px', | |||
}); | |||
}); | |||
}); |
@@ -50,7 +50,7 @@ export const INPUT_SIZES = { | |||
}; | |||
export const LAYOUT_VIEWPORT_MIN_WIDTH = 1280; | |||
export const LAYOUT_VIEWPORT_MAX_WIDTH = 1400; | |||
export const LAYOUT_VIEWPORT_MAX_WIDTH = 1280; | |||
export const LAYOUT_VIEWPORT_MAX_WIDTH_LARGE = 1680; | |||
export const LAYOUT_MAIN_CONTENT_GUTTER = 60; | |||
export const LAYOUT_SIDEBAR_WIDTH = 240; |
@@ -0,0 +1,87 @@ | |||
/* | |||
* 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, LightPrimary, QualityGateIndicator, TextMuted } from 'design-system'; | |||
import React from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; | |||
import { Component, Status } from '../../../types/types'; | |||
import BranchQualityGateConditions from './BranchQualityGateConditions'; | |||
interface Props { | |||
status: Status; | |||
branchLike?: BranchLike; | |||
component: Pick<Component, 'key'>; | |||
failedConditions: QualityGateStatusConditionEnhanced[]; | |||
} | |||
export default function BranchQualityGate(props: Readonly<Props>) { | |||
const { status, branchLike, component, failedConditions } = props; | |||
return ( | |||
<> | |||
<BranchQGStatus status={status} /> | |||
<BranchQualityGateConditions | |||
branchLike={branchLike} | |||
component={component} | |||
failedConditions={failedConditions} | |||
/> | |||
</> | |||
); | |||
} | |||
function BranchQGStatus({ status }: Readonly<Pick<Props, 'status'>>) { | |||
const intl = useIntl(); | |||
return ( | |||
<div className="sw-flex sw-items-center sw-mb-5"> | |||
<QualityGateIndicator | |||
status={status} | |||
className="sw-mr-2" | |||
size="xl" | |||
ariaLabel={intl.formatMessage( | |||
{ id: 'overview.quality_gate_x' }, | |||
{ '0': intl.formatMessage({ id: `overview.gate.${status}` }) }, | |||
)} | |||
/> | |||
<div className="sw-flex sw-flex-col sw-justify-around"> | |||
<div className="sw-flex sw-items-center"> | |||
<TextMuted | |||
className="sw-body-sm" | |||
text={intl.formatMessage({ id: 'overview.quality_gate' })} | |||
/> | |||
<HelpTooltip | |||
className="sw-ml-2" | |||
overlay={intl.formatMessage({ id: 'overview.quality_gate.help' })} | |||
> | |||
<HelperHintIcon aria-label="help-tooltip" /> | |||
</HelpTooltip> | |||
</div> | |||
<div> | |||
<LightPrimary as="h1" className="sw-heading-xl"> | |||
{intl.formatMessage({ id: `metric.level.${status}` })} | |||
</LightPrimary> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,197 @@ | |||
/* | |||
* 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 { ChevronRightIcon, DangerButtonSecondary } from 'design-system'; | |||
import React from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { getLocalizedMetricName } from '../../../helpers/l10n'; | |||
import { formatMeasure, getShortType, isDiffMetric } from '../../../helpers/measures'; | |||
import { | |||
getComponentDrilldownUrl, | |||
getComponentIssuesUrl, | |||
getComponentSecurityHotspotsUrl, | |||
} from '../../../helpers/urls'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { IssueType } from '../../../types/issues'; | |||
import { MetricKey, MetricType } from '../../../types/metrics'; | |||
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; | |||
import { Component } from '../../../types/types'; | |||
import { | |||
METRICS_REPORTED_IN_OVERVIEW_CARDS, | |||
RATING_METRICS_MAPPING, | |||
RATING_TO_SEVERITIES_MAPPING, | |||
} from '../utils'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
component: Pick<Component, 'key'>; | |||
failedConditions: QualityGateStatusConditionEnhanced[]; | |||
} | |||
export default function BranchQualityGateConditions(props: Readonly<Props>) { | |||
const { branchLike, component, failedConditions } = props; | |||
const filteredFailedConditions = failedConditions.filter( | |||
(condition) => !METRICS_REPORTED_IN_OVERVIEW_CARDS.includes(condition.metric as MetricKey), | |||
); | |||
return ( | |||
<ul className="sw-flex sw-items-center sw-gap-2 sw-flex-wrap"> | |||
{filteredFailedConditions.map((condition) => ( | |||
<li key={condition.metric}> | |||
<FailedQGCondition branchLike={branchLike} component={component} condition={condition} /> | |||
</li> | |||
))} | |||
</ul> | |||
); | |||
} | |||
function FailedQGCondition( | |||
props: Readonly< | |||
Pick<Props, 'branchLike' | 'component'> & { condition: QualityGateStatusConditionEnhanced } | |||
>, | |||
) { | |||
const { branchLike, component, condition } = props; | |||
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> | |||
); | |||
} | |||
interface FailedMetricProps { | |||
condition: QualityGateStatusConditionEnhanced; | |||
} | |||
export function FailedMetric(props: Readonly<FailedMetricProps>) { | |||
const { | |||
condition: { | |||
measure: { metric }, | |||
}, | |||
} = props; | |||
if (metric.type === MetricType.Rating) { | |||
return <FailedRatingMetric {...props} />; | |||
} | |||
return <FailedGeneralMetric {...props} />; | |||
} | |||
function FailedRatingMetric({ condition }: Readonly<FailedMetricProps>) { | |||
const { | |||
error, | |||
measure: { | |||
metric: { type, domain }, | |||
}, | |||
} = condition; | |||
const intl = useIntl(); | |||
return ( | |||
<> | |||
{intl.formatMessage( | |||
{ id: 'overview.failed_condition.x_required' }, | |||
{ | |||
metric: `${intl.formatMessage({ | |||
id: `metric_domain.${domain}`, | |||
})} ${intl.formatMessage({ id: 'metric.type.RATING' }).toLowerCase()}`, | |||
threshold: ( | |||
<strong className="sw-body-sm-highlight sw-ml-1">{formatMeasure(error, type)}</strong> | |||
), | |||
}, | |||
)} | |||
</> | |||
); | |||
} | |||
function FailedGeneralMetric({ condition }: Readonly<FailedMetricProps>) { | |||
const { | |||
error, | |||
measure: { metric }, | |||
} = condition; | |||
const intl = useIntl(); | |||
const measureFormattingOptions = { decimals: 2, omitExtraDecimalZeros: true }; | |||
return ( | |||
<> | |||
{intl.formatMessage( | |||
{ id: 'overview.failed_condition.x_required' }, | |||
{ | |||
metric: ( | |||
<> | |||
<strong className="sw-body-sm-highlight sw-mr-1"> | |||
{formatMeasure( | |||
condition.actual, | |||
getShortType(metric.type), | |||
measureFormattingOptions, | |||
)} | |||
</strong> | |||
{getLocalizedMetricName(metric, true)} | |||
</> | |||
), | |||
threshold: ( | |||
<strong className="sw-body-sm-highlight sw-ml-1"> | |||
{condition.op === 'GT' ? <>≤</> : <>≥</>}{' '} | |||
{formatMeasure(error, getShortType(metric.type), measureFormattingOptions)} | |||
</strong> | |||
), | |||
}, | |||
)} | |||
</> | |||
); | |||
} | |||
function getQGConditionUrl( | |||
componentKey: string, | |||
condition: QualityGateStatusConditionEnhanced, | |||
branchLike?: BranchLike, | |||
) { | |||
const { metric } = condition; | |||
const sinceLeakPeriod = isDiffMetric(metric); | |||
const issueType = RATING_METRICS_MAPPING[metric]; | |||
if (issueType) { | |||
if (issueType === IssueType.SecurityHotspot) { | |||
return getComponentSecurityHotspotsUrl(componentKey, { | |||
...getBranchLikeQuery(branchLike), | |||
...(sinceLeakPeriod ? { sinceLeakPeriod: 'true' } : {}), | |||
}); | |||
} | |||
return getComponentIssuesUrl(componentKey, { | |||
resolved: 'false', | |||
types: issueType, | |||
...getBranchLikeQuery(branchLike), | |||
...(sinceLeakPeriod ? { sinceLeakPeriod: 'true' } : {}), | |||
...(issueType !== IssueType.CodeSmell | |||
? { severities: RATING_TO_SEVERITIES_MAPPING[Number(condition.error) - 1] } | |||
: {}), | |||
}); | |||
} | |||
return getComponentDrilldownUrl({ | |||
componentKey, | |||
metric, | |||
branchLike, | |||
listView: true, | |||
}); | |||
} |
@@ -36,6 +36,7 @@ import { IssueType } from '../../../types/issues'; | |||
import { MetricKey, MetricType } from '../../../types/metrics'; | |||
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; | |||
import { Component, Dict } from '../../../types/types'; | |||
import { RATING_TO_SEVERITIES_MAPPING } from '../utils'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
@@ -71,13 +72,6 @@ export default class QualityGateCondition extends React.PureComponent<Props> { | |||
} | |||
getUrlForBugsOrVulnerabilities(type: string, inNewCodePeriod: boolean) { | |||
const RATING_TO_SEVERITIES_MAPPING = [ | |||
'BLOCKER,CRITICAL,MAJOR,MINOR', | |||
'BLOCKER,CRITICAL,MAJOR', | |||
'BLOCKER,CRITICAL', | |||
'BLOCKER', | |||
]; | |||
const { condition } = this.props; | |||
const threshold = condition.level === 'ERROR' ? condition.error : condition.warning; | |||
@@ -0,0 +1,136 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockQualityGateStatusConditionEnhanced } from '../../../../helpers/mocks/quality-gates'; | |||
import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { byLabelText, byRole } from '../../../../helpers/testSelector'; | |||
import { MetricKey, MetricType } from '../../../../types/metrics'; | |||
import { FCProps } from '../../../../types/misc'; | |||
import { Status } from '../../utils'; | |||
import BranchQualityGate from '../BranchQualityGate'; | |||
it('renders failed QG', () => { | |||
renderBranchQualityGate(); | |||
// Maintainability rating condition | |||
expect( | |||
byRole('link', { | |||
name: 'overview.failed_condition.x_requiredmetric_domain.Maintainability metric.type.rating A', | |||
}).get(), | |||
).toBeInTheDocument(); | |||
// Security Hotspots rating condition | |||
expect( | |||
byRole('link', { | |||
name: 'overview.failed_condition.x_requiredmetric_domain.Security Review metric.type.rating A', | |||
}).get(), | |||
).toBeInTheDocument(); | |||
// New code smells | |||
expect( | |||
byRole('link', { | |||
name: 'overview.failed_condition.x_required 5 Code Smells ≤ 1', | |||
}).get(), | |||
).toBeInTheDocument(); | |||
// Conditions to cover | |||
expect( | |||
byRole('link', { | |||
name: 'overview.failed_condition.x_required 5 Conditions to cover ≥ 10', | |||
}).get(), | |||
).toBeInTheDocument(); | |||
expect(byLabelText('overview.quality_gate_x.overview.gate.ERROR').get()).toBeInTheDocument(); | |||
}); | |||
it('renders passed QG', () => { | |||
renderBranchQualityGate({ failedConditions: [], status: Status.OK }); | |||
expect(byLabelText('overview.quality_gate_x.overview.gate.OK').get()).toBeInTheDocument(); | |||
expect(byRole('link').query()).not.toBeInTheDocument(); | |||
}); | |||
function renderBranchQualityGate(props: Partial<FCProps<typeof BranchQualityGate>> = {}) { | |||
return renderComponent( | |||
<BranchQualityGate | |||
status={Status.ERROR} | |||
branchLike={mockPullRequest()} | |||
component={mockComponent()} | |||
failedConditions={[ | |||
mockQualityGateStatusConditionEnhanced({ | |||
actual: '5.0', | |||
error: '1.0', | |||
metric: MetricKey.new_maintainability_rating, | |||
measure: mockMeasureEnhanced({ | |||
metric: mockMetric({ | |||
domain: 'Maintainability', | |||
key: MetricKey.new_maintainability_rating, | |||
name: 'Maintainability rating', | |||
type: MetricType.Rating, | |||
}), | |||
}), | |||
}), | |||
mockQualityGateStatusConditionEnhanced({ | |||
actual: '5.0', | |||
error: '1.0', | |||
metric: MetricKey.new_security_review_rating, | |||
measure: mockMeasureEnhanced({ | |||
metric: mockMetric({ | |||
domain: 'Security Review', | |||
key: MetricKey.new_security_review_rating, | |||
name: 'Security Review Rating', | |||
type: MetricType.Rating, | |||
}), | |||
}), | |||
}), | |||
mockQualityGateStatusConditionEnhanced({ | |||
actual: '5', | |||
error: '1', | |||
metric: MetricKey.new_code_smells, | |||
measure: mockMeasureEnhanced({ | |||
metric: mockMetric({ | |||
domain: 'Maintainability', | |||
key: MetricKey.new_code_smells, | |||
name: 'Code Smells', | |||
type: MetricType.ShortInteger, | |||
}), | |||
}), | |||
}), | |||
mockQualityGateStatusConditionEnhanced({ | |||
actual: '5', | |||
error: '10', | |||
op: 'up', | |||
metric: MetricKey.conditions_to_cover, | |||
measure: mockMeasureEnhanced({ | |||
metric: mockMetric({ | |||
key: MetricKey.conditions_to_cover, | |||
name: 'Conditions to cover', | |||
type: MetricType.ShortInteger, | |||
}), | |||
}), | |||
}), | |||
]} | |||
{...props} | |||
/>, | |||
); | |||
} |
@@ -23,34 +23,25 @@ import { | |||
CenteredLayout, | |||
CoverageIndicator, | |||
DuplicationsIndicator, | |||
HelperHintIcon, | |||
Link, | |||
Spinner, | |||
TextMuted, | |||
} from 'design-system'; | |||
import { uniq } from 'lodash'; | |||
import * as React from 'react'; | |||
import { useEffect, useState } from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { getMeasuresWithMetrics } from '../../../api/measures'; | |||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | |||
import { duplicationRatingConverter } from '../../../components/measure/utils'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures'; | |||
import { isDefined } from '../../../helpers/types'; | |||
import { getQualityGateUrl, getQualityGatesUrl } from '../../../helpers/urls'; | |||
import { useBranchStatusQuery } from '../../../queries/branch'; | |||
import { PullRequest } from '../../../types/branch-like'; | |||
import { IssueType } from '../../../types/issues'; | |||
import { Component, MeasureEnhanced } from '../../../types/types'; | |||
import MeasuresPanelIssueMeasure from '../branches/MeasuresPanelIssueMeasure'; | |||
import MeasuresPanelPercentMeasure from '../branches/MeasuresPanelPercentMeasure'; | |||
import BranchQualityGate from '../components/BranchQualityGate'; | |||
import IgnoredConditionWarning from '../components/IgnoredConditionWarning'; | |||
import MetaTopBar from '../components/MetaTopBar'; | |||
import QualityGateConditions from '../components/QualityGateConditions'; | |||
import QualityGateStatusHeader from '../components/QualityGateStatusHeader'; | |||
import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView'; | |||
import SonarLintPromotion from '../components/SonarLintPromotion'; | |||
import '../styles.css'; | |||
import { MeasurementType, PR_METRICS, Status } from '../utils'; | |||
@@ -107,11 +98,6 @@ export default function PullRequestOverview(props: Props) { | |||
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)) | |||
@@ -119,93 +105,58 @@ export default function PullRequestOverview(props: Props) { | |||
return ( | |||
<CenteredLayout> | |||
<div className="it__pr-overview sw-mt-12"> | |||
<MetaTopBar branchLike={branchLike} measures={measures} /> | |||
<BasicSeparator className="sw-my-4" /> | |||
{ignoredConditions && <IgnoredConditionWarning />} | |||
<div className="sw-flex sw-flex-col sw-mr-12 width-30"> | |||
<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')}</Link>, | |||
}} | |||
/> | |||
} | |||
> | |||
<HelperHintIcon aria-label="help-tooltip" /> | |||
</HelpTooltip> | |||
</div> | |||
{status === Status.OK && failedConditions.length === 0 && ( | |||
<QualityGateStatusPassedView /> | |||
)} | |||
{status !== Status.OK && <BasicSeparator />} | |||
{failedConditions.length > 0 && ( | |||
<div> | |||
<QualityGateConditions | |||
branchLike={branchLike} | |||
collapsible | |||
component={component} | |||
failedConditions={failedConditions} | |||
/> | |||
</div> | |||
)} | |||
</Card> | |||
<SonarLintPromotion qgConditions={conditions} /> | |||
</div> | |||
<div className="sw-flex-1"> | |||
<div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4"> | |||
{[ | |||
IssueType.Bug, | |||
IssueType.CodeSmell, | |||
IssueType.Vulnerability, | |||
IssueType.SecurityHotspot, | |||
].map((type: IssueType) => ( | |||
<Card key={type} className="sw-p-8"> | |||
<MeasuresPanelIssueMeasure | |||
branchLike={branchLike} | |||
component={component} | |||
isNewCodeTab | |||
measures={measures} | |||
type={type} | |||
/> | |||
</Card> | |||
))} | |||
{[MeasurementType.Coverage, MeasurementType.Duplication].map( | |||
(type: MeasurementType) => ( | |||
<div className="it__pr-overview sw-mt-12 sw-grid sw-grid-cols-12"> | |||
<div className="sw-col-start-2 sw-col-span-10"> | |||
<MetaTopBar branchLike={branchLike} measures={measures} /> | |||
<BasicSeparator className="sw-my-4" /> | |||
{ignoredConditions && <IgnoredConditionWarning />} | |||
{status && ( | |||
<BranchQualityGate | |||
branchLike={branchLike} | |||
component={component} | |||
status={status} | |||
failedConditions={failedConditions} | |||
/> | |||
)} | |||
<div className="sw-flex-1"> | |||
<div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4"> | |||
{[ | |||
IssueType.Bug, | |||
IssueType.CodeSmell, | |||
IssueType.Vulnerability, | |||
IssueType.SecurityHotspot, | |||
].map((type: IssueType) => ( | |||
<Card key={type} className="sw-p-8"> | |||
<MeasuresPanelPercentMeasure | |||
<MeasuresPanelIssueMeasure | |||
branchLike={branchLike} | |||
component={component} | |||
isNewCodeTab | |||
measures={measures} | |||
ratingIcon={renderMeasureIcon(type)} | |||
type={type} | |||
useDiffMetric | |||
/> | |||
</Card> | |||
), | |||
)} | |||
))} | |||
{[MeasurementType.Coverage, MeasurementType.Duplication].map( | |||
(type: MeasurementType) => ( | |||
<Card key={type} className="sw-p-8"> | |||
<MeasuresPanelPercentMeasure | |||
branchLike={branchLike} | |||
component={component} | |||
measures={measures} | |||
ratingIcon={renderMeasureIcon(type)} | |||
type={type} | |||
useDiffMetric | |||
/> | |||
</Card> | |||
), | |||
)} | |||
</div> | |||
</div> | |||
<SonarLintPromotion qgConditions={conditions} /> | |||
</div> | |||
</div> | |||
</CenteredLayout> |
@@ -26,6 +26,7 @@ import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockQualityGateProjectCondition } from '../../../../helpers/mocks/quality-gates'; | |||
import { mockLoggedInUser, mockMeasure, mockMetric } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { byLabelText, byRole } from '../../../../helpers/testSelector'; | |||
import { ComponentPropsType } from '../../../../helpers/testUtils'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { MetricKey, MetricType } from '../../../../types/metrics'; | |||
@@ -121,6 +122,8 @@ it('should render correctly for a passed QG', async () => { | |||
renderPullRequestOverview(); | |||
await waitFor(async () => expect(await screen.findByText('metric.level.OK')).toBeInTheDocument()); | |||
expect(screen.getByLabelText('overview.quality_gate_x.overview.gate.OK')).toBeInTheDocument(); | |||
expect(screen.getByText('metric.new_lines.name')).toBeInTheDocument(); | |||
expect(screen.getByText(/overview.last_analysis_x/)).toBeInTheDocument(); | |||
}); | |||
@@ -165,18 +168,21 @@ it('should render correctly for a failed QG', async () => { | |||
renderPullRequestOverview(); | |||
await waitFor(async () => | |||
expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument(), | |||
expect( | |||
await byLabelText('overview.quality_gate_x.overview.gate.ERROR').find(), | |||
).toBeInTheDocument(), | |||
); | |||
expect(await screen.findByText('1.0% metric.new_coverage.name')).toBeInTheDocument(); | |||
expect(await screen.findByText('quality_gates.operator.GT 2.0%')).toBeInTheDocument(); | |||
expect( | |||
await screen.findByText('1.0% metric.duplicated_lines.name quality_gates.conditions.new_code'), | |||
byRole('link', { | |||
name: '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', | |||
}).get(), | |||
).toBeInTheDocument(); | |||
expect(await screen.findByText('quality_gates.operator.GT 1.0%')).toBeInTheDocument(); | |||
expect(screen.getByText('quality_gates.operator.GT 3')).toBeInTheDocument(); | |||
}); | |||
function renderPullRequestOverview( |
@@ -26,7 +26,7 @@ import { parseAsString } from '../../helpers/query'; | |||
import { IssueType } from '../../types/issues'; | |||
import { MetricKey } from '../../types/metrics'; | |||
import { AnalysisMeasuresVariations, MeasureHistory } from '../../types/project-activity'; | |||
import { RawQuery } from '../../types/types'; | |||
import { Dict, RawQuery } from '../../types/types'; | |||
export const METRICS: string[] = [ | |||
// quality gate | |||
@@ -152,6 +152,35 @@ const MEASUREMENTS_MAP = { | |||
}, | |||
}; | |||
export const RATING_TO_SEVERITIES_MAPPING = [ | |||
'BLOCKER,CRITICAL,MAJOR,MINOR', | |||
'BLOCKER,CRITICAL,MAJOR', | |||
'BLOCKER,CRITICAL', | |||
'BLOCKER', | |||
]; | |||
export const RATING_METRICS_MAPPING: Dict<IssueType> = { | |||
[MetricKey.reliability_rating]: IssueType.Bug, | |||
[MetricKey.new_reliability_rating]: IssueType.Bug, | |||
[MetricKey.security_rating]: IssueType.Vulnerability, | |||
[MetricKey.new_security_rating]: IssueType.Vulnerability, | |||
[MetricKey.sqale_rating]: IssueType.CodeSmell, | |||
[MetricKey.new_maintainability_rating]: IssueType.CodeSmell, | |||
[MetricKey.security_review_rating]: IssueType.SecurityHotspot, | |||
[MetricKey.new_security_review_rating]: IssueType.SecurityHotspot, | |||
}; | |||
export const METRICS_REPORTED_IN_OVERVIEW_CARDS = [ | |||
MetricKey.new_violations, | |||
MetricKey.violations, | |||
MetricKey.new_coverage, | |||
MetricKey.coverage, | |||
MetricKey.new_security_hotspots_reviewed, | |||
MetricKey.security_hotspots_reviewed, | |||
MetricKey.new_duplicated_lines_density, | |||
MetricKey.duplicated_lines_density, | |||
]; | |||
export function getIssueRatingName(type: IssueType) { | |||
return translate('metric_domain', ISSUETYPE_METRIC_KEYS_MAP[type].ratingName); | |||
} |
@@ -34,6 +34,7 @@ import { RuleRepository } from '../types/coding-rules'; | |||
import { EditionKey } from '../types/editions'; | |||
import { IssueScope, IssueSeverity, IssueStatus, IssueType, RawIssue } from '../types/issues'; | |||
import { Language } from '../types/languages'; | |||
import { MetricKey, MetricType } from '../types/metrics'; | |||
import { Notification } from '../types/notifications'; | |||
import { DumpStatus, DumpTask } from '../types/project-dump'; | |||
import { TaskStatuses } from '../types/tasks'; | |||
@@ -395,11 +396,14 @@ export function mockLocation(overrides: Partial<Location> = {}): Location { | |||
}; | |||
} | |||
export function mockMetric(overrides: Partial<Pick<Metric, 'key' | 'name' | 'type'>> = {}): Metric { | |||
const key = overrides.key || 'coverage'; | |||
export function mockMetric( | |||
overrides: Partial<Pick<Metric, 'key' | 'name' | 'type' | 'domain'>> = {}, | |||
): Metric { | |||
const key = overrides.key || MetricKey.coverage; | |||
const name = overrides.name || key; | |||
const type = overrides.type || 'PERCENT'; | |||
const type = overrides.type || MetricType.Percent; | |||
return { | |||
...overrides, | |||
id: key, | |||
key, | |||
name, |
@@ -2897,6 +2897,7 @@ metric.ncloc_language_distribution.description=Non Commenting Lines of Code Dist | |||
metric.ncloc_language_distribution.name=Lines of Code Per Language | |||
metric.new_blocker_violations.description=New Blocker issues | |||
metric.new_blocker_violations.name=New Blocker Issues | |||
metric.new_blocker_violations.short_name=Blocker Issues | |||
metric.new_branch_coverage.description=Condition coverage of new/changed code | |||
metric.new_branch_coverage.name=Condition Coverage on New Code | |||
metric.new_branch_coverage.extra_short_name=Condition Coverage | |||
@@ -2915,6 +2916,7 @@ metric.new_coverage.name=Coverage on New Code | |||
metric.new_coverage.short_name=Coverage | |||
metric.new_critical_violations.description=New Critical issues | |||
metric.new_critical_violations.name=New Critical Issues | |||
metric.new_critical_violations.short_name=Critical Issues | |||
metric.new_development_cost.description=Development cost on new code | |||
metric.new_development_cost.name=Development Cost on New Code | |||
metric.new_duplicated_blocks.name=Duplicated Blocks on New Code | |||
@@ -2930,6 +2932,7 @@ metric.new_duplicated_lines_density.short_name=Duplications | |||
metric.new_duplicated_lines_density.extra_short_name=Density | |||
metric.new_info_violations.description=New Info issues | |||
metric.new_info_violations.name=New Info Issues | |||
metric.new_info_violations.short_name=Info Issues | |||
metric.new_it_branch_coverage.description=Integration tests condition coverage of new/changed code | |||
metric.new_it_branch_coverage.name=Condition Coverage by IT on New Code | |||
metric.new_it_conditions_to_cover.description=New conditions to cover by integration tests | |||
@@ -2955,8 +2958,10 @@ metric.new_maintainability_rating.name=Maintainability Rating on New Code | |||
metric.new_maintainability_rating.extra_short_name=Rating | |||
metric.new_major_violations.description=New Major issues | |||
metric.new_major_violations.name=New Major Issues | |||
metric.new_major_violations.short_name=Major Issues | |||
metric.new_minor_violations.description=New Minor issues | |||
metric.new_minor_violations.name=New Minor Issues | |||
metric.new_minor_violations.short_name=Minor Issues | |||
metric.new_lines.name=New Lines | |||
metric.new_lines.description=New lines | |||
metric.new_lines.short_name=Lines | |||
@@ -3740,6 +3745,7 @@ 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_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 |