]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20176 Add banner to notify about automatic branches NCD update
authorAmbroise C <ambroise.christea@sonarsource.com>
Fri, 18 Aug 2023 14:14:28 +0000 (16:14 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 24 Aug 2023 20:03:07 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchList.tsx
server/sonar-web/src/main/js/apps/projectNewCode/components/__tests__/ProjectNewCodeDefinitionApp-it.tsx
server/sonar-web/src/main/js/components/new-code-definition/BranchNCDAutoUpdateMessage.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx
server/sonar-web/src/main/js/components/new-code-definition/utils.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 8b4f0fac7d676050a0b5fad1689dac4296da3cc2..3d75feda42eb76b54025044016b08c54585829ba 100644 (file)
@@ -22,6 +22,11 @@ import {
   listBranchesNewCodeDefinition,
   resetNewCodeDefinition,
 } from '../../../api/newCodeDefinition';
+import BranchNCDAutoUpdateMessage from '../../../components/new-code-definition/BranchNCDAutoUpdateMessage';
+import {
+  PreviouslyNonCompliantBranchNCD,
+  isPreviouslyNonCompliantDaysNCD,
+} from '../../../components/new-code-definition/utils';
 import Spinner from '../../../components/ui/Spinner';
 import { isBranch, sortBranches } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
@@ -43,6 +48,7 @@ interface State {
   branches: BranchWithNewCodePeriod[];
   editedBranch?: BranchWithNewCodePeriod;
   loading: boolean;
+  previouslyNonCompliantBranchNCDs?: PreviouslyNonCompliantBranchNCD[];
 }
 
 export default class BranchList extends React.PureComponent<Props, State> {
@@ -93,7 +99,15 @@ export default class BranchList extends React.PureComponent<Props, State> {
           };
         });
 
