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';
branches: BranchWithNewCodePeriod[];
editedBranch?: BranchWithNewCodePeriod;
loading: boolean;
+ previouslyNonCompliantBranchNCDs?: PreviouslyNonCompliantBranchNCD[];
}
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 });
};
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 });
}
};
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;
}
return (
- <>
+ <div>
+ {previouslyNonCompliantBranchNCDs && (
+ <BranchNCDAutoUpdateMessage
+ component={component}
+ previouslyNonCompliantBranchNCDs={previouslyNonCompliantBranchNCDs}
+ />
+ )}
<table className="data zebra">
<thead>
<tr>
globalNewCodeDefinition={globalNewCodeDefinition}
/>
)}
- </>
+ </div>
);
}
}
* 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';
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';
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();
});
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();
});
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();
it('renders correctly with branch support feature', async () => {
const { ui } = getPageObjects();
- renderProjectBaselineApp({
+ renderProjectNewCodeDefinitionApp({
featureList: [Feature.BranchSupport],
appState: mockAppState({ canAdmin: true }),
});
it('can set previous version specific setting', async () => {
const { ui, user } = getPageObjects();
- renderProjectBaselineApp();
+ renderProjectNewCodeDefinitionApp();
await ui.appIsLoaded();
expect(await ui.previousVersionRadio.find()).toHaveClass('disabled');
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');
it('can set reference branch specific setting', async () => {
const { ui, user } = getPageObjects();
- renderProjectBaselineApp({
+ renderProjectNewCodeDefinitionApp({
featureList: [Feature.BranchSupport],
});
await ui.appIsLoaded();
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();
it('renders correctly branch modal', async () => {
const { ui } = getPageObjects();
- renderProjectBaselineApp({
+ renderProjectNewCodeDefinitionApp({
featureList: [Feature.BranchSupport],
});
await ui.appIsLoaded();
it('can set a previous version setting for branch', async () => {
const { ui, user } = getPageObjects();
- renderProjectBaselineApp({
+ renderProjectNewCodeDefinitionApp({
featureList: [Feature.BranchSupport],
});
await ui.appIsLoaded();
it('can set a number of days setting for branch', async () => {
const { ui } = getPageObjects();
- renderProjectBaselineApp({
+ renderProjectNewCodeDefinitionApp({
featureList: [Feature.BranchSupport],
});
await ui.appIsLoaded();
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();
it('can set a reference branch setting for branch', async () => {
const { ui } = getPageObjects();
- renderProjectBaselineApp({
+ renderProjectNewCodeDefinitionApp({
featureList: [Feature.BranchSupport],
});
await ui.appIsLoaded();
).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,
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() {
--- /dev/null
+/*
+ * 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>
+ );
+}