]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19294 Restrict "number of days" NCD option available values
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Tue, 16 May 2023 15:53:50 +0000 (17:53 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 22 May 2023 20:02:56 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
server/sonar-web/src/main/js/apps/projectBaseline/utils.ts
server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx
server/sonar-web/src/main/js/helpers/__tests__/periods-test.ts
server/sonar-web/src/main/js/helpers/periods.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index b01d84952c20d7e30a9516187550838942f952e1..f6f2e1ae0844a2b3fceb599ab8857a3dcff76e46 100644 (file)
  * 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 (
     <RadioCard
       className={className}
@@ -55,22 +82,48 @@ export default function BaselineSettingDays(props: Props) {
             <MandatoryFieldsExplanation />
 
             <ValidationInput
-              error={undefined}
               labelHtmlFor="baseline_number_of_days"
-              isInvalid={isChanged && !isValid}
+              isInvalid={!isValid}
               isValid={isChanged && isValid}
+              errorPlacement={ValidationInputErrorPlacement.Bottom}
+              error={translateWithParameters(
+                'baseline.number_days.invalid',
+                MIN_NUMBER_OF_DAYS,
+                MAX_NUMBER_OF_DAYS
+              )}
               label={translate('baseline.specify_days')}
               required={true}
             >
               <input
+                id="baseline_number_of_days"
                 onChange={(e) => onChangeDays(e.currentTarget.value)}
                 type="text"
                 value={days}
               />
             </ValidationInput>
+
+            {!isChanged && !isValid && (
+              <Alert variant="warning" className="sw-mt-2">
+                <p className="sw-mb-2 sw-font-bold">
+                  {translate('baseline.number_days.compliance_warning.title')}
+                </p>
+                <p className="sw-mb-2">
+                  {translate(
+                    `baseline.number_days.compliance_warning.content.${settingLevel}${
+                      isBranchSupportEnabled &&
+                      settingLevel === BaselineSettingDaysSettingLevel.Project
+                        ? '.with_branch_support'
+                        : ''
+                    }`
+                  )}
+                </p>
+              </Alert>
+            )}
           </>
         )}
       </>
     </RadioCard>
   );
 }
+
+export default withAvailableFeatures(BaselineSettingDays);
index 1c1d8822ce61e83c9b589a14d631d98646d490f7..e2d8067e774aeed1a3538403c57cbcf6cb0cbf00 100644 (file)
@@ -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<Prop
                 onChangeDays={this.handleSelectDays}
                 onSelect={this.handleSelectSetting}
                 selected={selected === NewCodePeriodSettingType.NUMBER_OF_DAYS}
+                settingLevel={BaselineSettingDaysSettingLevel.Branch}
               />
               <BaselineSettingAnalysis
                 onSelect={this.handleSelectSetting}
index 3010e74e10196d29bcd13cab99070293f6403d30..136f93cbabd5207e6846045639f7eacccbfa66d2 100644 (file)
@@ -29,7 +29,7 @@ import { ParsedAnalysis } from '../../../types/project-activity';
 import { NewCodePeriod, NewCodePeriodSettingType } from '../../../types/types';
 import { 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';
@@ -157,6 +157,7 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
             selected={
               overrideGeneralSetting && selected === NewCodePeriodSettingType.NUMBER_OF_DAYS
             }
+            settingLevel={BaselineSettingDaysSettingLevel.Project}
           />
           {branchesEnabled ? (
             <BaselineSettingReferenceBranch
index 2ca20b85b1e9fba26dabc710953e3ab4335ad920..e40c4d896707c7b1ac13d737c71bc1f1c9c039fb 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { isNewCodeDefinitionCompliant } from '../../helpers/periods';
 import { NewCodePeriodSettingType } from '../../types/types';
 
-export function validateDays(days: string) {
-  const parsed = parseInt(days, 10);
-
-  return !(days.length < 1 || isNaN(parsed) || parsed < 1 || String(parsed) !== days);
-}
-
 export function getSettingValue({
   analysis,
   days,
@@ -85,7 +80,11 @@ export function validateSetting(state: {
     overrideGeneralSetting === false ||
     selected === NewCodePeriodSettingType.PREVIOUS_VERSION ||
     (selected === NewCodePeriodSettingType.SPECIFIC_ANALYSIS && analysis.length > 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 };
index ccd94da007e967ee5910d448283b406fe4450c90..fbac9119d87f795a9f1df7b33f7cbbc3e0fb4162 100644 (file)
@@ -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> {
                   </div>
 
                   <div className="settings-definition-right">
-                    {loading ? (
-                      <DeferredSpinner />
-                    ) : (
+                    <DeferredSpinner loading={loading} timeout={500}>
                       <form onSubmit={this.onSubmit}>
                         <BaselineSettingPreviousVersion
                           isDefault={true}
@@ -196,6 +198,7 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> {
                           onChangeDays={this.onSelectDays}
                           onSelect={this.onSelectSetting}
                           selected={selected === NewCodePeriodSettingType.NUMBER_OF_DAYS}
+                          settingLevel={BaselineSettingDaysSettingLevel.Global}
                         />
                         {isChanged && (
                           <div className="big-spacer-top">
@@ -220,7 +223,7 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> {
                           </div>
                         )}
                       </form>
-                    )}
+                    </DeferredSpinner>
                   </div>
                 </div>
               </li>
index 8d355a23ccf93f2932d83fa9c047a8102373dd56..bc0bcedaa365a5688449cf3f8cf3393cfc757131 100644 (file)
  * 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(<NewCodePeriod />);
 }
index 73736fd7d7d527af5d5de13d7902f55d1c4a494c..c6283e473ae59997649f5b4ec8228b58666fd029 100644 (file)
@@ -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],
index 2ffa7a88a8034deebf43f986585e337f3c6825d2..c973948b58fd65ba19b6d599cd75ed3a37b68fe4 100644 (file)
@@ -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
       );
index 19f4a4e8f7fd35f3edd31da2b777df117afe647d..c172cc4170835ccccd017842ddaca52ae1451c50 100644 (file)
@@ -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