From 82dd0377cad198fe680a3169506d02cd80e3fe84 Mon Sep 17 00:00:00 2001 From: Philippe Perrin Date: Tue, 16 May 2023 17:53:50 +0200 Subject: [PATCH] SONAR-19294 Restrict "number of days" NCD option available values --- .../components/BaselineSettingDays.tsx | 67 +++++++++++++++++-- .../components/BranchBaselineSettingModal.tsx | 3 +- .../components/ProjectBaselineSelector.tsx | 3 +- .../src/main/js/apps/projectBaseline/utils.ts | 13 ++-- .../settings/components/NewCodePeriod.tsx | 17 +++-- .../components/__tests__/NewCodePeriod-it.tsx | 28 +++++++- .../main/js/helpers/__tests__/periods-test.ts | 1 + .../sonar-web/src/main/js/helpers/periods.ts | 9 ++- .../resources/org/sonar/l10n/core.properties | 6 ++ 9 files changed, 120 insertions(+), 27 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx index b01d84952c2..f6f2e1ae084 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx @@ -18,13 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import withAvailableFeatures, { + WithAvailableFeaturesProps, +} from '../../../app/components/available-features/withAvailableFeatures'; import RadioCard from '../../../components/controls/RadioCard'; -import ValidationInput from '../../../components/controls/ValidationInput'; +import ValidationInput, { + ValidationInputErrorPlacement, +} from '../../../components/controls/ValidationInput'; +import { Alert } from '../../../components/ui/Alert'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; -import { translate } from '../../../helpers/l10n'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { MAX_NUMBER_OF_DAYS, MIN_NUMBER_OF_DAYS } from '../../../helpers/periods'; +import { Feature } from '../../../types/features'; import { NewCodePeriodSettingType } from '../../../types/types'; -export interface Props { +export interface Props extends WithAvailableFeaturesProps { className?: string; days: string; disabled?: boolean; @@ -33,10 +41,29 @@ export interface Props { onChangeDays: (value: string) => void; onSelect: (selection: NewCodePeriodSettingType) => void; selected: boolean; + settingLevel: BaselineSettingDaysSettingLevel; } -export default function BaselineSettingDays(props: Props) { - const { className, days, disabled, isChanged, isValid, onChangeDays, onSelect, selected } = props; +export enum BaselineSettingDaysSettingLevel { + Global = 'global', + Project = 'project', + Branch = 'branch', +} + +function BaselineSettingDays(props: Props) { + const { + className, + days, + disabled, + isChanged, + isValid, + onChangeDays, + onSelect, + selected, + settingLevel, + } = props; + const isBranchSupportEnabled = props.hasFeature(Feature.BranchSupport); + return ( onChangeDays(e.currentTarget.value)} type="text" value={days} /> + + {!isChanged && !isValid && ( + +

+ {translate('baseline.number_days.compliance_warning.title')} +

+

+ {translate( + `baseline.number_days.compliance_warning.content.${settingLevel}${ + isBranchSupportEnabled && + settingLevel === BaselineSettingDaysSettingLevel.Project + ? '.with_branch_support' + : '' + }` + )} +

+
+ )} )}
); } + +export default withAvailableFeatures(BaselineSettingDays); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx index 1c1d8822ce6..e2d8067e774 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx @@ -29,7 +29,7 @@ import { ParsedAnalysis } from '../../../types/project-activity'; import { NewCodePeriod, NewCodePeriodSettingType } from '../../../types/types'; import { getSettingValue, validateSetting } from '../utils'; import BaselineSettingAnalysis from './BaselineSettingAnalysis'; -import BaselineSettingDays from './BaselineSettingDays'; +import BaselineSettingDays, { BaselineSettingDaysSettingLevel } from './BaselineSettingDays'; import BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion'; import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch'; import BranchAnalysisList from './BranchAnalysisList'; @@ -175,6 +175,7 @@ export default class BranchBaselineSettingModal extends React.PureComponent {branchesEnabled ? ( 0) || - (selected === NewCodePeriodSettingType.NUMBER_OF_DAYS && validateDays(days)) || + (selected === NewCodePeriodSettingType.NUMBER_OF_DAYS && + isNewCodeDefinitionCompliant({ + type: NewCodePeriodSettingType.NUMBER_OF_DAYS, + value: days, + })) || (selected === NewCodePeriodSettingType.REFERENCE_BRANCH && referenceBranch.length > 0); return { isChanged, isValid }; diff --git a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx index ccd94da007e..fbac9119d87 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx @@ -25,10 +25,12 @@ import { ResetButtonLink, SubmitButton } from '../../../components/controls/butt import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; +import { isNewCodeDefinitionCompliant } from '../../../helpers/periods'; import { NewCodePeriodSettingType } from '../../../types/types'; -import BaselineSettingDays from '../../projectBaseline/components/BaselineSettingDays'; +import BaselineSettingDays, { + BaselineSettingDaysSettingLevel, +} from '../../projectBaseline/components/BaselineSettingDays'; import BaselineSettingPreviousVersion from '../../projectBaseline/components/BaselineSettingPreviousVersion'; -import { validateDays } from '../../projectBaseline/utils'; interface State { currentSetting?: NewCodePeriodSettingType; @@ -130,7 +132,9 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> { (selected === NewCodePeriodSettingType.NUMBER_OF_DAYS && String(days) !== currentSettingValue); - const isValid = selected !== NewCodePeriodSettingType.NUMBER_OF_DAYS || validateDays(days); + const isValid = + selected !== NewCodePeriodSettingType.NUMBER_OF_DAYS || + isNewCodeDefinitionCompliant({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: days }); return ( <> @@ -179,9 +183,7 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> {
- {loading ? ( - - ) : ( +
{ onChangeDays={this.onSelectDays} onSelect={this.onSelectSetting} selected={selected === NewCodePeriodSettingType.NUMBER_OF_DAYS} + settingLevel={BaselineSettingDaysSettingLevel.Global} /> {isChanged && (
@@ -220,7 +223,7 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> {
)} - )} +
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx index 8d355a23ccf..bc0bcedaa36 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx @@ -17,11 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { byRole, byText } from 'testing-library-selector'; import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; +import { NewCodePeriodSettingType } from '../../../../types/types'; import NewCodePeriod from '../NewCodePeriod'; let newCodeMock: NewCodePeriodsServiceMock; @@ -39,6 +41,7 @@ const ui = { savedMsg: byText('settings.state.saved'), prevVersionRadio: byRole('radio', { name: /baseline.previous_version/ }), daysNumberRadio: byRole('radio', { name: /baseline.number_days/ }), + daysNumberErrorMessage: byText('baseline.number_days.invalid', { exact: false }), daysInput: byRole('textbox'), saveButton: byRole('button', { name: 'save' }), cancelButton: byRole('button', { name: 'cancel' }), @@ -64,9 +67,9 @@ it('renders and behaves as expected', async () => { await user.clear(ui.daysInput.get()); await user.type(ui.daysInput.get(), 'asdas'); expect(ui.saveButton.get()).toBeDisabled(); - await user.clear(ui.daysInput.get()); // Save enabled for valid days number + await user.clear(ui.daysInput.get()); await user.type(ui.daysInput.get(), '10'); expect(ui.saveButton.get()).toBeEnabled(); @@ -76,6 +79,7 @@ it('renders and behaves as expected', async () => { // Can save change await user.click(ui.daysNumberRadio.get()); + await user.clear(ui.daysInput.get()); await user.type(ui.daysInput.get(), '10'); await user.click(ui.saveButton.get()); expect(ui.savedMsg.get()).toBeInTheDocument(); @@ -87,6 +91,28 @@ it('renders and behaves as expected', async () => { expect(ui.savedMsg.get()).toBeInTheDocument(); }); +it('renders and behaves properly when the current value is not compliant', async () => { + const user = userEvent.setup(); + newCodeMock.setNewCodePeriod({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '91' }); + renderNewCodePeriod(); + + expect(await ui.newCodeTitle.find()).toBeInTheDocument(); + expect(ui.daysNumberRadio.get()).toBeChecked(); + expect(ui.daysInput.get()).toHaveValue('91'); + + // Should warn about non compliant value + expect(ui.daysNumberErrorMessage.get()).toBeInTheDocument(); + expect(screen.getByRole('alert')).toHaveTextContent( + 'baseline.number_days.compliance_warning.title' + ); + + await user.clear(ui.daysInput.get()); + await user.type(ui.daysInput.get(), '92'); + + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + expect(ui.daysNumberErrorMessage.get()).toBeInTheDocument(); +}); + function renderNewCodePeriod() { return renderComponent(); } diff --git a/server/sonar-web/src/main/js/helpers/__tests__/periods-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/periods-test.ts index 73736fd7d7d..c6283e473ae 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/periods-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/periods-test.ts @@ -115,6 +115,7 @@ describe('isNewCodeDefinitionCompliant', () => { it.each([ [{ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '0' }, false], [{ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '15' }, true], + [{ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '15.3' }, false], [{ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '91' }, false], [{ type: NewCodePeriodSettingType.PREVIOUS_VERSION }, true], [{ type: NewCodePeriodSettingType.REFERENCE_BRANCH }, true], diff --git a/server/sonar-web/src/main/js/helpers/periods.ts b/server/sonar-web/src/main/js/helpers/periods.ts index 2ffa7a88a80..c973948b58f 100644 --- a/server/sonar-web/src/main/js/helpers/periods.ts +++ b/server/sonar-web/src/main/js/helpers/periods.ts @@ -69,14 +69,17 @@ export function isApplicationPeriod( return (period as ApplicationPeriod).project !== undefined; } -const MIN_NUMBER_OF_DAYS = 1; -const MAX_NUMBER_OF_DAYS = 90; +export const MIN_NUMBER_OF_DAYS = 1; +export const MAX_NUMBER_OF_DAYS = 90; export function isNewCodeDefinitionCompliant(newCodePeriod: NewCodePeriod) { switch (newCodePeriod.type) { case NewCodePeriodSettingType.NUMBER_OF_DAYS: + if (!newCodePeriod.value) { + return false; + } return ( - newCodePeriod.value !== undefined && + Number.isInteger(+newCodePeriod.value) && MIN_NUMBER_OF_DAYS <= +newCodePeriod.value && +newCodePeriod.value <= MAX_NUMBER_OF_DAYS ); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 19f4a4e8f7f..c172cc41708 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -642,6 +642,12 @@ baseline.previous_version.description=Any code that has changed since the previo baseline.number_days=Number of days baseline.number_days.usecase=Recommended for projects following continuous delivery. baseline.number_days.description=Any code that has changed in the last x days is considered new code. If no action is taken on a new issue after x days, this issue will become part of the overall code. +baseline.number_days.invalid=Please provide a whole number between {0} and {1} +baseline.number_days.compliance_warning.title=Update the number of days to benefit from the Clean as You Code methodology +baseline.number_days.compliance_warning.content.global=We recommend that you update this New Code definition so that new projects and existing projects that do not use a specific New Code definition benefit from the Clean as You Code methodology by default. +baseline.number_days.compliance_warning.content.project=We recommend that you update this New Code definition so that your project benefits from the Clean as You Code methodology. +baseline.number_days.compliance_warning.content.project.with_branch_support=We recommend that you update this New Code definition so that new branches and existing branches that do not use a specific New Code definition benefit from the Clean as You Code methodology by default. +baseline.number_days.compliance_warning.content.branch=We recommend that you update this New Code definition so that your branch benefits from the Clean as You Code methodology. baseline.specific_analysis=Specific analysis baseline.specific_analysis.description=Choose an analysis as the baseline for the new code. baseline.reference_branch=Reference branch -- 2.39.5