]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19332 Warn and prevent parent NCD option selection if not compliant (#8326)
authorAndrey Luiz <andrey.luiz@sonarsource.com>
Tue, 23 May 2023 13:52:57 +0000 (15:52 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 23 May 2023 20:03:09 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx
server/sonar-web/src/main/js/components/controls/buttons.css
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 0560401c2a1b1074792aa0cd004a8b6091297bda..a386e12a2d1f4c3e5a170baa265b8fb3b41c6227 100644 (file)
@@ -26,6 +26,7 @@ import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { BranchWithNewCodePeriod } from '../../../types/branch-like';
 import { NewCodePeriod, NewCodePeriodSettingType } from '../../../types/types';
+import { isNewCodeDefinitionCompliant } from '../../../helpers/periods';
 
 export interface BranchListRowProps {
   branch: BranchWithNewCodePeriod;
@@ -98,6 +99,8 @@ export default function BranchListRow(props: BranchListRowProps) {
     );
   }
 
+  const isCompliant = isNewCodeDefinitionCompliant(inheritedSetting);
+
   return (
     <tr className={settingWarning ? 'branch-setting-warning' : ''}>
       <td className="nowrap">
@@ -125,7 +128,13 @@ export default function BranchListRow(props: BranchListRowProps) {
             {translate('edit')}
           </ActionsDropdownItem>
           {branch.newCodePeriod && (
-            <ActionsDropdownItem onClick={() => props.onResetToDefault(branch.name)}>
+            <ActionsDropdownItem
+              disabled={!isCompliant}
+              onClick={() => props.onResetToDefault(branch.name)}
+              tooltipOverlay={
+                isCompliant ? null : translate('project_baseline.compliance.warning.title.project')
+              }
+            >
               {translate('reset_to_default')}
             </ActionsDropdownItem>
           )}
index 5f7925f1149a6985e6a06f4f23acacea9fcebec9..0b3f8674abdb27e15b8bf4a17edd337826ad95a1 100644 (file)
@@ -281,6 +281,7 @@ class ProjectBaselineApp extends React.PureComponent<Props, State> {
                   branch={branchLike}
                   branchList={branchList}
                   branchesEnabled={branchSupportEnabled}
+                  canAdmin={appState.canAdmin}
                   component={component.key}
                   currentSetting={currentSetting}
                   currentSettingValue={currentSettingValue}
index 136f93cbabd5207e6846045639f7eacccbfa66d2..8c38d5b16e64c08f551222b40429601aaf3dbbe0 100644 (file)
@@ -33,12 +33,17 @@ import BaselineSettingDays, { BaselineSettingDaysSettingLevel } from './Baseline
 import BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion';
 import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
 import BranchAnalysisList from './BranchAnalysisList';
+import { isNewCodeDefinitionCompliant } from '../../../helpers/periods';
+import Tooltip from '../../../components/controls/Tooltip';
+import { FormattedMessage } from 'react-intl';
+import Link from '../../../components/common/Link';
 
 export interface ProjectBaselineSelectorProps {
   analysis?: string;
   branch: Branch;
   branchList: Branch[];
   branchesEnabled?: boolean;
+  canAdmin: boolean | undefined;
   component: string;
   currentSetting?: NewCodePeriodSettingType;
   currentSettingValue?: string;
@@ -94,6 +99,7 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
     branch,
     branchList,
     branchesEnabled,
+    canAdmin,
     component,
     currentSetting,
     currentSettingValue,
@@ -105,6 +111,8 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
     selected,
   } = props;
 
+  const isGeneralSettingCompliant = isNewCodeDefinitionCompliant(generalSetting);
+
   const { isChanged, isValid } = validateSetting({
     analysis,
     currentSetting,
@@ -121,12 +129,47 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
         <Radio
           checked={!overrideGeneralSetting}
           className="big-spacer-bottom"
+          disabled={!isGeneralSettingCompliant}
           onCheck={() => props.onToggleSpecificSetting(false)}
           value="general"
         >
-          {translate('project_baseline.general_setting')}
+          <Tooltip
+            overlay={
+              isGeneralSettingCompliant
+                ? null
+                : translate('project_baseline.compliance.warning.title.global')
+            }
+          >
+            <span>{translate('project_baseline.global_setting')}</span>
+          </Tooltip>
         </Radio>
         <div className="big-spacer-left">{renderGeneralSetting(generalSetting)}</div>
+        {!isGeneralSettingCompliant && (
+          <Alert className="sw-mt-10 sw-max-w-[700px]" variant="warning">
+            <p className="sw-mb-2 sw-font-bold">
+              {translate('project_baseline.compliance.warning.title.global')}
+            </p>
+            <p className="sw-mb-2">
+              {canAdmin ? (
+                <FormattedMessage
+                  id="project_baseline.compliance.warning.explanation.admin"
+                  defaultMessage={translate(
+                    'project_baseline.compliance.warning.explanation.admin'
+                  )}
+                  values={{
+                    link: (
+                      <Link to="/admin/settings?category=new_code_period">
+                        {translate('project_baseline.warning.explanation.action.admin.link')}
+                      </Link>
+                    ),
+                  }}
+                />
+              ) : (
+                translate('project_baseline.compliance.warning.explanation')
+              )}
+            </p>
+          </Alert>
+        )}
 
         <Radio
           checked={overrideGeneralSetting}
index c43b7d4080e79f152445fd08e0c719cb51fe24e6..577aa5be783aecb26e1d67b4d0595c8b2377a662 100644 (file)
@@ -33,6 +33,7 @@ import {
 } from '../../../../helpers/testReactTestingUtils';
 import { Feature } from '../../../../types/features';
 import routes from '../../routes';
+import { NewCodePeriodSettingType } from '../../../../types/types';
 
 jest.mock('../../../../api/newCodePeriod');
 jest.mock('../../../../api/projectActivity');
@@ -61,6 +62,39 @@ it('renders correctly without branch support feature', async () => {
   expect(ui.referenceBranchRadio.query()).not.toBeInTheDocument();
 });
 
+it('prevents selection of global setting if it is not compliant and warns non-admin about it', async () => {
+  codePeriodsMock.setNewCodePeriod({
+    type: NewCodePeriodSettingType.NUMBER_OF_DAYS,
+    value: '99',
+    inherited: true,
+  });
+
+  const { ui } = getPageObjects();
+  renderProjectBaselineApp();
+  await ui.appIsLoaded();
+
+  expect(ui.generalSettingRadio.get()).toBeChecked();
+  expect(ui.generalSettingRadio.get()).toHaveClass('disabled');
+  expect(ui.complianceWarning.get()).toBeVisible();
+});
+
+it('prevents selection of global setting if it is not compliant and warns admin about it', async () => {
+  codePeriodsMock.setNewCodePeriod({
+    type: NewCodePeriodSettingType.NUMBER_OF_DAYS,
+    value: '99',
+    inherited: true,
+  });
+
+  const { ui } = getPageObjects();
+  renderProjectBaselineApp({ appState: mockAppState({ canAdmin: true }) });
+  await ui.appIsLoaded();
+
+  expect(ui.generalSettingRadio.get()).toBeChecked();
+  expect(ui.generalSettingRadio.get()).toHaveClass('disabled');
+  expect(ui.complianceWarningAdmin.get()).toBeVisible();
+  expect(ui.complianceWarning.query()).not.toBeInTheDocument();
+});
+
 it('renders correctly with branch support feature', async () => {
   const { ui } = getPageObjects();
   renderProjectBaselineApp({
@@ -228,7 +262,7 @@ function getPageObjects() {
     pageHeading: byRole('heading', { name: 'project_baseline.page' }),
     branchListHeading: byRole('heading', { name: 'project_baseline.default_setting' }),
     generalSettingsLink: byRole('link', { name: 'project_baseline.page.description2.link' }),
-    generalSettingRadio: byRole('radio', { name: 'project_baseline.general_setting' }),
+    generalSettingRadio: byRole('radio', { name: 'project_baseline.global_setting' }),
     specificSettingRadio: byRole('radio', { name: 'project_baseline.specific_setting' }),
     previousVersionRadio: byRole('radio', { name: /baseline.previous_version.description/ }),
     numberDaysRadio: byRole('radio', { name: /baseline.number_days.description/ }),
@@ -245,6 +279,8 @@ function getPageObjects() {
     editButton: byRole('button', { name: 'edit' }),
     resetToDefaultButton: byRole('button', { name: 'reset_to_default' }),
     saved: byText('settings.state.saved'),
+    complianceWarningAdmin: byText('project_baseline.compliance.warning.explanation.admin'),
+    complianceWarning: byText('project_baseline.compliance.warning.explanation'),
   };
 
   async function appIsLoaded() {
index b01909c75acc0ae4cb495e7d91eec7d1904de1a6..480ebf1ecea40c880325f415d9bd537cd30f52cf 100644 (file)
@@ -66,6 +66,7 @@ interface ItemProps {
   className?: string;
   children: React.ReactNode;
   destructive?: boolean;
+  disabled?: boolean;
   label?: string;
   tooltipOverlay?: React.ReactNode;
   tooltipPlacement?: Placement;
@@ -114,6 +115,7 @@ export class ActionsDropdownItem extends React.PureComponent<ItemProps> {
       children = (
         <ButtonPlain
           className={className}
+          disabled={this.props.disabled}
           preventDefault={true}
           id={this.props.id}
           onClick={this.handleClick}
index e2ef99551124119a4c38fa799736e90ceb294afa..e303d1dc024faa909098550a244412d784d8c64e 100644 (file)
   border: 0;
 }
 
+.button-plain:disabled {
+  color: var(--disableGrayText) !important;
+  cursor: not-allowed !important;
+}
+
 /* #endregion */
 
 /* #region .button-link */
index c172cc4170835ccccd017842ddaca52ae1451c50..c55e43999370f233c0c37f9ad405a0ddcffbe6cf 100644 (file)
@@ -632,10 +632,16 @@ project_baseline.page.description2=You can adjust this setting globally in {link
 project_baseline.page.description2.link=General Settings
 project_baseline.page.question=What should be the baseline for new code for this project?
 project_baseline.default_setting=Project setting
-project_baseline.general_setting=Use the general setting
+project_baseline.global_setting=Use the global setting
 project_baseline.specific_setting=Define a specific setting for this project
 project_baseline.configure_branches=Set a specific setting for a branch
 
+project_baseline.compliance.warning.title.project=Your project new code definition is not compliant with the Clean as You Code methodology
+project_baseline.compliance.warning.title.global=Your global new code definition is not compliant with the Clean as You Code methodology
+project_baseline.compliance.warning.explanation=Please ask an administrator to update the global new code definition before switching back to it.
+project_baseline.compliance.warning.explanation.admin=Please update the global new code definition under {link} before switching back to it.
+project_baseline.warning.explanation.action.admin.link=General Settings > New Code
+
 baseline.previous_version=Previous version
 baseline.previous_version.usecase=Recommended for projects following regular versions or releases.
 baseline.previous_version.description=Any code that has changed since the previous version is considered new code.