Browse Source

SONAR-20604 Enforce CaYC compliance on the SonarWay Quality Gate (#9616)

tags/10.3.0.82913
Andrey Luiz 8 months ago
parent
commit
1bcecd02ed

+ 13
- 6
server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts View File

@@ -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,

+ 11
- 2
server/sonar-web/src/main/js/apps/quality-gates/components/CaycCondition.tsx View File

@@ -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;


+ 32
- 0
server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx View File

@@ -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
- 15
server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsTable.tsx View File

@@ -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>
);
}

+ 17
- 12
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx View File

@@ -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>
);
}

+ 52
- 14
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx View File

@@ -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 && (

+ 1
- 1
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx View File

@@ -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}>

+ 9
- 19
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx View File

@@ -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', () => {

+ 8
- 5
server/sonar-web/src/main/js/apps/quality-gates/utils.ts View File

@@ -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;

+ 7
- 4
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save