@@ -125,6 +125,13 @@ export class QualityGatesServiceMock { | |||
mockQualityGate({ | |||
name: 'SonarSource way - CFamily', | |||
conditions: [ | |||
{ | |||
id: 'AXJMbIUHPAOIsUIE3eOi', | |||
metric: 'new_security_hotspots_reviewed', | |||
op: 'LT', | |||
error: '85', | |||
isCaycCondition: true, | |||
}, | |||
{ | |||
id: 'AXJMbIUHPAOIsUIE3eOu', | |||
metric: 'new_coverage', | |||
@@ -187,6 +194,17 @@ export class QualityGatesServiceMock { | |||
isBuiltIn: false, | |||
caycStatus: CaycStatus.NonCompliant, | |||
}), | |||
mockQualityGate({ | |||
name: 'Non Cayc Compliant QG', | |||
conditions: [ | |||
{ id: 'AXJMbIUHPAOIsUIE3eNs', metric: 'new_security_rating', op: 'GT', error: '1' }, | |||
{ id: 'AXJMbIUHPAOIsUIE3eOD', metric: 'new_reliability_rating', op: 'GT', error: '1' }, | |||
{ id: 'AXJMbIUHPAOIsUIE3eOF', metric: 'new_coverage', op: 'LT', error: '80' }, | |||
], | |||
isDefault: false, | |||
isBuiltIn: false, | |||
caycStatus: CaycStatus.Compliant, | |||
}), | |||
mockQualityGate({ | |||
name: 'Over Compliant CAYC QG', | |||
conditions: [ |
@@ -0,0 +1,62 @@ | |||
/* | |||
* 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 { | |||
CardWithPrimaryBackground, | |||
CheckIcon, | |||
LightLabel, | |||
SubHeadingHighlight, | |||
} from 'design-system'; | |||
import React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import DocumentationLink from '../../../components/common/DocumentationLink'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { OPTIMIZED_CAYC_CONDITIONS } from '../utils'; | |||
export default function CaycCompliantBanner() { | |||
return ( | |||
<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" | |||
values={{ | |||
cayc_link: ( | |||
<DocumentationLink to="/user-guide/clean-as-you-code/"> | |||
{translate('quality_gates.cayc')} | |||
</DocumentationLink> | |||
), | |||
}} | |||
/> | |||
</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(OPTIMIZED_CAYC_CONDITIONS).map((condition) => ( | |||
<li key={condition.metric}> | |||
<CheckIcon className="sw-mr-1 sw-pt-1/2" /> | |||
<LightLabel>{translate(`metric.${condition.metric}.description.positive`)}</LightLabel> | |||
</li> | |||
))} | |||
</ul> | |||
</CardWithPrimaryBackground> | |||
); | |||
} |
@@ -1,32 +0,0 @@ | |||
/* | |||
* 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> | |||
); | |||
} |
@@ -0,0 +1,71 @@ | |||
/* | |||
* 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 { ButtonPrimary, CardWithPrimaryBackground, SubHeadingHighlight } from 'design-system'; | |||
import React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import DocumentationLink from '../../../components/common/DocumentationLink'; | |||
import ModalButton from '../../../components/controls/ModalButton'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
renderCaycModal: ({ onClose }: { onClose: () => void }) => React.ReactNode; | |||
isOptimizing?: boolean; | |||
} | |||
export default function CaycNonCompliantBanner({ renderCaycModal, isOptimizing }: Readonly<Props>) { | |||
return ( | |||
<CardWithPrimaryBackground className="sw-mb-9 sw-p-8"> | |||
<SubHeadingHighlight className="sw-mb-2"> | |||
{translate( | |||
isOptimizing | |||
? 'quality_gates.cayc_optimize.banner.title' | |||
: 'quality_gates.cayc_missing.banner.title', | |||
)} | |||
</SubHeadingHighlight> | |||
<div> | |||
<FormattedMessage | |||
id={ | |||
isOptimizing | |||
? 'quality_gates.cayc_optimize.banner.description' | |||
: 'quality_gates.cayc_missing.banner.description' | |||
} | |||
values={{ | |||
cayc_link: ( | |||
<DocumentationLink to="/user-guide/clean-as-you-code/"> | |||
{translate('quality_gates.cayc')} | |||
</DocumentationLink> | |||
), | |||
}} | |||
/> | |||
</div> | |||
<ModalButton modal={renderCaycModal}> | |||
{({ onClick }) => ( | |||
<ButtonPrimary className="sw-mt-4" onClick={onClick}> | |||
{translate( | |||
isOptimizing | |||
? 'quality_gates.cayc_condition.review_optimize' | |||
: 'quality_gates.cayc_condition.review_update', | |||
)} | |||
</ButtonPrimary> | |||
)} | |||
</ModalButton> | |||
</CardWithPrimaryBackground> | |||
); | |||
} |
@@ -40,6 +40,7 @@ interface Props { | |||
onSaveCondition: (newCondition: Condition, oldCondition: Condition) => void; | |||
lockEditing: () => void; | |||
qualityGate: QualityGate; | |||
isOptimizing?: boolean; | |||
} | |||
export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>) { | |||
@@ -51,6 +52,7 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>) | |||
onAddCondition, | |||
lockEditing, | |||
onClose, | |||
isOptimizing, | |||
} = props; | |||
const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions); | |||
@@ -108,8 +110,11 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>) | |||
<div className="sw-mb-10"> | |||
<SubHeading as="p" className="sw-body-sm"> | |||
<FormattedMessage | |||
id="quality_gates.cayc.review_update_modal.description1" | |||
defaultMessage={translate('quality_gates.cayc.review_update_modal.description1')} | |||
id={ | |||
isOptimizing | |||
? 'quality_gates.cayc.review_optimize_modal.description1' | |||
: 'quality_gates.cayc.review_update_modal.description1' | |||
} | |||
values={{ | |||
cayc_link: ( | |||
<Link to={getDocUrl('/user-guide/clean-as-you-code/')}> | |||
@@ -164,7 +169,9 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>) | |||
<Modal | |||
isLarge | |||
headerTitle={translateWithParameters( | |||
'quality_gates.cayc.review_update_modal.header', | |||
isOptimizing | |||
? 'quality_gates.cayc.review_optimize_modal.header' | |||
: 'quality_gates.cayc.review_update_modal.header', | |||
qualityGate.name, | |||
)} | |||
onClose={onClose} | |||
@@ -176,7 +183,11 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>) | |||
type="submit" | |||
onClick={updateCaycQualityGate} | |||
> | |||
{translate('quality_gates.cayc.review_update_modal.confirm_text')} | |||
{translate( | |||
isOptimizing | |||
? 'quality_gates.cayc.review_optimize_modal.confirm_text' | |||
: 'quality_gates.cayc.review_update_modal.confirm_text', | |||
)} | |||
</ButtonPrimary> | |||
} | |||
secondaryButtonLabel={translate('close')} |
@@ -18,17 +18,9 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { | |||
GRID_INDEX_OFFSET, | |||
PERCENT_MULTIPLIER, | |||
getMaintainabilityGrid, | |||
} from '../../../helpers/ratings'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { MetricKey, MetricType } from '../../../types/metrics'; | |||
import { GlobalSettingKeys } from '../../../types/settings'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { Condition, Metric } from '../../../types/types'; | |||
import { GreenColorText } from './ConditionValue'; | |||
@@ -37,50 +29,22 @@ const NO_DESCRIPTION_CONDITION = [ | |||
MetricKey.new_security_hotspots_reviewed, | |||
MetricKey.new_coverage, | |||
MetricKey.new_duplicated_lines_density, | |||
MetricKey.new_reliability_rating, | |||
MetricKey.new_security_rating, | |||
MetricKey.new_maintainability_rating, | |||
]; | |||
interface Props { | |||
appState: AppState; | |||
condition: Condition; | |||
metric: Metric; | |||
isToBeModified?: boolean; | |||
} | |||
function ConditionValueDescription({ | |||
export default function ConditionValueDescription({ | |||
condition, | |||
appState: { settings }, | |||
metric, | |||
isToBeModified = false, | |||
}: Readonly<Props>) { | |||
if (condition.metric === MetricKey.new_maintainability_rating) { | |||
const maintainabilityGrid = getMaintainabilityGrid( | |||
settings[GlobalSettingKeys.RatingGrid] ?? '', | |||
); | |||
const maintainabilityRatingThreshold = | |||
maintainabilityGrid[Math.floor(Number(condition.error)) - GRID_INDEX_OFFSET]; | |||
const ratingLetter = formatMeasure(condition.error, MetricType.Rating); | |||
return ( | |||
<GreenColorText isToBeModified={isToBeModified}> | |||
( | |||
{condition.error === '1' | |||
? translateWithParameters( | |||
'quality_gates.cayc.new_maintainability_rating.A', | |||
formatMeasure(maintainabilityGrid[0] * PERCENT_MULTIPLIER, MetricType.Percent), | |||
) | |||
: translateWithParameters( | |||
'quality_gates.cayc.new_maintainability_rating', | |||
ratingLetter, | |||
formatMeasure( | |||
maintainabilityRatingThreshold * PERCENT_MULTIPLIER, | |||
MetricType.Percent, | |||
), | |||
)} | |||
) | |||
</GreenColorText> | |||
); | |||
} | |||
return ( | |||
<GreenColorText isToBeModified={isToBeModified}> | |||
{condition.isCaycCondition && | |||
@@ -99,5 +63,3 @@ function ConditionValueDescription({ | |||
</GreenColorText> | |||
); | |||
} | |||
export default withAppStateContext(ConditionValueDescription); |
@@ -19,9 +19,7 @@ | |||
*/ | |||
import { | |||
ButtonPrimary, | |||
ButtonSecondary, | |||
CardWithPrimaryBackground, | |||
FlagMessage, | |||
HeadingDark, | |||
HelperHintIcon, | |||
@@ -29,7 +27,6 @@ import { | |||
Link, | |||
Note, | |||
SubHeading, | |||
SubHeadingHighlight, | |||
} from 'design-system'; | |||
import { differenceWith, map, uniqBy } from 'lodash'; | |||
import * as React from 'react'; | |||
@@ -51,10 +48,11 @@ import { | |||
Metric, | |||
QualityGate, | |||
} from '../../../types/types'; | |||
import { CAYC_CONDITIONS, groupAndSortByPriorityConditions } from '../utils'; | |||
import { groupAndSortByPriorityConditions, isQualityGateOptimized } from '../utils'; | |||
import CaYCConditionsSimplificationGuide from './CaYCConditionsSimplificationGuide'; | |||
import CaycConditionsListItem from './CaycConditionsListItem'; | |||
import CaycCompliantBanner from './CaycCompliantBanner'; | |||
import CaycConditionsTable from './CaycConditionsTable'; | |||
import CaycFixOptimizeBanner from './CaycFixOptimizeBanner'; | |||
import ConditionModal from './ConditionModal'; | |||
import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal'; | |||
import ConditionsTable from './ConditionsTable'; | |||
@@ -76,7 +74,7 @@ const FORBIDDEN_METRICS: string[] = [ | |||
MetricKey.new_security_hotspots, | |||
]; | |||
export function Conditions({ | |||
function Conditions({ | |||
qualityGate, | |||
metrics, | |||
onRemoveCondition, | |||
@@ -109,8 +107,6 @@ export function Conditions({ | |||
metric: metrics[condition.metric], | |||
})); | |||
const getDocUrl = useDocUrl(); | |||
// set edit only when the name is change | |||
// i.e when user changes the quality gate | |||
React.useEffect(() => { | |||
@@ -143,6 +139,11 @@ export function Conditions({ | |||
[metrics, qualityGate, onAddCondition], | |||
); | |||
const getDocUrl = useDocUrl(); | |||
const isCompliantCustomQualityGate = | |||
qualityGate.caycStatus !== CaycStatus.NonCompliant && !qualityGate.isBuiltIn; | |||
const isOptimizing = isCompliantCustomQualityGate && !isQualityGateOptimized(qualityGate); | |||
const renderCaycModal = React.useCallback( | |||
({ onClose }: ModalProps) => { | |||
const { conditions = [] } = qualityGate; | |||
@@ -160,71 +161,31 @@ export function Conditions({ | |||
conditions={conditions} | |||
scope="new-cayc" | |||
onClose={onClose} | |||
isOptimizing={isOptimizing} | |||
/> | |||
); | |||
}, | |||
[qualityGate, metrics, updatedConditionId, onAddCondition, onRemoveCondition, onSaveCondition], | |||
[ | |||
qualityGate, | |||
metrics, | |||
updatedConditionId, | |||
onAddCondition, | |||
onRemoveCondition, | |||
onSaveCondition, | |||
isOptimizing, | |||
], | |||
); | |||
return ( | |||
<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> | |||
{isCompliantCustomQualityGate && !isOptimizing && <CaycCompliantBanner />} | |||
{isCompliantCustomQualityGate && isOptimizing && canEdit && ( | |||
<CaycFixOptimizeBanner renderCaycModal={renderCaycModal} isOptimizing /> | |||
)} | |||
{qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && ( | |||
<CardWithPrimaryBackground className="sw-mb-9 sw-p-8"> | |||
<SubHeadingHighlight className="sw-mb-2"> | |||
{translate('quality_gates.cayc_missing.banner.title')} | |||
</SubHeadingHighlight> | |||
<div> | |||
<FormattedMessage | |||
id="quality_gates.cayc_missing.banner.description" | |||
defaultMessage={translate('quality_gates.cayc_missing.banner.description')} | |||
values={{ | |||
cayc_link: ( | |||
<Link to={getDocUrl('/user-guide/clean-as-you-code/')}> | |||
{translate('quality_gates.cayc')} | |||
</Link> | |||
), | |||
}} | |||
/> | |||
</div> | |||
{canEdit && ( | |||
<ModalButton modal={renderCaycModal}> | |||
{({ onClick }) => ( | |||
<ButtonPrimary className="sw-mt-4" onClick={onClick}> | |||
{translate('quality_gates.cayc_condition.review_update')} | |||
</ButtonPrimary> | |||
)} | |||
</ModalButton> | |||
)} | |||
</CardWithPrimaryBackground> | |||
<CaycFixOptimizeBanner renderCaycModal={renderCaycModal} /> | |||
)} | |||
<header className="sw-flex sw-items-center sw-mb-4 sw-justify-between"> |
@@ -379,6 +379,44 @@ it('should show warning banner when CAYC condition is not properly set and shoul | |||
expect(overallConditionsWrapper.getByText('Complexity / Function')).toBeInTheDocument(); | |||
}); | |||
it('should show optimize banner when CAYC condition is not properly set and QG is compliant and should be able to update them', async () => { | |||
const user = userEvent.setup(); | |||
qualityGateHandler.setIsAdmin(true); | |||
renderQualityGateApp(); | |||
const qualityGate = await screen.findByText('Non Cayc Compliant QG'); | |||
await user.click(qualityGate); | |||
expect(screen.getByText('quality_gates.cayc_optimize.banner.title')).toBeInTheDocument(); | |||
expect(screen.getByText('quality_gates.cayc_optimize.banner.description')).toBeInTheDocument(); | |||
expect( | |||
screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_optimize' }), | |||
).toBeInTheDocument(); | |||
await user.click( | |||
screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_optimize' }), | |||
); | |||
expect( | |||
screen.getByRole('dialog', { | |||
name: 'quality_gates.cayc.review_optimize_modal.header.Non Cayc Compliant QG', | |||
}), | |||
).toBeInTheDocument(); | |||
expect( | |||
screen.getByText('quality_gates.cayc.review_optimize_modal.description1'), | |||
).toBeInTheDocument(); | |||
expect( | |||
screen.getByText('quality_gates.cayc.review_update_modal.description2'), | |||
).toBeInTheDocument(); | |||
expect( | |||
screen.getByRole('button', { name: 'quality_gates.cayc.review_optimize_modal.confirm_text' }), | |||
).toBeInTheDocument(); | |||
await user.click( | |||
screen.getByRole('button', { name: 'quality_gates.cayc.review_optimize_modal.confirm_text' }), | |||
); | |||
}); | |||
it('should not warn user when quality gate is not CAYC compliant and user has no permission to edit it', async () => { | |||
const user = userEvent.setup(); | |||
renderQualityGateApp(); | |||
@@ -391,6 +429,20 @@ it('should not warn user when quality gate is not CAYC compliant and user has no | |||
expect(screen.queryByText('quality_gates.cayc.tooltip.message')).not.toBeInTheDocument(); | |||
}); | |||
it('should not show optimize banner when quality gate is compliant but non-CaYC and user has no permission to edit it', async () => { | |||
const user = userEvent.setup(); | |||
renderQualityGateApp(); | |||
const nonCompliantQualityGate = await screen.findByRole('button', { | |||
name: 'Non Cayc Compliant QG', | |||
}); | |||
await user.click(nonCompliantQualityGate); | |||
expect(screen.queryByRole('alert')).not.toBeInTheDocument(); | |||
expect(screen.queryByText('quality_gates.cayc.tooltip.message')).not.toBeInTheDocument(); | |||
}); | |||
it('should warn user when quality gate is not CAYC compliant and user has permission to edit it', async () => { | |||
const user = userEvent.setup(); | |||
qualityGateHandler.setIsAdmin(true); | |||
@@ -399,11 +451,28 @@ it('should warn user when quality gate is not CAYC compliant and user has permis | |||
const nonCompliantQualityGate = await screen.findByRole('button', { name: /Non Cayc QG/ }); | |||
await user.click(nonCompliantQualityGate); | |||
// expect(screen.getByTestId('conditions')).toMatchSnapshot(); | |||
expect(await screen.findByText(/quality_gates.cayc_missing.banner.title/)).toBeInTheDocument(); | |||
expect(screen.getAllByText('quality_gates.cayc.tooltip.message').length).toBeGreaterThan(0); | |||
}); | |||
it('should show optimize banner when quality gate is compliant but non-CaYC and user has permission to edit it', async () => { | |||
const user = userEvent.setup(); | |||
qualityGateHandler.setIsAdmin(true); | |||
renderQualityGateApp(); | |||
const nonCompliantQualityGate = await screen.findByRole('button', { | |||
name: /Non Cayc Compliant QG/, | |||
}); | |||
await user.click(nonCompliantQualityGate); | |||
// expect(screen.getByTestId('conditions')).toMatchSnapshot(); | |||
expect(await screen.findByText(/quality_gates.cayc_optimize.banner.title/)).toBeInTheDocument(); | |||
expect(screen.getAllByText('quality_gates.cayc.tooltip.message').length).toBeGreaterThan(0); | |||
}); | |||
it('should render CaYC conditions on a separate table if Sonar way', async () => { | |||
const user = userEvent.setup(); | |||
qualityGateHandler.setIsAdmin(true); |
@@ -28,23 +28,25 @@ interface GroupedByMetricConditions { | |||
caycConditions: Condition[]; | |||
} | |||
type CaycMetricKeys = | |||
| MetricKey.new_violations | |||
type CommonCaycMetricKeys = | |||
| MetricKey.new_security_hotspots_reviewed | |||
| MetricKey.new_coverage | |||
| MetricKey.new_duplicated_lines_density; | |||
export const CAYC_CONDITIONS: Record< | |||
CaycMetricKeys, | |||
type OptimizedCaycMetricKeys = MetricKey.new_violations | CommonCaycMetricKeys; | |||
type UnoptimizedCaycMetricKeys = | |||
| MetricKey.new_reliability_rating | |||
| MetricKey.new_security_rating | |||
| MetricKey.new_maintainability_rating | |||
| CommonCaycMetricKeys; | |||
type AllCaycMetricKeys = OptimizedCaycMetricKeys | UnoptimizedCaycMetricKeys; | |||
const COMMON_CONDITIONS: Record< | |||
CommonCaycMetricKeys, | |||
Condition & { shouldRenderOperator?: boolean } | |||
> = { | |||
[MetricKey.new_violations]: { | |||
id: MetricKey.new_violations, | |||
metric: MetricKey.new_violations, | |||
op: 'GT', | |||
error: '0', | |||
isCaycCondition: true, | |||
}, | |||
[MetricKey.new_security_hotspots_reviewed]: { | |||
id: MetricKey.new_security_hotspots_reviewed, | |||
metric: MetricKey.new_security_hotspots_reviewed, | |||
@@ -70,6 +72,56 @@ export const CAYC_CONDITIONS: Record< | |||
}, | |||
}; | |||
export const OPTIMIZED_CAYC_CONDITIONS: Record< | |||
OptimizedCaycMetricKeys, | |||
Condition & { shouldRenderOperator?: boolean } | |||
> = { | |||
[MetricKey.new_violations]: { | |||
id: MetricKey.new_violations, | |||
metric: MetricKey.new_violations, | |||
op: 'GT', | |||
error: '0', | |||
isCaycCondition: true, | |||
}, | |||
...COMMON_CONDITIONS, | |||
}; | |||
const UNOPTIMIZED_CAYC_CONDITIONS: Record< | |||
UnoptimizedCaycMetricKeys, | |||
Condition & { shouldRenderOperator?: boolean } | |||
> = { | |||
[MetricKey.new_reliability_rating]: { | |||
id: MetricKey.new_reliability_rating, | |||
metric: MetricKey.new_reliability_rating, | |||
op: 'GT', | |||
error: '1', | |||
isCaycCondition: true, | |||
}, | |||
[MetricKey.new_security_rating]: { | |||
id: MetricKey.new_security_rating, | |||
metric: MetricKey.new_security_rating, | |||
op: 'GT', | |||
error: '1', | |||
isCaycCondition: true, | |||
}, | |||
[MetricKey.new_maintainability_rating]: { | |||
id: MetricKey.new_maintainability_rating, | |||
metric: MetricKey.new_maintainability_rating, | |||
op: 'GT', | |||
error: '1', | |||
isCaycCondition: true, | |||
}, | |||
...COMMON_CONDITIONS, | |||
}; | |||
const ALL_CAYC_CONDITIONS: Record< | |||
AllCaycMetricKeys, | |||
Condition & { shouldRenderOperator?: boolean } | |||
> = { | |||
...OPTIMIZED_CAYC_CONDITIONS, | |||
...UNOPTIMIZED_CAYC_CONDITIONS, | |||
}; | |||
const CAYC_CONDITION_ORDER_PRIORITIES: Dict<number> = [ | |||
MetricKey.new_violations, | |||
MetricKey.new_security_hotspots_reviewed, | |||
@@ -79,26 +131,50 @@ const CAYC_CONDITION_ORDER_PRIORITIES: Dict<number> = [ | |||
.reverse() | |||
.reduce((acc, key, i) => ({ ...acc, [key.toString()]: i + 1 }), {} as Dict<number>); | |||
const CAYC_CONDITIONS_WITHOUT_FIXED_VALUE: CaycMetricKeys[] = [ | |||
const CAYC_CONDITIONS_WITHOUT_FIXED_VALUE: AllCaycMetricKeys[] = [ | |||
MetricKey.new_duplicated_lines_density, | |||
MetricKey.new_coverage, | |||
]; | |||
const CAYC_CONDITIONS_WITH_FIXED_VALUE: CaycMetricKeys[] = [ | |||
const CAYC_CONDITIONS_WITH_FIXED_VALUE: AllCaycMetricKeys[] = [ | |||
MetricKey.new_security_hotspots_reviewed, | |||
MetricKey.new_violations, | |||
MetricKey.new_reliability_rating, | |||
MetricKey.new_security_rating, | |||
MetricKey.new_maintainability_rating, | |||
]; | |||
export function isConditionWithFixedValue(condition: Condition) { | |||
return CAYC_CONDITIONS_WITH_FIXED_VALUE.includes(condition.metric as CaycMetricKeys); | |||
return CAYC_CONDITIONS_WITH_FIXED_VALUE.includes(condition.metric as OptimizedCaycMetricKeys); | |||
} | |||
export function getCaycConditionMetadata(condition: Condition) { | |||
const foundCondition = CAYC_CONDITIONS[condition.metric as CaycMetricKeys]; | |||
const foundCondition = OPTIMIZED_CAYC_CONDITIONS[condition.metric as OptimizedCaycMetricKeys]; | |||
return { | |||
shouldRenderOperator: foundCondition?.shouldRenderOperator, | |||
}; | |||
} | |||
export function isQualityGateOptimized(qualityGate: QualityGate) { | |||
return ( | |||
!qualityGate.isBuiltIn && | |||
qualityGate.caycStatus !== CaycStatus.NonCompliant && | |||
Object.values(OPTIMIZED_CAYC_CONDITIONS).every((condition) => { | |||
const foundCondition = qualityGate.conditions?.find((c) => c.metric === condition.metric); | |||
return ( | |||
foundCondition && | |||
!isWeakCondition(condition.metric as OptimizedCaycMetricKeys, foundCondition) | |||
); | |||
}) | |||
); | |||
} | |||
function isWeakCondition(key: AllCaycMetricKeys, selectedCondition: Condition) { | |||
return ( | |||
!CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) && | |||
ALL_CAYC_CONDITIONS[key]?.error !== selectedCondition.error | |||
); | |||
} | |||
export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) { | |||
const result: { | |||
weakConditions: Condition[]; | |||
@@ -107,31 +183,18 @@ export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) { | |||
weakConditions: [], | |||
missingConditions: [], | |||
}; | |||
Object.keys(CAYC_CONDITIONS).forEach((key: CaycMetricKeys) => { | |||
Object.keys(OPTIMIZED_CAYC_CONDITIONS).forEach((key: OptimizedCaycMetricKeys) => { | |||
const selectedCondition = conditions.find((condition) => condition.metric === key); | |||
if (!selectedCondition) { | |||
result.missingConditions.push(CAYC_CONDITIONS[key]); | |||
} else if ( | |||
!CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) && | |||
CAYC_CONDITIONS[key]?.error !== selectedCondition.error | |||
) { | |||
result.missingConditions.push(OPTIMIZED_CAYC_CONDITIONS[key]); | |||
} else if (isWeakCondition(key, selectedCondition)) { | |||
result.weakConditions.push(selectedCondition); | |||
} | |||
}); | |||
return result; | |||
} | |||
export function getCaycConditionsWithCorrectValue(conditions: Condition[]) { | |||
return Object.keys(CAYC_CONDITIONS).map((key: CaycMetricKeys) => { | |||
const selectedCondition = conditions.find((condition) => condition.metric === key); | |||
if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) && selectedCondition) { | |||
return selectedCondition; | |||
} | |||
return CAYC_CONDITIONS[key]; | |||
}); | |||
} | |||
export function groupConditionsByMetric( | |||
function groupConditionsByMetric( | |||
conditions: Condition[], | |||
isBuiltInQG = false, | |||
): GroupedByMetricConditions { | |||
@@ -183,11 +246,11 @@ export function groupAndSortByPriorityConditions( | |||
} | |||
export function getCorrectCaycCondition(condition: Condition) { | |||
const conditionMetric = condition.metric as CaycMetricKeys; | |||
const conditionMetric = condition.metric as OptimizedCaycMetricKeys; | |||
if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(conditionMetric)) { | |||
return condition; | |||
} | |||
return CAYC_CONDITIONS[conditionMetric]; | |||
return OPTIMIZED_CAYC_CONDITIONS[conditionMetric]; | |||
} | |||
export function addCondition(qualityGate: QualityGate, condition: Condition): QualityGate { | |||
@@ -225,32 +288,27 @@ export function replaceCondition( | |||
return { ...qualityGate, conditions }; | |||
} | |||
export function updateCaycCompliantStatus(conditions: Condition[]) { | |||
if (conditions.length < Object.keys(CAYC_CONDITIONS).length) { | |||
return CaycStatus.NonCompliant; | |||
} | |||
for (const key of Object.keys(CAYC_CONDITIONS)) { | |||
const caycMetric = key as CaycMetricKeys; | |||
const selectedCondition = conditions.find((condition) => condition.metric === key); | |||
if (!selectedCondition) { | |||
return CaycStatus.NonCompliant; | |||
} | |||
if ( | |||
!CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(caycMetric) && | |||
selectedCondition && | |||
selectedCondition.error !== CAYC_CONDITIONS[caycMetric].error | |||
) { | |||
return CaycStatus.NonCompliant; | |||
} | |||
} | |||
function updateCaycCompliantStatus(conditions: Condition[]) { | |||
const isCompliantOptimized = Object.values(OPTIMIZED_CAYC_CONDITIONS).every((condition) => { | |||
const foundCondition = conditions.find((c) => c.metric === condition.metric); | |||
return ( | |||
foundCondition && | |||
!isWeakCondition(condition.metric as OptimizedCaycMetricKeys, foundCondition) | |||
); | |||
}); | |||
const isCompliantUnoptimized = Object.values(UNOPTIMIZED_CAYC_CONDITIONS).every((condition) => { | |||
const foundCondition = conditions.find((c) => c.metric === condition.metric); | |||
return ( | |||
foundCondition && | |||
!isWeakCondition(condition.metric as UnoptimizedCaycMetricKeys, foundCondition) | |||
); | |||
}); | |||
if (conditions.length > Object.keys(CAYC_CONDITIONS).length) { | |||
return CaycStatus.OverCompliant; | |||
if (isCompliantOptimized || isCompliantUnoptimized) { | |||
return CaycStatus.Compliant; | |||
} | |||
return CaycStatus.Compliant; | |||
return CaycStatus.NonCompliant; | |||
} | |||
export function getPossibleOperators(metric: Metric) { |
@@ -2221,6 +2221,12 @@ quality_gates.cayc.review_update_modal.header=Fix "{0}" to comply with Clean as | |||
quality_gates.cayc.review_update_modal.confirm_text=Fix Quality Gate | |||
quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Please review the changes below. | |||
quality_gates.cayc.review_update_modal.description2=All other conditions will be preserved | |||
quality_gates.cayc_optimize.banner.title=This quality gate can be further optimized for Clean as You Code | |||
quality_gates.cayc_optimize.banner.description=This quality gate complies with the {cayc_link} methodology, but it can be further optimized to ensure that new code has 0 issues. | |||
quality_gates.cayc_condition.review_optimize=Review and Optimize Quality Gate | |||
quality_gates.cayc.review_optimize_modal.header=Optimize "{0}" for Clean as You Code | |||
quality_gates.cayc.review_optimize_modal.confirm_text=Optimize Quality Gate | |||
quality_gates.cayc.review_optimize_modal.description1=This quality gate will be optimized for {cayc_link}. Please review the changes below. | |||
quality_gates.cayc.condition_simplification_tour.page_1.title='Clean as You Code' ready! | |||
quality_gates.cayc.condition_simplification_tour.page_1.content1=The conditions in this quality gate have been updated to ensure that any code added or changed is clean. | |||
quality_gates.cayc.condition_simplification_tour.page_2.title=One condition, zero issues |