@@ -143,26 +143,33 @@ export class QualityGatesServiceMock { | |||
mockQualityGate({ | |||
name: 'Sonar way', | |||
conditions: [ | |||
{ id: 'AXJMbIUHPAOIsUIE3eNs', metric: 'new_security_rating', op: 'GT', error: '1' }, | |||
{ id: 'AXJMbIUHPAOIsUIE3eOD', metric: 'new_reliability_rating', op: 'GT', error: '1' }, | |||
{ | |||
id: 'AXJMbIUHPAOIsUIE3eOE', | |||
metric: 'new_maintainability_rating', | |||
id: 'AXJMbIUHPAOIsUIE3eNs', | |||
metric: 'new_violations', | |||
op: 'GT', | |||
error: '1', | |||
error: '0', | |||
isCaycCondition: true, | |||
}, | |||
{ | |||
id: 'AXJMbIUHPAOIsUIE3eOF', | |||
metric: 'new_coverage', | |||
op: 'LT', | |||
error: '80', | |||
isCaycCondition: true, | |||
}, | |||
{ id: 'AXJMbIUHPAOIsUIE3eOF', metric: 'new_coverage', op: 'LT', error: '80' }, | |||
{ | |||
id: 'AXJMbIUHPAOIsUIE3eOG', | |||
metric: 'new_duplicated_lines_density', | |||
op: 'GT', | |||
error: '3', | |||
isCaycCondition: true, | |||
}, | |||
{ | |||
id: 'AXJMbIUHPAOIsUIE3eOk', | |||
metric: 'new_security_hotspots_reviewed', | |||
op: 'LT', | |||
error: '100', | |||
isCaycCondition: true, | |||
}, | |||
], | |||
isDefault: false, |
@@ -44,7 +44,7 @@ function CaycCondition({ condition, metric, metrics }: Readonly<Props>) { | |||
}; | |||
return ( | |||
<TableRow> | |||
<StyledTableRow> | |||
<ContentCell | |||
data-guiding-id={ | |||
condition.metric === MetricKey.new_violations ? 'caycConditionsSimplification' : undefined | |||
@@ -74,10 +74,19 @@ function CaycCondition({ condition, metric, metrics }: Readonly<Props>) { | |||
</> | |||
)} | |||
</StyledContentCell> | |||
</TableRow> | |||
</StyledTableRow> | |||
); | |||
} | |||
const StyledTableRow = styled(TableRow)` | |||
&:first-child > td { | |||
border-top: 0; | |||
} | |||
&:last-child > td { | |||
border-bottom: 0; | |||
} | |||
`; | |||
const StyledContentCell = styled(ContentCell)` | |||
white-space: nowrap; | |||
@@ -0,0 +1,32 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { CheckIcon, LightLabel } from 'design-system'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default function CaycConditionsListItem({ metricKey }: Readonly<{ metricKey: string }>) { | |||
return ( | |||
<li> | |||
<CheckIcon className="sw-mr-1 sw-pt-1/2" /> | |||
<LightLabel>{translate(`metric.${metricKey}.description.positive`)}</LightLabel> | |||
</li> | |||
); | |||
} |
@@ -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 { Table } from 'design-system'; | |||
import { HighlightedSection, Table } from 'design-system'; | |||
import * as React from 'react'; | |||
import { Condition as ConditionType, Dict, Metric } from '../../../types/types'; | |||
import CaycCondition from './CaycCondition'; | |||
@@ -29,19 +29,21 @@ interface Props { | |||
export default function CaycConditionsTable({ metrics, conditions }: Readonly<Props>) { | |||
return ( | |||
<Table | |||
columnCount={2} | |||
columnWidths={['auto', '1fr']} | |||
className="sw-my-2" | |||
data-testid="quality-gates__conditions-cayc" | |||
> | |||
{conditions.map((condition) => ( | |||
<CaycCondition | |||
key={condition.id} | |||
condition={condition} | |||
metric={metrics[condition.metric]} | |||
/> | |||
))} | |||
</Table> | |||
<HighlightedSection className="sw-px-4 sw-py-0 sw-my-2"> | |||
<Table | |||
columnCount={2} | |||
columnWidths={['auto', '1fr']} | |||
className="sw-my-2" | |||
data-testid="quality-gates__conditions-cayc" | |||
> | |||
{conditions.map((condition) => ( | |||
<CaycCondition | |||
key={condition.id} | |||
condition={condition} | |||
metric={metrics[condition.metric]} | |||
/> | |||
))} | |||
</Table> | |||
</HighlightedSection> | |||
); | |||
} |
@@ -33,9 +33,10 @@ import { Condition, Metric } from '../../../types/types'; | |||
import { GreenColorText } from './ConditionValue'; | |||
const NO_DESCRIPTION_CONDITION = [ | |||
'new_security_hotspots_reviewed', | |||
'new_coverage', | |||
'new_duplicated_lines_density', | |||
MetricKey.new_violations, | |||
MetricKey.new_security_hotspots_reviewed, | |||
MetricKey.new_coverage, | |||
MetricKey.new_duplicated_lines_density, | |||
]; | |||
interface Props { | |||
@@ -82,15 +83,19 @@ function ConditionValueDescription({ | |||
return ( | |||
<GreenColorText isToBeModified={isToBeModified}> | |||
{condition.isCaycCondition && !NO_DESCRIPTION_CONDITION.includes(condition.metric) && ( | |||
<> | |||
( | |||
{translate( | |||
`quality_gates.cayc.${condition.metric}.${formatMeasure(condition.error, metric.type)}`, | |||
)} | |||
) | |||
</> | |||
)} | |||
{condition.isCaycCondition && | |||
!NO_DESCRIPTION_CONDITION.includes(condition.metric as MetricKey) && ( | |||
<> | |||
( | |||
{translate( | |||
`quality_gates.cayc.${condition.metric}.${formatMeasure( | |||
condition.error, | |||
metric.type, | |||
)}`, | |||
)} | |||
) | |||
</> | |||
)} | |||
</GreenColorText> | |||
); | |||
} |
@@ -19,6 +19,7 @@ | |||
*/ | |||
import { | |||
ButtonPrimary, | |||
ButtonSecondary, | |||
CardWithPrimaryBackground, | |||
FlagMessage, | |||
@@ -28,8 +29,7 @@ import { | |||
Link, | |||
Note, | |||
SubHeading, | |||
SubnavigationFlowSeparator, | |||
Title, | |||
SubHeadingHighlight, | |||
} from 'design-system'; | |||
import { differenceWith, map, uniqBy } from 'lodash'; | |||
import * as React from 'react'; | |||
@@ -51,8 +51,9 @@ import { | |||
Metric, | |||
QualityGate, | |||
} from '../../../types/types'; | |||
import { groupAndSortByPriorityConditions } from '../utils'; | |||
import { CAYC_CONDITIONS, groupAndSortByPriorityConditions } from '../utils'; | |||
import CaYCConditionsSimplificationGuide from './CaYCConditionsSimplificationGuide'; | |||
import CaycConditionsListItem from './CaycConditionsListItem'; | |||
import CaycConditionsTable from './CaycConditionsTable'; | |||
import ConditionModal from './ConditionModal'; | |||
import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal'; | |||
@@ -87,15 +88,12 @@ export function Conditions({ | |||
const [editing, setEditing] = React.useState<boolean>( | |||
qualityGate.caycStatus === CaycStatus.NonCompliant, | |||
); | |||
const isQGCompliant = | |||
qualityGate.caycStatus === CaycStatus.Compliant || | |||
qualityGate.caycStatus === CaycStatus.OverCompliant; | |||
const { name } = qualityGate; | |||
const canEdit = Boolean(qualityGate.actions?.manageConditions); | |||
const { conditions = [] } = qualityGate; | |||
const existingConditions = conditions.filter((condition) => metrics[condition.metric]); | |||
const { overallCodeConditions, newCodeConditions, caycConditions } = | |||
groupAndSortByPriorityConditions(existingConditions, metrics, isQGCompliant); | |||
groupAndSortByPriorityConditions(existingConditions, metrics, qualityGate.isBuiltIn); | |||
const duplicates: ConditionType[] = []; | |||
const savedConditions = existingConditions.filter((condition) => condition.id != null); | |||
@@ -172,12 +170,39 @@ export function Conditions({ | |||
<div> | |||
<CaYCConditionsSimplificationGuide /> | |||
{qualityGate.caycStatus !== CaycStatus.NonCompliant && !qualityGate.isBuiltIn && ( | |||
<CardWithPrimaryBackground className="sw-mb-9 sw-p-8"> | |||
<SubHeadingHighlight className="sw-mb-2"> | |||
{translate('quality_gates.cayc.banner.title')} | |||
</SubHeadingHighlight> | |||
<div> | |||
<FormattedMessage | |||
id="quality_gates.cayc.banner.description1" | |||
defaultMessage={translate('quality_gates.cayc.banner.description1')} | |||
values={{ | |||
cayc_link: ( | |||
<Link to={getDocUrl('/user-guide/clean-as-you-code/')}> | |||
{translate('quality_gates.cayc')} | |||
</Link> | |||
), | |||
}} | |||
/> | |||
</div> | |||
<div className="sw-my-2">{translate('quality_gates.cayc.banner.description2')}</div> | |||
<ul className="sw-body-sm sw-flex sw-flex-col sw-gap-2"> | |||
{Object.values(CAYC_CONDITIONS).map((condition) => ( | |||
<CaycConditionsListItem key={condition.metric} metricKey={condition.metric} /> | |||
))} | |||
</ul> | |||
</CardWithPrimaryBackground> | |||
)} | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && ( | |||
<CardWithPrimaryBackground className="sw-mb-9 sw-p-8"> | |||
<Title as="h2" className="sw-mb-2 sw-heading-md"> | |||
<SubHeadingHighlight className="sw-mb-2"> | |||
{translate('quality_gates.cayc_missing.banner.title')} | |||
</Title> | |||
<SubHeading className="sw-body-sm sw-mb-4"> | |||
</SubHeadingHighlight> | |||
<div> | |||
<FormattedMessage | |||
id="quality_gates.cayc_missing.banner.description" | |||
defaultMessage={translate('quality_gates.cayc_missing.banner.description')} | |||
@@ -189,14 +214,13 @@ export function Conditions({ | |||
), | |||
}} | |||
/> | |||
</SubHeading> | |||
<SubnavigationFlowSeparator className="sw-m-0" /> | |||
</div> | |||
{canEdit && ( | |||
<ModalButton modal={renderCaycModal}> | |||
{({ onClick }) => ( | |||
<ButtonSecondary className="sw-mt-4" onClick={onClick}> | |||
<ButtonPrimary className="sw-mt-4" onClick={onClick}> | |||
{translate('quality_gates.cayc_condition.review_update')} | |||
</ButtonSecondary> | |||
</ButtonPrimary> | |||
)} | |||
</ModalButton> | |||
)} | |||
@@ -208,6 +232,20 @@ export function Conditions({ | |||
<HeadingDark className="sw-body-md-highlight sw-m-0"> | |||
{translate('quality_gates.conditions')} | |||
</HeadingDark> | |||
{!qualityGate.isBuiltIn && ( | |||
<DocumentationTooltip | |||
className="sw-ml-2" | |||
content={translate('quality_gates.conditions.help')} | |||
links={[ | |||
{ | |||
href: '/user-guide/clean-as-you-code/', | |||
label: translate('quality_gates.conditions.help.link'), | |||
}, | |||
]} | |||
> | |||
<HelperHintIcon /> | |||
</DocumentationTooltip> | |||
)} | |||
</div> | |||
<div> | |||
{(qualityGate.caycStatus === CaycStatus.NonCompliant || editing) && canEdit && ( |
@@ -92,7 +92,7 @@ export default function DetailsHeader({ | |||
<> | |||
<div className="it__layout-page-main-header sw-flex sw-items-center sw-justify-between sw-mb-9"> | |||
<div className="sw-flex sw-flex-col"> | |||
<div className="sw-flex sw-items-center"> | |||
<div className="sw-flex sw-items-baseline"> | |||
<SubTitle className="sw-m-0">{qualityGate.name}</SubTitle> | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && ( | |||
<Tooltip overlay={<CaycBadgeTooltip />} mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}> |
@@ -370,19 +370,7 @@ it('should show warning banner when CAYC condition is not properly set and shoul | |||
screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' }), | |||
); | |||
const conditionsWrapper = within(await screen.findByTestId('quality-gates__conditions-cayc')); | |||
expect( | |||
conditionsWrapper.getByText('metric.new_violations.description.positive'), | |||
).toBeInTheDocument(); | |||
expect( | |||
conditionsWrapper.getByText('metric.new_security_hotspots_reviewed.description.positive'), | |||
).toBeInTheDocument(); | |||
expect( | |||
conditionsWrapper.getByText('metric.new_coverage.description.positive'), | |||
).toBeInTheDocument(); | |||
expect( | |||
conditionsWrapper.getByText('metric.new_duplicated_lines_density.description.positive'), | |||
).toBeInTheDocument(); | |||
expect(await screen.findByText('quality_gates.cayc.banner.title')).toBeInTheDocument(); | |||
const overallConditionsWrapper = within( | |||
await screen.findByTestId('quality-gates__conditions-overall'), | |||
@@ -415,12 +403,14 @@ it('should warn user when quality gate is not CAYC compliant and user has permis | |||
expect(screen.getAllByText('quality_gates.cayc.tooltip.message').length).toBeGreaterThan(0); | |||
}); | |||
it('should render CaYC conditions on a separate table', async () => { | |||
it('should render CaYC conditions on a separate table if Sonar way', async () => { | |||
const user = userEvent.setup(); | |||
qualityGateHandler.setIsAdmin(true); | |||
renderQualityGateApp(); | |||
await user.click(await screen.findByText('Sonar way')); | |||
expect(screen.queryByText('quality_gates.cayc.banner.title')).not.toBeInTheDocument(); | |||
expect(await screen.findByTestId('quality-gates__conditions-cayc')).toBeInTheDocument(); | |||
expect(await screen.findByTestId('quality-gates__conditions-new')).toBeInTheDocument(); | |||
}); | |||
it('should display CaYC condition simplification tour for users who didnt dismissed it', async () => { | |||
@@ -428,7 +418,7 @@ it('should display CaYC condition simplification tour for users who didnt dismis | |||
qualityGateHandler.setIsAdmin(true); | |||
renderQualityGateApp({ currentUser: mockLoggedInUser() }); | |||
const qualityGate = await screen.findByText('SonarSource way'); | |||
const qualityGate = await screen.findByText('Sonar way'); | |||
await act(async () => { | |||
await user.click(qualityGate); | |||
@@ -440,7 +430,7 @@ it('should display CaYC condition simplification tour for users who didnt dismis | |||
await user.click(byRole('alertdialog').byRole('button', { name: 'dismiss' }).get()); | |||
}); | |||
expect(await byRole('alertdialog').query()).not.toBeInTheDocument(); | |||
expect(byRole('alertdialog').query()).not.toBeInTheDocument(); | |||
expect(dismissNotice).toHaveBeenLastCalledWith(NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION); | |||
}); | |||
@@ -453,13 +443,13 @@ it('should not display CaYC condition simplification tour for users who dismisse | |||
}), | |||
}); | |||
const qualityGate = await screen.findByText('SonarSource way'); | |||
const qualityGate = await screen.findByText('Sonar way'); | |||
await act(async () => { | |||
await user.click(qualityGate); | |||
}); | |||
expect(await byRole('alertdialog').query()).not.toBeInTheDocument(); | |||
expect(byRole('alertdialog').query()).not.toBeInTheDocument(); | |||
}); | |||
describe('The Project section', () => { |
@@ -34,7 +34,10 @@ type CaycMetricKeys = | |||
| MetricKey.new_coverage | |||
| MetricKey.new_duplicated_lines_density; | |||
const CAYC_CONDITIONS: Record<CaycMetricKeys, Condition & { shouldRenderOperator?: boolean }> = { | |||
export const CAYC_CONDITIONS: Record< | |||
CaycMetricKeys, | |||
Condition & { shouldRenderOperator?: boolean } | |||
> = { | |||
[MetricKey.new_violations]: { | |||
id: MetricKey.new_violations, | |||
metric: MetricKey.new_violations, | |||
@@ -130,12 +133,12 @@ export function getCaycConditionsWithCorrectValue(conditions: Condition[]) { | |||
export function groupConditionsByMetric( | |||
conditions: Condition[], | |||
isQGCompliant: boolean, | |||
isBuiltInQG = false, | |||
): GroupedByMetricConditions { | |||
return conditions.reduce( | |||
(result, condition) => { | |||
const isNewCode = isDiffMetric(condition.metric); | |||
if (condition.isCaycCondition && isQGCompliant) { | |||
if (condition.isCaycCondition && isBuiltInQG) { | |||
result.caycConditions.push(condition); | |||
} else if (isNewCode) { | |||
result.newCodeConditions.push(condition); | |||
@@ -156,9 +159,9 @@ export function groupConditionsByMetric( | |||
export function groupAndSortByPriorityConditions( | |||
conditions: Condition[], | |||
metrics: Dict<Metric>, | |||
isQGCompliant: boolean, | |||
isBuiltInQG = false, | |||
): GroupedByMetricConditions { | |||
const groupedConditions = groupConditionsByMetric(conditions, isQGCompliant); | |||
const groupedConditions = groupConditionsByMetric(conditions, isBuiltInQG); | |||
function sortFn(a: Condition, b: Condition) { | |||
const priorityA = CAYC_CONDITION_ORDER_PRIORITIES[a.metric] ?? 0; |
@@ -2176,12 +2176,12 @@ quality_gates.conditions.cayc.hint=The conditions below must be true for your pr | |||
quality_gates.conditions.cayc.threshold.hint=Sonar recommends this threshold. Create a new Quality Gate to set a different value. | |||
quality_gates.conditions.new_code=On New Code | |||
quality_gates.conditions.new_code.long=Conditions on New Code | |||
quality_gates.conditions.new_code.description=Conditions on New Code apply to all branches and to Pull Requests. | |||
quality_gates.conditions.new_code.description=Conditions on new code apply to all branches and to Pull Requests. | |||
quality_gates.conditions.new_code_1=1 condition failed on new code | |||
quality_gates.conditions.new_code_x={0} conditions failed on new code | |||
quality_gates.conditions.overall_code=On Overall Code | |||
quality_gates.conditions.overall_code.long=Conditions on Overall Code | |||
quality_gates.conditions.overall_code.description=Conditions on Overall Code apply to branches only. | |||
quality_gates.conditions.overall_code.description=Conditions on overall code apply to branches only. | |||
quality_gates.conditions.overall_code_1=1 condition failed on overall code | |||
quality_gates.conditions.overall_code_x={0} conditions failed on overall code | |||
quality_gates.conditions.operator=Operator | |||
@@ -2224,9 +2224,12 @@ quality_gates.cayc.new_security_rating.A=No vulnerabilities | |||
quality_gates.cayc.unlock_edit=Unlock editing | |||
quality_gates.cayc.tooltip.message=This quality gate does not comply with Clean as You Code. | |||
quality_gates.cayc.badge.tooltip.learn_more=Learn more: Clean as You Code | |||
quality_gates.cayc.banner.title=This quality gate complies with Clean as You Code | |||
quality_gates.cayc.banner.description1=This quality gate complies with the {cayc_link} methodology, so that you benefit from the most efficient approach to delivering Clean Code. | |||
quality_gates.cayc.banner.description2=It ensures that: | |||
quality_gates.cayc_unfollow.description=You may click unlock to edit this quality gate. Adding extra conditions to a compliant quality gate can result in drawbacks. Are you reconsidering {cayc_link}? We strongly recommend this methodology to achieve a Clean Code status. | |||
quality_gates.cayc.review_update_modal.add_condition.header= {0} condition(s) on New Code will be added | |||
quality_gates.cayc.review_update_modal.modify_condition.header= {0} condition(s) on New Code will be modified | |||
quality_gates.cayc.review_update_modal.add_condition.header= {0} condition(s) on new code will be added | |||
quality_gates.cayc.review_update_modal.modify_condition.header= {0} condition(s) on new code will be modified | |||
#------------------------------------------------------------------------------ | |||
# |