-        this.setState({ branches: branchesWithBaseline, loading: false });
+        const previouslyNonCompliantBranchNCDs = newCodePeriods.filter(
+          isPreviouslyNonCompliantDaysNCD
+        );
+
+        this.setState({
+          branches: branchesWithBaseline,
+          loading: false,
+          previouslyNonCompliantBranchNCDs,
+        });
       },
       () => {
         this.setState({ loading: false });
@@ -116,11 +130,14 @@ export default class BranchList extends React.PureComponent<Props, State> {
   };
 
   closeEditModal = (branch?: string, newSetting?: NewCodeDefinition) => {
-    if (branch) {
-      this.setState({
+    if (branch !== undefined) {
+      this.setState(({ previouslyNonCompliantBranchNCDs }) => ({
         branches: this.updateBranchNewCodePeriod(branch, newSetting),
+        previouslyNonCompliantBranchNCDs: previouslyNonCompliantBranchNCDs?.filter(
+          ({ branchKey }) => branchKey !== branch
+        ),
         editedBranch: undefined,
-      });
+      }));
     } else {
       this.setState({ editedBranch: undefined });
     }
@@ -136,8 +153,8 @@ export default class BranchList extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { branchList, inheritedSetting, globalNewCodeDefinition } = this.props;
-    const { branches, editedBranch, loading } = this.state;
+    const { branchList, component, inheritedSetting, globalNewCodeDefinition } = this.props;
+    const { branches, editedBranch, loading, previouslyNonCompliantBranchNCDs } = this.state;
 
     if (branches.length < 1) {
       return null;
@@ -148,7 +165,13 @@ export default class BranchList extends React.PureComponent<Props, State> {
     }
 
     return (
-      <>
+      <div>
+        {previouslyNonCompliantBranchNCDs && (
+          <BranchNCDAutoUpdateMessage
+            component={component}
+            previouslyNonCompliantBranchNCDs={previouslyNonCompliantBranchNCDs}
+          />
+        )}
         <table className="data zebra">
           <thead>
             <tr>
@@ -182,7 +205,7 @@ export default class BranchList extends React.PureComponent<Props, State> {
             globalNewCodeDefinition={globalNewCodeDefinition}
           />
         )}
-      </>
+      </div>
     );
   }
 }
index 3a3a60974b762b390b5aec7a00b72dc5c0c8a819..084e134f393cecdc517a1844e3c1e855b95f601a 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 { within } from '@testing-library/react';
+import { act, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { first, last } from 'lodash';
 import selectEvent from 'react-select-event';
+import { MessageTypes } from '../../../../api/messages';
 import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
+import MessagesServiceMock from '../../../../api/mocks/MessagesServiceMock';
 import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
 import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
 import { mockComponent } from '../../../../helpers/mocks/component';
@@ -31,7 +33,7 @@ import {
   RenderContext,
   renderAppWithComponentContext,
 } from '../../../../helpers/testReactTestingUtils';
-import { byRole, byText } from '../../../../helpers/testSelector';
+import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
 import { Feature } from '../../../../types/features';
 import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
 import routes from '../../routes';
@@ -40,19 +42,21 @@ jest.mock('../../../../api/newCodeDefinition');
 jest.mock('../../../../api/projectActivity');
 jest.mock('../../../../api/branches');
 
-const codePeriodsMock = new NewCodeDefinitionServiceMock();
+const newCodeDefinitionMock = new NewCodeDefinitionServiceMock();
 const projectActivityMock = new ProjectActivityServiceMock();
 const branchHandler = new BranchesServiceMock();
+const messagesMock = new MessagesServiceMock();
 
 afterEach(() => {
   branchHandler.reset();
-  codePeriodsMock.reset();
+  newCodeDefinitionMock.reset();
   projectActivityMock.reset();
+  messagesMock.reset();
 });
 
 it('renders correctly without branch support feature', async () => {
   const { ui } = getPageObjects();
-  renderProjectBaselineApp();
+  renderProjectNewCodeDefinitionApp();
   await ui.appIsLoaded();
 
   expect(await ui.generalSettingRadio.find()).toBeChecked();
@@ -67,14 +71,14 @@ it('renders correctly without branch support feature', async () => {
 });
 
 it('prevents selection of global setting if it is not compliant and warns non-admin about it', async () => {
-  codePeriodsMock.setNewCodePeriod({
+  newCodeDefinitionMock.setNewCodePeriod({
     type: NewCodeDefinitionType.NumberOfDays,
     value: '99',
     inherited: true,
   });
 
   const { ui } = getPageObjects();
-  renderProjectBaselineApp();
+  renderProjectNewCodeDefinitionApp();
   await ui.appIsLoaded();
 
   expect(await ui.generalSettingRadio.find()).toBeChecked();
@@ -83,14 +87,14 @@ it('prevents selection of global setting if it is not compliant and warns non-ad
 });
 
 it('prevents selection of global setting if it is not compliant and warns admin about it', async () => {
-  codePeriodsMock.setNewCodePeriod({
+  newCodeDefinitionMock.setNewCodePeriod({
     type: NewCodeDefinitionType.NumberOfDays,
     value: '99',
     inherited: true,
   });
 
   const { ui } = getPageObjects();
-  renderProjectBaselineApp({ appState: mockAppState({ canAdmin: true }) });
+  renderProjectNewCodeDefinitionApp({ appState: mockAppState({ canAdmin: true }) });
   await ui.appIsLoaded();
 
   expect(await ui.generalSettingRadio.find()).toBeChecked();
@@ -101,7 +105,7 @@ it('prevents selection of global setting if it is not compliant and warns admin
 
 it('renders correctly with branch support feature', async () => {
   const { ui } = getPageObjects();
-  renderProjectBaselineApp({
+  renderProjectNewCodeDefinitionApp({
     featureList: [Feature.BranchSupport],
     appState: mockAppState({ canAdmin: true }),
   });
@@ -120,7 +124,7 @@ it('renders correctly with branch support feature', async () => {
 
 it('can set previous version specific setting', async () => {
   const { ui, user } = getPageObjects();
-  renderProjectBaselineApp();
+  renderProjectNewCodeDefinitionApp();
   await ui.appIsLoaded();
 
   expect(await ui.previousVersionRadio.find()).toHaveClass('disabled');
@@ -141,7 +145,7 @@ it('can set previous version specific setting', async () => {
 
 it('can set number of days specific setting', async () => {
   const { ui, user } = getPageObjects();
-  renderProjectBaselineApp();
+  renderProjectNewCodeDefinitionApp();
   await ui.appIsLoaded();
 
   expect(await ui.numberDaysRadio.find()).toHaveClass('disabled');
@@ -162,7 +166,7 @@ it('can set number of days specific setting', async () => {
 
 it('can set reference branch specific setting', async () => {
   const { ui, user } = getPageObjects();
-  renderProjectBaselineApp({
+  renderProjectNewCodeDefinitionApp({
     featureList: [Feature.BranchSupport],
   });
   await ui.appIsLoaded();
@@ -179,11 +183,11 @@ it('can set reference branch specific setting', async () => {
 
 it('cannot set specific analysis setting', async () => {
   const { ui } = getPageObjects();
-  codePeriodsMock.setNewCodePeriod({
+  newCodeDefinitionMock.setNewCodePeriod({
     type: NewCodeDefinitionType.SpecificAnalysis,
     value: 'analysis_id',
   });
-  renderProjectBaselineApp();
+  renderProjectNewCodeDefinitionApp();
   await ui.appIsLoaded();
 
   expect(await ui.specificAnalysisRadio.find()).toBeChecked();
@@ -198,7 +202,7 @@ it('cannot set specific analysis setting', async () => {
 
 it('renders correctly branch modal', async () => {
   const { ui } = getPageObjects();
-  renderProjectBaselineApp({
+  renderProjectNewCodeDefinitionApp({
     featureList: [Feature.BranchSupport],
   });
   await ui.appIsLoaded();
@@ -210,7 +214,7 @@ it('renders correctly branch modal', async () => {
 
 it('can set a previous version setting for branch', async () => {
   const { ui, user } = getPageObjects();
-  renderProjectBaselineApp({
+  renderProjectNewCodeDefinitionApp({
     featureList: [Feature.BranchSupport],
   });
   await ui.appIsLoaded();
@@ -232,7 +236,7 @@ it('can set a previous version setting for branch', async () => {
 
 it('can set a number of days setting for branch', async () => {
   const { ui } = getPageObjects();
-  renderProjectBaselineApp({
+  renderProjectNewCodeDefinitionApp({
     featureList: [Feature.BranchSupport],
   });
   await ui.appIsLoaded();
@@ -246,14 +250,14 @@ it('can set a number of days setting for branch', async () => {
 
 it('cannot set a specific analysis setting for branch', async () => {
   const { ui } = getPageObjects();
-  codePeriodsMock.setListBranchesNewCode([
+  newCodeDefinitionMock.setListBranchesNewCode([
     mockNewCodePeriodBranch({
       branchKey: 'main',
       type: NewCodeDefinitionType.SpecificAnalysis,
       value: 'analysis_id',
     }),
   ]);
-  renderProjectBaselineApp({
+  renderProjectNewCodeDefinitionApp({
     featureList: [Feature.BranchSupport],
   });
   await ui.appIsLoaded();
@@ -272,7 +276,7 @@ it('cannot set a specific analysis setting for branch', async () => {
 
 it('can set a reference branch setting for branch', async () => {
   const { ui } = getPageObjects();
-  renderProjectBaselineApp({
+  renderProjectNewCodeDefinitionApp({
     featureList: [Feature.BranchSupport],
   });
   await ui.appIsLoaded();
@@ -284,7 +288,108 @@ it('can set a reference branch setting for branch', async () => {
   ).toBeInTheDocument();
 });
 
-function renderProjectBaselineApp(context: RenderContext = {}, params?: string) {
+it('should display NCD banner if some branches had their NCD automatically changed', async () => {
+  const { ui } = getPageObjects();
+
+  newCodeDefinitionMock.setListBranchesNewCode([
+    {
+      projectKey: 'test-project:test',
+      branchKey: 'test-branch',
+      type: NewCodeDefinitionType.NumberOfDays,
+      value: '25',
+      inherited: true,
+      updatedAt: 1692720953662,
+    },
+    {
+      projectKey: 'test-project:test',
+      branchKey: 'master',
+      type: NewCodeDefinitionType.NumberOfDays,
+      value: '32',
+      previousNonCompliantValue: '150',
+      updatedAt: 1692721852743,
+    },
+  ]);
+
+  renderProjectNewCodeDefinitionApp({
+    featureList: [Feature.BranchSupport],
+  });
+
+  expect(await ui.branchNCDsBanner.find()).toBeInTheDocument();
+  expect(
+    ui.branchNCDsBanner.byText('new_code_definition.auto_update.branch.list_itemmaster32150').get()
+  ).toBeInTheDocument();
+});
+
+it('should not display NCD banner if some branches had their NCD automatically changed and banne has been dismissed', async () => {
+  const { ui } = getPageObjects();
+
+  newCodeDefinitionMock.setListBranchesNewCode([
+    {
+      projectKey: 'test-project:test',
+      branchKey: 'test-branch',
+      type: NewCodeDefinitionType.NumberOfDays,
+      value: '25',
+      inherited: true,
+      updatedAt: 1692720953662,
+    },
+    {
+      projectKey: 'test-project:test',
+      branchKey: 'master',
+      type: NewCodeDefinitionType.NumberOfDays,
+      value: '32',
+      previousNonCompliantValue: '150',
+      updatedAt: 1692721852743,
+    },
+  ]);
+  messagesMock.setMessageDismissed({
+    projectKey: 'test-project:test',
+    messageType: MessageTypes.BranchNcd90,
+  });
+
+  renderProjectNewCodeDefinitionApp({
+    featureList: [Feature.BranchSupport],
+  });
+
+  expect(await ui.branchNCDsBanner.query()).not.toBeInTheDocument();
+});
+
+it('should correctly dismiss branch banner', async () => {
+  const { ui } = getPageObjects();
+
+  newCodeDefinitionMock.setListBranchesNewCode([
+    {
+      projectKey: 'test-project:test',
+      branchKey: 'test-branch',
+      type: NewCodeDefinitionType.NumberOfDays,
+      value: '25',
+      inherited: true,
+      updatedAt: 1692720953662,
+    },
+    {
+      projectKey: 'test-project:test',
+      branchKey: 'master',
+      type: NewCodeDefinitionType.NumberOfDays,
+      value: '32',
+      previousNonCompliantValue: '150',
+      updatedAt: 1692721852743,
+    },
+  ]);
+
+  renderProjectNewCodeDefinitionApp({
+    featureList: [Feature.BranchSupport],
+  });
+
+  expect(await ui.branchNCDsBanner.find()).toBeInTheDocument();
+
+  const user = userEvent.setup();
+  await act(async () => {
+    await user.click(ui.dismissButton.get());
+  });
+
+  expect(ui.branchNCDsBanner.query()).not.toBeInTheDocument();
+});
+
+function renderProjectNewCodeDefinitionApp(context: RenderContext = {}, params?: string) {
   return renderAppWithComponentContext(
     'baseline',
     routes,
@@ -328,6 +433,8 @@ function getPageObjects() {
     saved: byText('settings.state.saved'),
     complianceWarningAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
     complianceWarning: byText('new_code_definition.compliance.warning.explanation'),
+    branchNCDsBanner: byText(/new_code_definition.auto_update.branch.message/),
+    dismissButton: byLabelText('alert.dismiss'),
   };
 
   async function appIsLoaded() {
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/BranchNCDAutoUpdateMessage.tsx b/server/sonar-web/src/main/js/components/new-code-definition/BranchNCDAutoUpdateMessage.tsx
new file mode 100644 (file)
index 0000000..197ef8d
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * 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 React, { useCallback, useEffect, useState } from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { MessageTypes, checkMessageDismissed, setMessageDismissed } from '../../api/messages';
+import { Component } from '../../types/types';
+import DocLink from '../common/DocLink';
+import DismissableAlertComponent from '../ui/DismissableAlertComponent';
+import { PreviouslyNonCompliantBranchNCD } from './utils';
+
+interface NCDAutoUpdateMessageProps {
+  component: Component;
+  previouslyNonCompliantBranchNCDs: PreviouslyNonCompliantBranchNCD[];
+}
+
+export default function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) {
+  const { component, previouslyNonCompliantBranchNCDs } = props;
+  const intl = useIntl();
+
+  const [dismissed, setDismissed] = useState(true);
+
+  const handleBannerDismiss = useCallback(async () => {
+    await setMessageDismissed({ messageType: MessageTypes.BranchNcd90, projectKey: component.key });
+    setDismissed(true);
+  }, [component]);
+
+  useEffect(() => {
+    async function checkBranchMessageDismissed() {
+      if (previouslyNonCompliantBranchNCDs.length > 0) {
+        const messageStatus = await checkMessageDismissed({
+          messageType: MessageTypes.BranchNcd90,
+          projectKey: component.key,
+        });
+        setDismissed(messageStatus.dismissed);
+      }
+    }
+
+    if (previouslyNonCompliantBranchNCDs.length > 0) {
+      checkBranchMessageDismissed();
+    }
+  }, [component, previouslyNonCompliantBranchNCDs]);
+
+  if (dismissed || previouslyNonCompliantBranchNCDs.length === 0) {
+    return null;
+  }
+
+  const branchesList = (
+    <ul className="sw-list-disc sw-my-4 sw-list-inside">
+      {previouslyNonCompliantBranchNCDs.map((branchNCD) => (
+        <li key={branchNCD.branchKey}>
+          <FormattedMessage
+            id="new_code_definition.auto_update.branch.list_item"
+            values={{
+              branchName: branchNCD.branchKey,
+              days: branchNCD.value,
+              previousDays: branchNCD.previousNonCompliantValue,
+            }}
+          />
+        </li>
+      ))}
+    </ul>
+  );
+
+  return (
+    <DismissableAlertComponent
+      className="sw-my-4"
+      onDismiss={handleBannerDismiss}
+      variant="info"
+      display="banner"
+    >
+      <FormattedMessage
+        id="new_code_definition.auto_update.branch.message"
+        values={{
+          date: new Date(previouslyNonCompliantBranchNCDs[0].updatedAt).toLocaleDateString(),
+          branchesList,
+          link: (
+            <DocLink to="/project-administration/clean-as-you-code-settings/defining-new-code/#new-code-definition-options">
+              {intl.formatMessage({ id: 'learn_more' })}
+            </DocLink>
+          ),
+        }}
+      />
+    </DismissableAlertComponent>
+  );
+}
index cb6d5c95c3fdcb5e9aff12b526c441f5f3a038ae..ecb71e828939033cb7d8a451ec70e62aa95ef5fb 100644 (file)
@@ -86,10 +86,7 @@ function NCDAutoUpdateMessage(props: NCDAutoUpdateMessageProps) {
         }
       );
 
-      if (
-        isPreviouslyNonCompliantDaysNCD(newCodeDefinition) &&
-        (!component || !newCodeDefinition?.inherited)
-      ) {
+      if (isPreviouslyNonCompliantDaysNCD(newCodeDefinition)) {
         setPreviouslyNonCompliantNewCodeDefinition(newCodeDefinition);
 
         const messageStatus = await checkMessageDismissed(
index 95cf53fc6f463efb89eb4e450f284dec3e553c73..3dc273cb7c40decc2271bdb24eeb083bd8d3a62d 100644 (file)
  */
 
 import { hasGlobalPermission } from '../../helpers/users';
-import { NewCodeDefinition, NewCodeDefinitionType } from '../../types/new-code-definition';
+import {
+  NewCodeDefinition,
+  NewCodeDefinitionBranch,
+  NewCodeDefinitionType,
+} from '../../types/new-code-definition';
 import { Permissions } from '../../types/permissions';
 import { Component } from '../../types/types';
 import { CurrentUser, isLoggedIn } from '../../types/users';
@@ -34,13 +38,22 @@ export enum NewCodeDefinitionLevels {
 export type PreviouslyNonCompliantNCD = NewCodeDefinition &
   Required<Pick<NewCodeDefinition, 'previousNonCompliantValue' | 'updatedAt'>>;
 
+export type PreviouslyNonCompliantBranchNCD = PreviouslyNonCompliantNCD & NewCodeDefinitionBranch;
+
 export function isPreviouslyNonCompliantDaysNCD(
   newCodeDefinition: NewCodeDefinition
-): newCodeDefinition is PreviouslyNonCompliantNCD {
+): newCodeDefinition is PreviouslyNonCompliantNCD;
+export function isPreviouslyNonCompliantDaysNCD(
+  newCodeDefinition: NewCodeDefinitionBranch
+): newCodeDefinition is PreviouslyNonCompliantBranchNCD;
+export function isPreviouslyNonCompliantDaysNCD(
+  newCodeDefinition: NewCodeDefinition | NewCodeDefinitionBranch
+): newCodeDefinition is PreviouslyNonCompliantNCD | PreviouslyNonCompliantBranchNCD {
   return (
     newCodeDefinition.type === NewCodeDefinitionType.NumberOfDays &&
     newCodeDefinition.previousNonCompliantValue !== undefined &&
-    newCodeDefinition.updatedAt !== undefined
+    newCodeDefinition.updatedAt !== undefined &&
+    !newCodeDefinition.inherited
   );
 }
 
index cb846a32df7cd796c217d3dda4367cc7eed44b4b..cfd279ce06e95c0db6d1e8499c0a00846b52a226 100644 (file)
@@ -3961,6 +3961,8 @@ new_code_definition.reference_branch.description=Choose a branch as the baseline
 new_code_definition.reference_branch.usecase=Recommended for projects using feature branches.
 new_code_definition.reference_branch.notice=The main branch will be set as the reference branch when the project is created. You will be able to choose another branch as the reference branch when your project will have more branches.
 
+new_code_definition.auto_update.branch.message=The new code definition of the following branch(es) was automatically changed on {date}, following a SonarQube upgrade, as it was exceeding the maximum value: {branchesList} {link}
+new_code_definition.auto_update.branch.list_item={branchName}: Number of days was changed from {previousDays} to {days}.
 new_code_definition.auto_update.global.message=The global new code definition was automatically changed from {previousDays} to {days} days on {date}, following a SonarQube upgrade, as it was exceeding the maximum value. {link}
 new_code_definition.auto_update.ncd_page.message=The number of days was automatically changed from {previousDays} to {days} on {date}, following a SonarQube upgrade, as it was exceeding the maximum value. {link}
 new_code_definition.auto_update.project.message=This project's new code definition was automatically changed from {previousDays} to {days} days on {date}, following a SonarQube upgrade, as it was exceeding the maximum value. {link}