@@ -18,12 +18,14 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
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 { QualityGateStatus } from '../../../types/quality-gates'; | |||
import SonarLintPromotion from '../components/SonarLintPromotion'; | |||
import QualityGatePanelSection from './QualityGatePanelSection'; | |||
export interface QualityGatePanelProps { | |||
@@ -116,6 +118,9 @@ export function QualityGatePanel(props: QualityGatePanelProps) { | |||
</> | |||
)} | |||
</div> | |||
<SonarLintPromotion | |||
qgConditions={flatMap(qgStatuses, qgStatus => qgStatus.failedConditions)} | |||
/> | |||
</div> | |||
); | |||
} |
@@ -23,7 +23,7 @@ import { isDiffMetric } from '../../../helpers/measures'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { QualityGateStatus } from '../../../types/quality-gates'; | |||
import { QualityGateConditions } from '../components/QualityGateConditions'; | |||
import QualityGateConditions from '../components/QualityGateConditions'; | |||
export interface QualityGatePanelSectionProps { | |||
branchLike?: BranchLike; |
@@ -182,6 +182,81 @@ exports[`should render correctly for applications 1`] = ` | |||
/> | |||
</div> | |||
</div> | |||
<Connect(withCurrentUser(SonarLintPromotion)) | |||
qgConditions={ | |||
Array [ | |||
Object { | |||
"actual": "10", | |||
"error": "0", | |||
"level": "ERROR", | |||
"measure": Object { | |||
"bestValue": true, | |||
"leak": "1", | |||
"metric": Object { | |||
"id": "coverage", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
}, | |||
"period": Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
"value": "1.0", | |||
}, | |||
"metric": "foo", | |||
"op": "GT", | |||
}, | |||
Object { | |||
"actual": "10", | |||
"error": "0", | |||
"level": "ERROR", | |||
"measure": Object { | |||
"bestValue": true, | |||
"leak": "1", | |||
"metric": Object { | |||
"id": "coverage", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
}, | |||
"period": Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
"value": "1.0", | |||
}, | |||
"metric": "foo", | |||
"op": "GT", | |||
}, | |||
Object { | |||
"actual": "10", | |||
"error": "0", | |||
"level": "ERROR", | |||
"measure": Object { | |||
"bestValue": true, | |||
"leak": "1", | |||
"metric": Object { | |||
"id": "new_code_smells", | |||
"key": "new_code_smells", | |||
"name": "New_code_smells", | |||
"type": "PERCENT", | |||
}, | |||
"period": Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
"value": "1.0", | |||
}, | |||
"metric": "new_code_smells", | |||
"op": "GT", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
`; | |||
@@ -320,6 +395,35 @@ exports[`should render correctly for applications 2`] = ` | |||
/> | |||
</div> | |||
</div> | |||
<Connect(withCurrentUser(SonarLintPromotion)) | |||
qgConditions={ | |||
Array [ | |||
Object { | |||
"actual": "10", | |||
"error": "0", | |||
"level": "ERROR", | |||
"measure": Object { | |||
"bestValue": true, | |||
"leak": "1", | |||
"metric": Object { | |||
"id": "coverage", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
}, | |||
"period": Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
"value": "1.0", | |||
}, | |||
"metric": "foo", | |||
"op": "GT", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
`; | |||
@@ -424,6 +528,35 @@ exports[`should render correctly for projects 1`] = ` | |||
/> | |||
</div> | |||
</div> | |||
<Connect(withCurrentUser(SonarLintPromotion)) | |||
qgConditions={ | |||
Array [ | |||
Object { | |||
"actual": "10", | |||
"error": "0", | |||
"level": "ERROR", | |||
"measure": Object { | |||
"bestValue": true, | |||
"leak": "1", | |||
"metric": Object { | |||
"id": "coverage", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
}, | |||
"period": Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
"value": "1.0", | |||
}, | |||
"metric": "foo", | |||
"op": "GT", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
`; | |||
@@ -466,6 +599,9 @@ exports[`should render correctly for projects 2`] = ` | |||
</span> | |||
</div> | |||
</div> | |||
<Connect(withCurrentUser(SonarLintPromotion)) | |||
qgConditions={Array []} | |||
/> | |||
</div> | |||
`; | |||
@@ -585,5 +721,34 @@ exports[`should render correctly for projects 3`] = ` | |||
/> | |||
</div> | |||
</div> | |||
<Connect(withCurrentUser(SonarLintPromotion)) | |||
qgConditions={ | |||
Array [ | |||
Object { | |||
"actual": "10", | |||
"error": "0", | |||
"level": "ERROR", | |||
"measure": Object { | |||
"bestValue": true, | |||
"leak": "1", | |||
"metric": Object { | |||
"id": "coverage", | |||
"key": "coverage", | |||
"name": "Coverage", | |||
"type": "PERCENT", | |||
}, | |||
"period": Object { | |||
"bestValue": true, | |||
"index": 1, | |||
"value": "1.0", | |||
}, | |||
"value": "1.0", | |||
}, | |||
"metric": "foo", | |||
"op": "GT", | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
`; |
@@ -9,7 +9,7 @@ exports[`should render correctly 1`] = ` | |||
> | |||
quality_gates.conditions.new_code | |||
</h4> | |||
<QualityGateConditions | |||
<Memo(QualityGateConditions) | |||
component={ | |||
Object { | |||
"failedConditions": Array [ | |||
@@ -99,7 +99,7 @@ exports[`should render correctly 1`] = ` | |||
> | |||
quality_gates.conditions.overall_code | |||
</h4> | |||
<QualityGateConditions | |||
<Memo(QualityGateConditions) | |||
component={ | |||
Object { | |||
"failedConditions": Array [ | |||
@@ -201,7 +201,7 @@ exports[`should render correctly 2`] = ` | |||
> | |||
quality_gates.conditions.new_code | |||
</h4> | |||
<QualityGateConditions | |||
<Memo(QualityGateConditions) | |||
component={ | |||
Object { | |||
"failedConditions": Array [ | |||
@@ -291,7 +291,7 @@ exports[`should render correctly 2`] = ` | |||
> | |||
quality_gates.conditions.overall_code | |||
</h4> | |||
<QualityGateConditions | |||
<Memo(QualityGateConditions) | |||
component={ | |||
Object { | |||
"failedConditions": Array [ |
@@ -0,0 +1,79 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 { FormattedMessage } from 'react-intl'; | |||
import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; | |||
import SonarLintIcon from '../../../components/icons/SonarLintIcon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { QualityGateStatusCondition } from '../../../types/quality-gates'; | |||
export interface SonarLintPromotionProps { | |||
currentUser: T.CurrentUser; | |||
qgConditions?: QualityGateStatusCondition[]; | |||
} | |||
const CONDITIONS_TO_SHOW = [ | |||
MetricKey.new_blocker_violations, | |||
MetricKey.new_critical_violations, | |||
MetricKey.new_info_violations, | |||
MetricKey.new_violations, | |||
MetricKey.new_major_violations, | |||
MetricKey.new_minor_violations, | |||
MetricKey.new_code_smells, | |||
MetricKey.new_bugs, | |||
MetricKey.new_vulnerabilities, | |||
MetricKey.new_security_rating, | |||
MetricKey.new_maintainability_rating, | |||
MetricKey.new_reliability_rating | |||
]; | |||
export function SonarLintPromotion({ currentUser, qgConditions }: SonarLintPromotionProps) { | |||
const showMessage = qgConditions?.some( | |||
qgCondition => | |||
CONDITIONS_TO_SHOW.includes(qgCondition.metric as MetricKey) && qgCondition.level === 'ERROR' | |||
); | |||
if (!showMessage || currentUser.usingSonarLintConnectedMode) { | |||
return null; | |||
} | |||
return ( | |||
<div className="it__overview__sonarlint-promotion big-spacer-top overview-quality-gate-sonar-lint-info"> | |||
<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" | |||
rel="noopener noreferrer" | |||
target="_blank"> | |||
SonarLint | |||
</a> | |||
<SonarLintIcon size={16} /> | |||
</> | |||
) | |||
}} | |||
/> | |||
</div> | |||
); | |||
} | |||
export default withCurrentUser(SonarLintPromotion); |
@@ -0,0 +1,63 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockQualityGateStatusCondition } from '../../../../helpers/mocks/quality-gates'; | |||
import { mockCurrentUser } from '../../../../helpers/testMocks'; | |||
import { MetricKey } from '../../../../types/metrics'; | |||
import { SonarLintPromotion, SonarLintPromotionProps } from '../SonarLintPromotion'; | |||
it('should render correctly', () => { | |||
expect(shallowRender().type()).toBeNull(); | |||
expect( | |||
shallowRender({ currentUser: mockCurrentUser({ usingSonarLintConnectedMode: true }) }).type() | |||
).toBeNull(); | |||
expect( | |||
shallowRender({ | |||
qgConditions: [mockQualityGateStatusCondition({ metric: MetricKey.new_bugs, level: 'ERROR' })] | |||
}) | |||
).toMatchSnapshot('has failed condition'); | |||
}); | |||
it.each( | |||
[ | |||
MetricKey.new_blocker_violations, | |||
MetricKey.new_critical_violations, | |||
MetricKey.new_info_violations, | |||
MetricKey.new_violations, | |||
MetricKey.new_major_violations, | |||
MetricKey.new_minor_violations, | |||
MetricKey.new_code_smells, | |||
MetricKey.new_bugs, | |||
MetricKey.new_vulnerabilities, | |||
MetricKey.new_security_rating, | |||
MetricKey.new_maintainability_rating, | |||
MetricKey.new_reliability_rating | |||
].map(Array.of) | |||
)('should show message for %s', metric => { | |||
const wrapper = shallowRender({ | |||
qgConditions: [mockQualityGateStatusCondition({ metric: metric as string })] | |||
}); | |||
expect(wrapper.type()).not.toBeNull(); | |||
}); | |||
function shallowRender(props: Partial<SonarLintPromotionProps> = {}) { | |||
return shallow(<SonarLintPromotion currentUser={mockCurrentUser()} {...props} />); | |||
} |
@@ -0,0 +1,28 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: has failed condition 1`] = ` | |||
<div | |||
className="it__overview__sonarlint-promotion big-spacer-top overview-quality-gate-sonar-lint-info" | |||
> | |||
<FormattedMessage | |||
defaultMessage="overview.fix_failed_conditions_with_sonarlint" | |||
id="overview.fix_failed_conditions_with_sonarlint" | |||
values={ | |||
Object { | |||
"link": <React.Fragment> | |||
<a | |||
href="https://www.sonarqube.org/sonarlint/?referrer=sonarqube" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
SonarLint | |||
</a> | |||
<SonarLintIcon | |||
size={16} | |||
/> | |||
</React.Fragment>, | |||
} | |||
} | |||
/> | |||
</div> | |||
`; |
@@ -37,6 +37,7 @@ import IssueLabel from '../components/IssueLabel'; | |||
import IssueRating from '../components/IssueRating'; | |||
import MeasurementLabel from '../components/MeasurementLabel'; | |||
import QualityGateConditions from '../components/QualityGateConditions'; | |||
import SonarLintPromotion from '../components/SonarLintPromotion'; | |||
import '../styles.css'; | |||
import { MeasurementType, PR_METRICS } from '../utils'; | |||
import AfterMergeEstimate from './AfterMergeEstimate'; | |||
@@ -197,6 +198,8 @@ export class PullRequestOverview extends React.PureComponent<Props, State> { | |||
/> | |||
</h2> | |||
<LargeQualityGateBadge component={component} level={status} /> | |||
<SonarLintPromotion qgConditions={conditions} /> | |||
</div> | |||
{failedConditions.length > 0 && ( |
@@ -53,6 +53,28 @@ exports[`should render correctly for a failed QG 1`] = ` | |||
} | |||
level="ERROR" | |||
/> | |||
<Connect(withCurrentUser(SonarLintPromotion)) | |||
qgConditions={ | |||
Array [ | |||
Object { | |||
"actual": "10", | |||
"error": "1.0", | |||
"level": "OK", | |||
"metric": "new_bugs", | |||
"op": "GT", | |||
"period": 1, | |||
}, | |||
Object { | |||
"actual": "10", | |||
"error": "1.0", | |||
"level": "ERROR", | |||
"metric": "new_code_smells", | |||
"op": "GT", | |||
"period": 1, | |||
}, | |||
] | |||
} | |||
/> | |||
</div> | |||
<div | |||
className="pr-overview-failed-conditions big-spacer-right" | |||
@@ -1402,6 +1424,9 @@ exports[`should render correctly for a passed QG 1`] = ` | |||
} | |||
level="OK" | |||
/> | |||
<Connect(withCurrentUser(SonarLintPromotion)) | |||
qgConditions={Array []} | |||
/> | |||
</div> | |||
<div | |||
className="flex-1" |
@@ -30,6 +30,11 @@ | |||
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; | |||
@@ -249,6 +254,10 @@ | |||
color: var(--transparentWhite); | |||
} | |||
.pr-overview .overview-quality-gate-sonar-lint-info { | |||
width: 207px; | |||
} | |||
.pr-pverview .overview-measures-row { | |||
min-height: 85px; | |||
} |
@@ -0,0 +1,42 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 Icon, { IconProps } from './Icon'; | |||
export default function SonarLintIcon(props: IconProps) { | |||
return ( | |||
<Icon {...props} viewBox="0 0 512 512"> | |||
<defs> | |||
<path id="a" d="M0 0h512v512H0z" /> | |||
</defs> | |||
<clipPath id="b"> | |||
<use xlinkHref="#a" style={{ overflow: 'visible' }} /> | |||
</clipPath> | |||
<path | |||
d="M255.7 450.7c-107.8 0-195.5-87.6-195.5-195.3 0-107.7 87.7-195.3 195.5-195.3s195.5 87.6 195.5 195.3c0 107.7-87.7 195.3-195.5 195.3m0-355.2c-88.2 0-160 71.7-160 159.8 0 88.1 71.8 159.8 160 159.8s160-71.7 160-159.8c0-88.1-71.8-159.8-160-159.8" | |||
style={{ clipPath: 'url(#b)', fill: '#cb2029' }} | |||
/> | |||
<path | |||
d="M392.6 244.9c-5.1-10.2-11.5-23-24.1-23-12.5 0-18.9 12.7-24.1 23-4.8 9.5-7.5 14.5-13.5 14.5s-8.7-5-13.5-14.5c-5.1-10.2-11.5-23-24.1-23s-18.9 12.7-24.1 23c-4.8 9.5-7.5 14.5-13.5 14.5s-8.7-5-13.5-14.5c-5.1-10.2-11.5-23-24.1-23s-18.9 12.7-24.1 23c-4.8 9.5-7.5 14.5-13.5 14.5s-8.7-5-13.5-14.5c-5.1-10.2-11.5-23-24.1-23-12.5 0-18.9 12.1-24 22.3 0 0-3.9 8.7-5.6 12.8-1.7 4.1-8.3 19.7-2.3 27.2 3.4 4.2 8.3-1.8 11.1-6.2 2.1-3.4 7.1-13.2 7.1-13.2 5.1-9.1 7.8-14 13.7-14 6 0 8.7 5 13.5 14.5 5.1 10.2 11.5 23 24.1 23 12.5 0 18.9-12.7 24.1-23 4.8-9.5 7.5-14.5 13.5-14.5s8.7 5 13.5 14.5c5.1 10.2 11.5 23 24.1 23 12.5 0 18.9-12.7 24.1-23 4.8-9.5 7.5-14.5 13.5-14.5s8.7 5 13.5 14.5c5.1 10.2 11.5 23 24.1 23s18.9-12.7 24.1-23c4.8-9.5 7.5-14.5 13.5-14.5s8.7 5 13.5 14.5c0 0 2.2 4.3 5.4 9.9 1.4 2.3 6.6 11.1 11.2 10.8 4.1-.3 5.6-13.3 1-24.7-3.1-7.6-7-16.4-7-16.4z" | |||
style={{ fill: '#cb2029' }} | |||
/> | |||
</Icon> | |||
); | |||
} |
@@ -218,6 +218,7 @@ declare namespace T { | |||
export interface CurrentUser { | |||
isLoggedIn: boolean; | |||
permissions?: { global: string[] }; | |||
usingSonarLintConnectedMode?: boolean; | |||
} | |||
export interface CurrentUserSetting { |
@@ -2997,6 +2997,7 @@ system.version_is_availble={version} is available | |||
overview.failed_conditions=Failed conditions | |||
overview.X_more_failed_conditions={0} more failed conditions | |||
overview.X_conditions_failed={0} conditions failed | |||
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.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 |