aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/components.ts16
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx97
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx248
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx59
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/types.ts8
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx82
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx92
-rw-r--r--server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx (renamed from server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx)26
-rw-r--r--server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx (renamed from server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx)23
-rw-r--r--server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx186
-rw-r--r--server/sonar-web/src/main/js/types/types.ts8
17 files changed, 747 insertions, 183 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts
index 60d38ed488c..7c05e3ec95c 100644
--- a/server/sonar-web/src/main/js/api/components.ts
+++ b/server/sonar-web/src/main/js/api/components.ts
@@ -97,11 +97,27 @@ export function deletePortfolio(portfolio: string): Promise<void | Response> {
return post('/api/views/delete', { key: portfolio }).catch(throwGlobalError);
}
+export function setupManualProjectCreation(data: {
+ name: string;
+ project: string;
+ mainBranch: string;
+ visibility?: Visibility;
+}) {
+ return (newCodeDefinitionType?: string, newCodeDefinitionValue?: string) =>
+ createProject({
+ ...data,
+ newCodeDefinitionType,
+ newCodeDefinitionValue,
+ });
+}
+
export function createProject(data: {
name: string;
project: string;
mainBranch: string;
visibility?: Visibility;
+ newCodeDefinitionType?: string;
+ newCodeDefinitionValue?: string;
}): Promise<{ project: ProjectBase }> {
return postJSON('/api/projects/create', data).catch(throwGlobalError);
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
index 53d317ad3af..379b9a2b27b 100644
--- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
@@ -17,30 +17,37 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { noop } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
+import { FormattedMessage } from 'react-intl';
import { getAlmSettings } from '../../../api/alm-settings';
import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
import withAvailableFeatures, {
WithAvailableFeaturesProps,
} from '../../../app/components/available-features/withAvailableFeatures';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
+import DocLink from '../../../components/common/DocLink';
+import { ButtonLink, SubmitButton } from '../../../components/controls/buttons';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
+import NewCodeDefinitionSelector from '../../../components/new-code-definition/NewCodeDefinitionSelector';
import { translate } from '../../../helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import { AppState } from '../../../types/appstate';
import { Feature } from '../../../types/features';
+import { NewCodePeriodWithCompliance } from '../../../types/types';
import AlmBindingDefinitionForm from '../../settings/components/almIntegration/AlmBindingDefinitionForm';
import AzureProjectCreate from './Azure/AzureProjectCreate';
import BitbucketCloudProjectCreate from './BitbucketCloud/BitbucketCloudProjectCreate';
import BitbucketProjectCreate from './BitbucketServer/BitbucketProjectCreate';
+import CreateProjectPageHeader from './components/CreateProjectPageHeader';
import CreateProjectModeSelection from './CreateProjectModeSelection';
import GitHubProjectCreate from './Github/GitHubProjectCreate';
import GitlabProjectCreate from './Gitlab/GitlabProjectCreate';
import ManualProjectCreate from './manual/ManualProjectCreate';
import './style.css';
-import { CreateProjectModes } from './types';
+import { CreateProjectApiCallback, CreateProjectModes } from './types';
export interface CreateProjectPageProps extends WithAvailableFeaturesProps {
appState: AppState;
@@ -55,7 +62,9 @@ interface State {
githubSettings: AlmSettingsInstance[];
gitlabSettings: AlmSettingsInstance[];
loading: boolean;
+ isProjectSetupDone: boolean;
creatingAlmDefinition?: AlmKeys;
+ selectedNcd: NewCodePeriodWithCompliance | null;
}
const PROJECT_MODE_FOR_ALM_KEY = {
@@ -68,6 +77,8 @@ const PROJECT_MODE_FOR_ALM_KEY = {
export class CreateProjectPage extends React.PureComponent<CreateProjectPageProps, State> {
mounted = false;
+ createProjectFnRef: CreateProjectApiCallback | null = null;
+
state: State = {
azureSettings: [],
bitbucketSettings: [],
@@ -75,6 +86,8 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
githubSettings: [],
gitlabSettings: [],
loading: true,
+ isProjectSetupDone: false,
+ selectedNcd: null,
};
componentDidMount() {
@@ -124,6 +137,21 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
this.props.router.push(getProjectUrl(projectKey));
};
+ handleManualProjectCreate = () => {
+ const { selectedNcd } = this.state;
+ if (this.createProjectFnRef && selectedNcd) {
+ this.createProjectFnRef(selectedNcd.type, selectedNcd.value).then(
+ ({ project }) => this.handleProjectCreate(project.key),
+ noop
+ );
+ }
+ };
+
+ handleProjectSetupDone = (createProject: CreateProjectApiCallback) => {
+ this.createProjectFnRef = createProject;
+ this.setState({ isProjectSetupDone: true });
+ };
+
handleOnCancelCreation = () => {
this.setState({ creatingAlmDefinition: undefined });
};
@@ -146,6 +174,16 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
}
};
+ handleNcdChanged = (ncd: NewCodePeriodWithCompliance) => {
+ this.setState({
+ selectedNcd: ncd,
+ });
+ };
+
+ handleGoBack = () => {
+ this.setState({ isProjectSetupDone: false });
+ };
+
renderProjectCreation(mode?: CreateProjectModes) {
const {
appState: { canAdmin },
@@ -227,7 +265,7 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
return (
<ManualProjectCreate
branchesEnabled={branchSupportEnabled}
- onProjectCreate={this.handleProjectCreate}
+ onProjectSetupDone={this.handleProjectSetupDone}
/>
);
}
@@ -251,9 +289,59 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
}
}
+ renderNcdSelection() {
+ const { appState } = this.props;
+ const { selectedNcd } = this.state;
+
+ return (
+ <div id="project-ncd-selection">
+ <CreateProjectPageHeader
+ title={translate('onboarding.create_project.new_code_definition.title')}
+ />
+
+ <h1 className="sw-mb-4">{translate('onboarding.create_project.new_code_definition')}</h1>
+ <p className="sw-mb-2">
+ {translate('onboarding.create_project.new_code_definition.description')}
+ </p>
+ <p className="sw-mb-2">
+ {translate('onboarding.create_project.new_code_definition.description1')}
+ </p>
+
+ <p className="sw-mb-2">
+ <FormattedMessage
+ defaultMessage={translate('onboarding.create_project.new_code_definition.description2')}
+ id="onboarding.create_project.new_code_definition.description2"
+ values={{
+ link: (
+ <DocLink to="/project-administration/defining-new-code/">
+ {translate('onboarding.create_project.new_code_definition.description2.link')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
+
+ <NewCodeDefinitionSelector
+ canAdmin={appState.canAdmin}
+ onNcdChanged={this.handleNcdChanged}
+ />
+
+ <div className="sw-flex sw-flex-row sw-gap-2 sw-mt-4">
+ <ButtonLink onClick={this.handleGoBack}>{translate('back')}</ButtonLink>
+ <SubmitButton
+ onClick={this.handleManualProjectCreate}
+ disabled={!selectedNcd?.isCompliant}
+ >
+ {translate('onboarding.create_project.new_code_definition.create_project')}
+ </SubmitButton>
+ </div>
+ </div>
+ );
+ }
+
render() {
const { location } = this.props;
- const { creatingAlmDefinition } = this.state;
+ const { creatingAlmDefinition, isProjectSetupDone } = this.state;
const mode: CreateProjectModes | undefined = location.query?.mode;
return (
@@ -261,7 +349,8 @@ export class CreateProjectPage extends React.PureComponent<CreateProjectPageProp
<Helmet title={translate('onboarding.create_project.select_method')} titleTemplate="%s" />
<A11ySkipTarget anchor="create_project_main" />
<div className="page page-limited huge-spacer-bottom position-relative" id="create-project">
- {this.renderProjectCreation(mode)}
+ {isProjectSetupDone ? this.renderNcdSelection() : this.renderProjectCreation(mode)}
+
{creatingAlmDefinition && (
<AlmBindingDefinitionForm
alm={creatingAlmDefinition}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
new file mode 100644
index 00000000000..06bb6cb7c3e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/Manual-it.tsx
@@ -0,0 +1,248 @@
+/*
+ * 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 userEvent from '@testing-library/user-event';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
+import * as React from 'react';
+import { byRole, byText } from 'testing-library-selector';
+import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
+import { getNewCodePeriod } from '../../../../api/newCodePeriod';
+import { mockProject } from '../../../../helpers/mocks/projects';
+import { mockAppState } from '../../../../helpers/testMocks';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { NewCodePeriodSettingType } from '../../../../types/types';
+import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';
+import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+
+jest.mock('../../../../api/alm-settings');
+jest.mock('../../../../api/newCodePeriod');
+jest.mock('../../../../api/components', () => ({
+ ...jest.requireActual('../../../../api/components'),
+ setupManualProjectCreation: jest
+ .fn()
+ .mockReturnValue(() => Promise.resolve({ project: mockProject() })),
+ doesComponentExists: jest
+ .fn()
+ .mockImplementation(({ component }) => Promise.resolve(component === 'exists')),
+}));
+jest.mock('../../../../api/settings', () => ({
+ getValue: jest.fn().mockResolvedValue({ value: 'main' }),
+}));
+
+const ui = {
+ manualCreateProjectOption: byText('onboarding.create_project.select_method.manual'),
+ manualProjectHeader: byText('onboarding.create_project.setup_manually'),
+ displayNameField: byRole('textbox', {
+ name: /onboarding.create_project.display_name/,
+ }),
+ projectNextButton: byRole('button', { name: 'next' }),
+ newCodeDefinitionHeader: byText('onboarding.create_project.new_code_definition.title'),
+ newCodeDefinitionBackButton: byRole('button', { name: 'back' }),
+ inheritGlobalNcdRadio: byRole('radio', { name: 'new_code_definition.global_setting' }),
+ projectCreateButton: byRole('button', {
+ name: 'onboarding.create_project.new_code_definition.create_project',
+ }),
+ overrideNcdRadio: byRole('radio', { name: 'new_code_definition.specific_setting' }),
+ ncdOptionPreviousVersionRadio: byRole('radio', {
+ name: /new_code_definition.previous_version/,
+ }),
+ ncdOptionRefBranchRadio: byRole('radio', {
+ name: /new_code_definition.reference_branch/,
+ }),
+ ncdOptionDaysRadio: byRole('radio', {
+ name: /new_code_definition.number_days/,
+ }),
+ ncdOptionDaysInput: byRole('textbox', {
+ name: /new_code_definition.number_days.specify_days/,
+ }),
+ ncdOptionDaysInputError: byText('new_code_definition.number_days.invalid.1.90'),
+ ncdWarningTextAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
+ ncdWarningText: byText('new_code_definition.compliance.warning.explanation'),
+ projectDashboardText: byText('/dashboard?id=foo'),
+};
+
+async function fillFormAndNext(displayName: string, user: UserEvent) {
+ await user.click(ui.manualCreateProjectOption.get());
+
+ expect(ui.manualProjectHeader.get()).toBeInTheDocument();
+
+ await user.click(ui.displayNameField.get());
+ await user.keyboard(displayName);
+
+ expect(ui.projectNextButton.get()).toBeEnabled();
+ await user.click(ui.projectNextButton.get());
+}
+
+let almSettingsHandler: AlmSettingsServiceMock;
+let newCodePeriodHandler: NewCodePeriodsServiceMock;
+
+beforeAll(() => {
+ almSettingsHandler = new AlmSettingsServiceMock();
+ newCodePeriodHandler = new NewCodePeriodsServiceMock();
+});
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ almSettingsHandler.reset();
+ newCodePeriodHandler.reset();
+});
+
+it('should fill form and move to NCD selection and back', async () => {
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('test', user);
+
+ expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
+
+ expect(ui.newCodeDefinitionBackButton.get()).toBeInTheDocument();
+ await user.click(ui.newCodeDefinitionBackButton.get());
+
+ expect(ui.manualProjectHeader.get()).toBeInTheDocument();
+
+ // TODO this must work at some point
+ // expect(ui.displayNameField.get()).toHaveValue('test');
+});
+
+it('should select the global NCD when it is compliant', async () => {
+ jest
+ .mocked(getNewCodePeriod)
+ .mockResolvedValue({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '30' });
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('test', user);
+
+ expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toBeEnabled();
+ expect(ui.projectCreateButton.get()).toBeDisabled();
+
+ await user.click(ui.inheritGlobalNcdRadio.get());
+
+ expect(ui.projectCreateButton.get()).toBeEnabled();
+});
+
+it('global NCD option should be disabled if not compliant', async () => {
+ jest
+ .mocked(getNewCodePeriod)
+ .mockResolvedValue({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '96' });
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('test', user);
+
+ expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toHaveClass('disabled');
+ expect(ui.projectCreateButton.get()).toBeDisabled();
+});
+
+it.each([
+ { canAdmin: true, message: ui.ncdWarningTextAdmin },
+ { canAdmin: false, message: ui.ncdWarningText },
+])(
+ 'should show warning message when global NCD is not compliant',
+ async ({ canAdmin, message }) => {
+ jest
+ .mocked(getNewCodePeriod)
+ .mockResolvedValue({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '96' });
+ const user = userEvent.setup();
+ renderCreateProject({ appState: mockAppState({ canAdmin }) });
+ await fillFormAndNext('test', user);
+
+ expect(message.get()).toBeInTheDocument();
+ }
+);
+
+it.each([ui.ncdOptionRefBranchRadio, ui.ncdOptionPreviousVersionRadio])(
+ 'should override the global NCD and pick a compliant NCD',
+ async (option) => {
+ jest
+ .mocked(getNewCodePeriod)
+ .mockResolvedValue({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '96' });
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('test', user);
+
+ expect(ui.newCodeDefinitionHeader.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toBeInTheDocument();
+ expect(ui.inheritGlobalNcdRadio.get()).toHaveClass('disabled');
+ expect(ui.projectCreateButton.get()).toBeDisabled();
+ expect(ui.overrideNcdRadio.get()).not.toHaveClass('disabled');
+ expect(option.get()).toHaveClass('disabled');
+
+ await user.click(ui.overrideNcdRadio.get());
+ expect(option.get()).not.toHaveClass('disabled');
+
+ await user.click(option.get());
+
+ expect(ui.projectCreateButton.get()).toBeEnabled();
+ }
+);
+
+it('number of days should show error message if value is not a number', async () => {
+ jest
+ .mocked(getNewCodePeriod)
+ .mockResolvedValue({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '60' });
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('test', user);
+
+ expect(ui.projectCreateButton.get()).toBeDisabled();
+ expect(ui.overrideNcdRadio.get()).not.toHaveClass('disabled');
+ expect(ui.ncdOptionDaysRadio.get()).toHaveClass('disabled');
+
+ await user.click(ui.overrideNcdRadio.get());
+ expect(ui.ncdOptionDaysRadio.get()).not.toHaveClass('disabled');
+
+ await user.click(ui.ncdOptionDaysRadio.get());
+
+ expect(ui.ncdOptionDaysInput.get()).toBeInTheDocument();
+ expect(ui.ncdOptionDaysInput.get()).toHaveValue('30');
+ expect(ui.projectCreateButton.get()).toBeEnabled();
+
+ await user.click(ui.ncdOptionDaysInput.get());
+ await user.keyboard('abc');
+
+ expect(ui.ncdOptionDaysInputError.get()).toBeInTheDocument();
+ expect(ui.projectCreateButton.get()).toBeDisabled();
+
+ await user.clear(ui.ncdOptionDaysInput.get());
+ await user.click(ui.ncdOptionDaysInput.get());
+ await user.keyboard('30');
+
+ expect(ui.ncdOptionDaysInputError.query()).not.toBeInTheDocument();
+ expect(ui.projectCreateButton.get()).toBeEnabled();
+});
+
+it('the project onboarding page should be displayed when the project is created', async () => {
+ newCodePeriodHandler.setNewCodePeriod({ type: NewCodePeriodSettingType.NUMBER_OF_DAYS });
+ const user = userEvent.setup();
+ renderCreateProject();
+ await fillFormAndNext('testing', user);
+
+ await user.click(ui.overrideNcdRadio.get());
+
+ expect(ui.projectCreateButton.get()).toBeEnabled();
+ await user.click(ui.projectCreateButton.get());
+
+ expect(await ui.projectDashboardText.find()).toBeInTheDocument();
+});
+
+function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) {
+ renderApp('project/create', <CreateProjectPage {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
index 98a05291a2a..8bcecd0dd45 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
@@ -20,13 +20,17 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
-import { createProject, doesComponentExists } from '../../../../api/components';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import { byRole } from 'testing-library-selector';
+import { doesComponentExists } from '../../../../api/components';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import ManualProjectCreate from '../manual/ManualProjectCreate';
+const ui = {
+ nextButton: byRole('button', { name: 'next' }),
+};
+
jest.mock('../../../../api/components', () => ({
- createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } }),
+ setupManualProjectCreation: jest.fn(),
doesComponentExists: jest
.fn()
.mockImplementation(({ component }) => Promise.resolve(component === 'exists')),
@@ -36,15 +40,8 @@ jest.mock('../../../../api/settings', () => ({
getValue: jest.fn().mockResolvedValue({ value: 'main' }),
}));
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
-
-beforeAll(() => {
- newCodePeriodHandler = new NewCodePeriodsServiceMock();
-});
-
beforeEach(() => {
jest.clearAllMocks();
- newCodePeriodHandler.reset();
});
it('should show branch information', async () => {
@@ -68,7 +65,7 @@ it('should validate form input', async () => {
expect(
screen.getByRole('textbox', { name: 'onboarding.create_project.project_key field_required' })
).toHaveValue('test');
- expect(screen.getByRole('button', { name: 'set_up' })).toBeEnabled();
+ expect(ui.nextButton.get()).toBeEnabled();
// Sanitize the key
await user.click(
@@ -93,7 +90,7 @@ it('should validate form input', async () => {
expect(
screen.getByText('onboarding.create_project.display_name.error.empty')
).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'set_up' })).toBeDisabled();
+ expect(ui.nextButton.get()).toBeDisabled();
// Only key
await user.click(
@@ -142,8 +139,8 @@ it('should validate form input', async () => {
it('should submit form input', async () => {
const user = userEvent.setup();
- const onProjectCreate = jest.fn();
- renderManualProjectCreate({ onProjectCreate });
+ const onProjectSetupDone = jest.fn();
+ renderManualProjectCreate({ onProjectSetupDone });
// All input valid
await user.click(
@@ -152,38 +149,14 @@ it('should submit form input', async () => {
})
);
await user.keyboard('test');
- await user.click(screen.getByRole('button', { name: 'set_up' }));
- expect(createProject).toHaveBeenCalledWith({
- name: 'test',
- project: 'test',
- mainBranch: 'main',
- });
- expect(onProjectCreate).toHaveBeenCalled();
-});
-
-it('should handle create failure', async () => {
- const user = userEvent.setup();
- (createProject as jest.Mock).mockRejectedValueOnce({});
- const onProjectCreate = jest.fn();
- renderManualProjectCreate({ onProjectCreate });
-
- // All input valid
- await user.click(
- await screen.findByRole('textbox', {
- name: 'onboarding.create_project.display_name field_required',
- })
- );
- await user.keyboard('test');
- await user.click(screen.getByRole('button', { name: 'set_up' }));
-
- expect(onProjectCreate).not.toHaveBeenCalled();
+ await user.click(ui.nextButton.get());
+ expect(onProjectSetupDone).toHaveBeenCalled();
});
it('should handle component exists failure', async () => {
const user = userEvent.setup();
- (doesComponentExists as jest.Mock).mockRejectedValueOnce({});
- const onProjectCreate = jest.fn();
- renderManualProjectCreate({ onProjectCreate });
+ jest.mocked(doesComponentExists).mockRejectedValueOnce({});
+ renderManualProjectCreate();
// All input valid
await user.click(
@@ -199,6 +172,6 @@ it('should handle component exists failure', async () => {
function renderManualProjectCreate(props: Partial<ManualProjectCreate['props']> = {}) {
renderComponent(
- <ManualProjectCreate branchesEnabled={false} onProjectCreate={jest.fn()} {...props} />
+ <ManualProjectCreate branchesEnabled={false} onProjectSetupDone={jest.fn()} {...props} />
);
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx
index f2c681e6164..ef344ed928d 100644
--- a/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx
@@ -21,26 +21,25 @@ import classNames from 'classnames';
import { debounce, isEmpty } from 'lodash';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { createProject, doesComponentExists } from '../../../../api/components';
+import { doesComponentExists, setupManualProjectCreation } from '../../../../api/components';
import { getValue } from '../../../../api/settings';
import DocLink from '../../../../components/common/DocLink';
import ProjectKeyInput from '../../../../components/common/ProjectKeyInput';
import ValidationInput from '../../../../components/controls/ValidationInput';
import { SubmitButton } from '../../../../components/controls/buttons';
import { Alert } from '../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
import MandatoryFieldsExplanation from '../../../../components/ui/MandatoryFieldsExplanation';
import { translate } from '../../../../helpers/l10n';
import { PROJECT_KEY_INVALID_CHARACTERS, validateProjectKey } from '../../../../helpers/projects';
import { ProjectKeyValidationResult } from '../../../../types/component';
import { GlobalSettingKeys } from '../../../../types/settings';
import CreateProjectPageHeader from '../components/CreateProjectPageHeader';
-import InstanceNewCodeDefinitionComplianceWarning from '../components/InstanceNewCodeDefinitionComplianceWarning';
import { PROJECT_NAME_MAX_LEN } from '../constants';
+import { CreateProjectApiCallback } from '../types';
interface Props {
branchesEnabled: boolean;
- onProjectCreate: (projectKey: string) => void;
+ onProjectSetupDone: (createProject: CreateProjectApiCallback) => void;
}
interface State {
@@ -54,7 +53,6 @@ interface State {
mainBranchName: string;
mainBranchNameError?: string;
mainBranchNameTouched: boolean;
- submitting: boolean;
}
const DEBOUNCE_DELAY = 250;
@@ -69,7 +67,6 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
this.state = {
projectKey: '',
projectName: '',
- submitting: false,
projectKeyTouched: false,
projectNameTouched: false,
mainBranchName: 'main',
@@ -132,18 +129,12 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
event.preventDefault();
const { projectKey, projectName, mainBranchName } = this.state;
if (this.canSubmit(this.state)) {
- this.setState({ submitting: true });
- createProject({
- project: projectKey,
- name: (projectName || projectKey).trim(),
- mainBranch: mainBranchName,
- }).then(
- ({ project }) => this.props.onProjectCreate(project.key),
- () => {
- if (this.mounted) {
- this.setState({ submitting: false });
- }
- }
+ this.props.onProjectSetupDone(
+ setupManualProjectCreation({
+ project: projectKey,
+ name: (projectName || projectKey).trim(),
+ mainBranch: mainBranchName,
+ })
);
}
};
@@ -221,7 +212,6 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
mainBranchName,
mainBranchNameError,
mainBranchNameTouched,
- submitting,
} = this.state;
const { branchesEnabled } = this.props;
@@ -235,8 +225,6 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
<>
<CreateProjectPageHeader title={translate('onboarding.create_project.setup_manually')} />
- <InstanceNewCodeDefinitionComplianceWarning />
-
<form id="create-project-manual" onSubmit={this.handleFormSubmit}>
<MandatoryFieldsExplanation className="big-spacer-bottom" />
@@ -308,10 +296,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
/>
</ValidationInput>
- <SubmitButton disabled={!this.canSubmit(this.state) || submitting}>
- {translate('set_up')}
- </SubmitButton>
- <DeferredSpinner className="spacer-left" loading={submitting} />
+ <SubmitButton disabled={!this.canSubmit(this.state)}>{translate('next')}</SubmitButton>
</form>
{branchesEnabled && (
diff --git a/server/sonar-web/src/main/js/apps/create/project/types.ts b/server/sonar-web/src/main/js/apps/create/project/types.ts
index f8c9fc9b054..ddd7c3700a4 100644
--- a/server/sonar-web/src/main/js/apps/create/project/types.ts
+++ b/server/sonar-web/src/main/js/apps/create/project/types.ts
@@ -17,6 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ProjectBase } from '../../../api/components';
+import { NewCodePeriodSettingType } from '../../../types/types';
+
export enum CreateProjectModes {
Manual = 'manual',
AzureDevOps = 'azure',
@@ -25,3 +28,8 @@ export enum CreateProjectModes {
GitHub = 'github',
GitLab = 'gitlab',
}
+
+export type CreateProjectApiCallback = (
+ newCodeDefinitionType?: NewCodePeriodSettingType,
+ newCodeDefinitionValue?: string
+) => Promise<{ project: ProjectBase }>;
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 5dcca14156d..c60deb34b08 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
@@ -19,8 +19,10 @@
*/
import * as React from 'react';
import { setNewCodePeriod } from '../../../api/newCodePeriod';
-import Modal from '../../../components/controls/Modal';
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+import Modal from '../../../components/controls/Modal';
+import NewCodeDefinitionDaysOption from '../../../components/new-code-definition/NewCodeDefinitionDaysOption';
+import NewCodeDefinitionPreviousVersionOption from '../../../components/new-code-definition/NewCodeDefinitionPreviousVersionOption';
import NewCodeDefinitionWarning from '../../../components/new-code-definition/NewCodeDefinitionWarning';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { toISO8601WithOffsetString } from '../../../helpers/dates';
@@ -30,8 +32,6 @@ 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 BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion';
import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
import BranchAnalysisList from './BranchAnalysisList';
@@ -170,12 +170,12 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
level="branch"
/>
<div className="display-flex-row huge-spacer-bottom" role="radiogroup">
- <BaselineSettingPreviousVersion
+ <NewCodeDefinitionPreviousVersionOption
isDefault={false}
onSelect={this.handleSelectSetting}
selected={selected === NewCodePeriodSettingType.PREVIOUS_VERSION}
/>
- <BaselineSettingDays
+ <NewCodeDefinitionDaysOption
days={days}
isChanged={isChanged}
isValid={isValid}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx
index a386e12a2d1..39197e19dd2 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx
@@ -24,9 +24,9 @@ import BranchLikeIcon from '../../../components/icons/BranchLikeIcon';
import WarningIcon from '../../../components/icons/WarningIcon';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { isNewCodeDefinitionCompliant } from '../../../helpers/periods';
import { BranchWithNewCodePeriod } from '../../../types/branch-like';
import { NewCodePeriod, NewCodePeriodSettingType } from '../../../types/types';
-import { isNewCodeDefinitionCompliant } from '../../../helpers/periods';
export interface BranchListRowProps {
branch: BranchWithNewCodePeriod;
@@ -50,9 +50,9 @@ function renderNewCodePeriodSetting(newCodePeriod: NewCodePeriod) {
</>
);
case NewCodePeriodSettingType.NUMBER_OF_DAYS:
- return `${translate('baseline.number_days')}: ${newCodePeriod.value}`;
+ return `${translate('new_code_definition.number_days')}: ${newCodePeriod.value}`;
case NewCodePeriodSettingType.PREVIOUS_VERSION:
- return translate('baseline.previous_version');
+ return translate('new_code_definition.previous_version');
case NewCodePeriodSettingType.REFERENCE_BRANCH:
return `${translate('baseline.reference_branch')}: ${newCodePeriod.value}`;
default:
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
index 06503b77976..f1104622cfe 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
@@ -19,23 +19,22 @@
*/
import classNames from 'classnames';
import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import Link from '../../../components/common/Link';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import Radio from '../../../components/controls/Radio';
import Tooltip from '../../../components/controls/Tooltip';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+import GlobalNewCodeDefinitionDescription from '../../../components/new-code-definition/GlobalNewCodeDefinitionDescription';
+import NewCodeDefinitionDaysOption from '../../../components/new-code-definition/NewCodeDefinitionDaysOption';
+import NewCodeDefinitionPreviousVersionOption from '../../../components/new-code-definition/NewCodeDefinitionPreviousVersionOption';
import NewCodeDefinitionWarning from '../../../components/new-code-definition/NewCodeDefinitionWarning';
import { Alert } from '../../../components/ui/Alert';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
import { isNewCodeDefinitionCompliant } from '../../../helpers/periods';
import { Branch } from '../../../types/branch-like';
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 BaselineSettingPreviousVersion from './BaselineSettingPreviousVersion';
import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
import BranchAnalysisList from './BranchAnalysisList';
@@ -63,33 +62,6 @@ export interface ProjectBaselineSelectorProps {
overrideGeneralSetting: boolean;
}
-function renderGeneralSetting(generalSetting: NewCodePeriod) {
- let setting: string;
- let description: string;
- let useCase: string;
- if (generalSetting.type === NewCodePeriodSettingType.NUMBER_OF_DAYS) {
- setting = `${translate('baseline.number_days')} (${translateWithParameters(
- 'duration.days',
- generalSetting.value || '?'
- )})`;
- description = translate('baseline.number_days.description');
- useCase = translate('baseline.number_days.usecase');
- } else {
- setting = translate('baseline.previous_version');
- description = translate('baseline.previous_version.description');
- useCase = translate('baseline.previous_version.usecase');
- }
-
- return (
- <div className="general-setting display-flex-start">
- <span className="sw-font-bold flex-0">{setting}:&nbsp;</span>
- <span>
- {description} {useCase}
- </span>
- </div>
- );
-}
-
function branchToOption(b: Branch) {
return { label: b.name, value: b.name, isMain: b.isMain };
}
@@ -112,7 +84,7 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
selected,
} = props;
- const isGeneralSettingCompliant = isNewCodeDefinitionCompliant(generalSetting);
+ const isGlobalNcdCompliant = isNewCodeDefinitionCompliant(generalSetting);
const { isChanged, isValid } = validateSetting({
analysis,
@@ -130,13 +102,13 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
<Radio
checked={!overrideGeneralSetting}
className="big-spacer-bottom"
- disabled={!isGeneralSettingCompliant}
+ disabled={!isGlobalNcdCompliant}
onCheck={() => props.onToggleSpecificSetting(false)}
value="general"
>
<Tooltip
overlay={
- isGeneralSettingCompliant
+ isGlobalNcdCompliant
? null
: translate('project_baseline.compliance.warning.title.global')
}
@@ -145,34 +117,12 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
</Tooltip>
</Radio>
- <div className="big-spacer-left">
- {!isGeneralSettingCompliant && (
- <Alert variant="warning" className="sw-mb-4 sw-max-w-[800px]">
- <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>
- )}
- {renderGeneralSetting(generalSetting)}
+ <div className="sw-ml-4">
+ <GlobalNewCodeDefinitionDescription
+ globalNcd={generalSetting}
+ isGlobalNcdCompliant={isGlobalNcdCompliant}
+ canAdmin={canAdmin}
+ />
</div>
<Radio
@@ -193,14 +143,14 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
level="project"
/>
<div className="display-flex-row big-spacer-bottom" role="radiogroup">
- <BaselineSettingPreviousVersion
+ <NewCodeDefinitionPreviousVersionOption
disabled={!overrideGeneralSetting}
onSelect={props.onSelectSetting}
selected={
overrideGeneralSetting && selected === NewCodePeriodSettingType.PREVIOUS_VERSION
}
/>
- <BaselineSettingDays
+ <NewCodeDefinitionDaysOption
days={days}
disabled={!overrideGeneralSetting}
isChanged={isChanged}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
index 6cf0cc88d26..c51eb64863a 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
@@ -29,8 +29,8 @@ import { mockComponent } from '../../../../helpers/mocks/component';
import { mockNewCodePeriodBranch } from '../../../../helpers/mocks/new-code-period';
import { mockAppState } from '../../../../helpers/testMocks';
import {
- RenderContext,
renderAppWithComponentContext,
+ RenderContext,
} from '../../../../helpers/testReactTestingUtils';
import { Feature } from '../../../../types/features';
import { NewCodePeriodSettingType } from '../../../../types/types';
@@ -213,7 +213,9 @@ it('can set a previous version setting for branch', async () => {
await ui.appIsLoaded();
await ui.setBranchPreviousVersionSetting('main');
- expect(within(byRole('table').get()).getByText('baseline.previous_version')).toBeInTheDocument();
+ expect(
+ within(byRole('table').get()).getByText('new_code_definition.previous_version')
+ ).toBeInTheDocument();
await user.click(await ui.branchActionsButton('main').find());
@@ -234,7 +236,9 @@ it('can set a number of days setting for branch', async () => {
await ui.setBranchNumberOfDaysSetting('main', '15');
- expect(within(byRole('table').get()).getByText('baseline.number_days: 15')).toBeInTheDocument();
+ expect(
+ within(byRole('table').get()).getByText('new_code_definition.number_days: 15')
+ ).toBeInTheDocument();
});
it('cannot set a specific analysis setting for branch', async () => {
@@ -295,8 +299,10 @@ function getPageObjects() {
generalSettingsLink: byRole('link', { name: 'project_baseline.page.description2.link' }),
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/ }),
+ previousVersionRadio: byRole('radio', {
+ name: /new_code_definition.previous_version.description/,
+ }),
+ numberDaysRadio: byRole('radio', { name: /new_code_definition.number_days.description/ }),
numberDaysInput: byRole('textbox'),
referenceBranchRadio: byRole('radio', { name: /baseline.reference_branch.description/ }),
chooseBranchSelect: byRole('combobox', { name: 'baseline.reference_branch.choose' }),
@@ -311,8 +317,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'),
+ complianceWarningAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
+ complianceWarning: byText('new_code_definition.compliance.warning.explanation'),
};
async function appIsLoaded() {
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 aea830a6d97..96498f2fd8a 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
@@ -23,13 +23,13 @@ import { getNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod';
import DocLink from '../../../components/common/DocLink';
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
+import NewCodeDefinitionDaysOption from '../../../components/new-code-definition/NewCodeDefinitionDaysOption';
+import NewCodeDefinitionPreviousVersionOption from '../../../components/new-code-definition/NewCodeDefinitionPreviousVersionOption';
import NewCodeDefinitionWarning from '../../../components/new-code-definition/NewCodeDefinitionWarning';
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 BaselineSettingPreviousVersion from '../../projectBaseline/components/BaselineSettingPreviousVersion';
interface State {
currentSetting?: NewCodePeriodSettingType;
@@ -184,12 +184,12 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> {
<div className="settings-definition-right">
<DeferredSpinner loading={loading} timeout={500}>
<form onSubmit={this.onSubmit}>
- <BaselineSettingPreviousVersion
+ <NewCodeDefinitionPreviousVersionOption
isDefault
onSelect={this.onSelectSetting}
selected={selected === NewCodePeriodSettingType.PREVIOUS_VERSION}
/>
- <BaselineSettingDays
+ <NewCodeDefinitionDaysOption
className="spacer-top sw-mb-4"
days={days}
isChanged={isChanged}
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 640829ede70..406b219b470 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
@@ -39,9 +39,9 @@ afterEach(() => {
const ui = {
newCodeTitle: byRole('heading', { name: 'settings.new_code_period.title' }),
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 }),
+ prevVersionRadio: byRole('radio', { name: /new_code_definition.previous_version/ }),
+ daysNumberRadio: byRole('radio', { name: /new_code_definition.number_days/ }),
+ daysNumberErrorMessage: byText('new_code_definition.number_days.invalid', { exact: false }),
daysInput: byRole('textbox'),
saveButton: byRole('button', { name: 'save' }),
cancelButton: byRole('button', { name: 'cancel' }),
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx b/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx
new file mode 100644
index 00000000000..2751b703257
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/new-code-definition/GlobalNewCodeDefinitionDescription.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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 * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { NewCodePeriod, NewCodePeriodSettingType } from '../../types/types';
+import Link from '../common/Link';
+import { Alert } from '../ui/Alert';
+
+interface Props {
+ globalNcd: NewCodePeriod;
+ isGlobalNcdCompliant: boolean;
+ canAdmin?: boolean;
+}
+
+export default function GlobalNewCodeDefinitionDescription({
+ globalNcd,
+ isGlobalNcdCompliant,
+ canAdmin,
+}: Props) {
+ let setting: string;
+ let description: string;
+ let useCase: string;
+ if (globalNcd.type === NewCodePeriodSettingType.NUMBER_OF_DAYS) {
+ setting = `${translate('new_code_definition.number_days')} (${translateWithParameters(
+ 'duration.days',
+ globalNcd.value ?? '?'
+ )})`;
+ description = translate('new_code_definition.number_days.description');
+ useCase = translate('new_code_definition.number_days.usecase');
+ } else {
+ setting = translate('new_code_definition.previous_version');
+ description = translate('new_code_definition.previous_version.description');
+ useCase = translate('new_code_definition.previous_version.usecase');
+ }
+
+ return (
+ <>
+ <div className="general-setting display-flex-start">
+ <span className="sw-font-bold flex-0">{setting}:&nbsp;</span>
+ <span>
+ {description} {useCase}
+ </span>
+ </div>
+ {!isGlobalNcdCompliant && (
+ <Alert variant="warning" className="sw-mt-4 sw-max-w-[800px]">
+ <p className="sw-mb-2 sw-font-bold">
+ {translate('new_code_definition.compliance.warning.title.global')}
+ </p>
+ <p className="sw-mb-2">
+ {canAdmin ? (
+ <FormattedMessage
+ id="new_code_definition.compliance.warning.explanation.admin"
+ defaultMessage={translate(
+ 'new_code_definition.compliance.warning.explanation.admin'
+ )}
+ values={{
+ link: (
+ <Link to="/admin/settings?category=new_code_period">
+ {translate(
+ 'new_code_definition.compliance.warning.explanation.action.admin.link'
+ )}
+ </Link>
+ ),
+ }}
+ />
+ ) : (
+ translate('new_code_definition.compliance.warning.explanation')
+ )}
+ </p>
+ </Alert>
+ )}
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx
index b92f1de972a..e6482306e27 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx
+++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx
@@ -18,14 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import RadioCard from '../../../components/controls/RadioCard';
-import ValidationInput, {
- ValidationInputErrorPlacement,
-} from '../../../components/controls/ValidationInput';
-import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { MAX_NUMBER_OF_DAYS, MIN_NUMBER_OF_DAYS } from '../../../helpers/periods';
-import { NewCodePeriodSettingType } from '../../../types/types';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { MAX_NUMBER_OF_DAYS, MIN_NUMBER_OF_DAYS } from '../../helpers/periods';
+import { NewCodePeriodSettingType } from '../../types/types';
+import RadioCard from '../controls/RadioCard';
+import ValidationInput, { ValidationInputErrorPlacement } from '../controls/ValidationInput';
+import MandatoryFieldsExplanation from '../ui/MandatoryFieldsExplanation';
export interface Props {
className?: string;
@@ -38,7 +36,7 @@ export interface Props {
selected: boolean;
}
-export default function BaselineSettingDays(props: Props) {
+export default function NewCodeDefinitionDaysOption(props: Props) {
const { className, days, disabled, isChanged, isValid, onChangeDays, onSelect, selected } = props;
return (
@@ -47,12 +45,12 @@ export default function BaselineSettingDays(props: Props) {
disabled={disabled}
onClick={() => onSelect(NewCodePeriodSettingType.NUMBER_OF_DAYS)}
selected={selected}
- title={translate('baseline.number_days')}
+ title={translate('new_code_definition.number_days')}
>
<>
<div>
- <p className="sw-mb-3">{translate('baseline.number_days.description')}</p>
- <p className="sw-mb-4">{translate('baseline.number_days.usecase')}</p>
+ <p className="sw-mb-3">{translate('new_code_definition.number_days.description')}</p>
+ <p className="sw-mb-4">{translate('new_code_definition.number_days.usecase')}</p>
</div>
{selected && (
<>
@@ -64,11 +62,11 @@ export default function BaselineSettingDays(props: Props) {
isValid={isChanged && isValid}
errorPlacement={ValidationInputErrorPlacement.Bottom}
error={translateWithParameters(
- 'baseline.number_days.invalid',
+ 'new_code_definition.number_days.invalid',
MIN_NUMBER_OF_DAYS,
MAX_NUMBER_OF_DAYS
)}
- label={translate('baseline.specify_days')}
+ label={translate('new_code_definition.number_days.specify_days')}
required
>
<input
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx
index f2e2656ff9c..263dbc0429e 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx
+++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx
@@ -18,31 +18,36 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import RadioCard from '../../../components/controls/RadioCard';
-import { translate } from '../../../helpers/l10n';
-import { NewCodePeriodSettingType } from '../../../types/types';
+import { translate } from '../../helpers/l10n';
+import { NewCodePeriodSettingType } from '../../types/types';
+import RadioCard from '../controls/RadioCard';
-export interface Props {
+interface Props {
disabled?: boolean;
isDefault?: boolean;
onSelect: (selection: NewCodePeriodSettingType) => void;
selected: boolean;
}
-export default function BaselineSettingPreviousVersion(props: Props) {
- const { disabled, isDefault, onSelect, selected } = props;
+export default function NewCodeDefinitionPreviousVersionOption({
+ disabled,
+ isDefault,
+ onSelect,
+ selected,
+}: Props) {
return (
<RadioCard
disabled={disabled}
onClick={() => onSelect(NewCodePeriodSettingType.PREVIOUS_VERSION)}
selected={selected}
title={
- translate('baseline.previous_version') + (isDefault ? ` (${translate('default')})` : '')
+ translate('new_code_definition.previous_version') +
+ (isDefault ? ` (${translate('default')})` : '')
}
>
<div>
- <p>{translate('baseline.previous_version.description')}</p>
- <p className="sw-mt-3">{translate('baseline.previous_version.usecase')}</p>
+ <p>{translate('new_code_definition.previous_version.description')}</p>
+ <p className="sw-mt-3">{translate('new_code_definition.previous_version.usecase')}</p>
</div>
</RadioCard>
);
diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx
new file mode 100644
index 00000000000..a6d8887f6c0
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionSelector.tsx
@@ -0,0 +1,186 @@
+/*
+ * 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 { noop } from 'lodash';
+import * as React from 'react';
+import { useEffect } from 'react';
+import { getNewCodePeriod } from '../../api/newCodePeriod';
+import { translate } from '../../helpers/l10n';
+import { isNewCodeDefinitionCompliant } from '../../helpers/periods';
+import {
+ NewCodePeriod,
+ NewCodePeriodSettingType,
+ NewCodePeriodWithCompliance,
+} from '../../types/types';
+import Radio from '../controls/Radio';
+import RadioCard from '../controls/RadioCard';
+import Tooltip from '../controls/Tooltip';
+import { Alert } from '../ui/Alert';
+import GlobalNewCodeDefinitionDescription from './GlobalNewCodeDefinitionDescription';
+import NewCodeDefinitionDaysOption from './NewCodeDefinitionDaysOption';
+import NewCodeDefinitionPreviousVersionOption from './NewCodeDefinitionPreviousVersionOption';
+
+interface Props {
+ canAdmin: boolean | undefined;
+ onNcdChanged: (ncd: NewCodePeriodWithCompliance) => void;
+}
+
+const INITIAL_DAYS = '30';
+
+export default function NewCodeDefinitionSelector(props: Props) {
+ const { canAdmin, onNcdChanged } = props;
+
+ const [globalNcd, setGlobalNcd] = React.useState<NewCodePeriod | null>(null);
+ const [selectedNcdType, setSelectedNcdType] = React.useState<NewCodePeriodSettingType | null>(
+ null
+ );
+ const [days, setDays] = React.useState<string>(INITIAL_DAYS);
+
+ const iGlobalNcdCompliant = React.useMemo(
+ () => Boolean(globalNcd && isNewCodeDefinitionCompliant(globalNcd)),
+ [globalNcd]
+ );
+
+ const isChanged = React.useMemo(
+ () => selectedNcdType === NewCodePeriodSettingType.NUMBER_OF_DAYS && days !== INITIAL_DAYS,
+ [selectedNcdType, days]
+ );
+
+ const isCompliant = React.useMemo(
+ () =>
+ !!selectedNcdType &&
+ isNewCodeDefinitionCompliant({
+ type: selectedNcdType,
+ value: days,
+ }),
+ [selectedNcdType, days]
+ );
+
+ useEffect(() => {
+ function fetchGlobalNcd() {
+ getNewCodePeriod().then(setGlobalNcd, noop);
+ }
+
+ fetchGlobalNcd();
+ }, []);
+
+ useEffect(() => {
+ if (selectedNcdType) {
+ const type =
+ selectedNcdType === NewCodePeriodSettingType.INHERITED ? undefined : selectedNcdType;
+ const value = selectedNcdType === NewCodePeriodSettingType.NUMBER_OF_DAYS ? days : undefined;
+ onNcdChanged({ isCompliant, type, value });
+ }
+ }, [selectedNcdType, days, isCompliant, onNcdChanged]);
+
+ return (
+ <>
+ <p className="sw-mt-10">
+ <strong>{translate('new_code_definition.question')}</strong>
+ </p>
+ <div className="big-spacer-top spacer-bottom" role="radiogroup">
+ <Radio
+ ariaLabel={translate('new_code_definition.global_setting')}
+ checked={selectedNcdType === NewCodePeriodSettingType.INHERITED}
+ className="big-spacer-bottom"
+ disabled={!iGlobalNcdCompliant}
+ onCheck={() => setSelectedNcdType(NewCodePeriodSettingType.INHERITED)}
+ value="general"
+ >
+ <Tooltip
+ overlay={
+ iGlobalNcdCompliant
+ ? null
+ : translate('new_code_definition.compliance.warning.title.global')
+ }
+ >
+ <span>{translate('new_code_definition.global_setting')}</span>
+ </Tooltip>
+ </Radio>
+
+ <div className="sw-ml-4">
+ {globalNcd && (
+ <GlobalNewCodeDefinitionDescription
+ globalNcd={globalNcd}
+ isGlobalNcdCompliant={iGlobalNcdCompliant}
+ canAdmin={canAdmin}
+ />
+ )}
+ </div>
+
+ <Radio
+ ariaLabel={translate('new_code_definition.specific_setting')}
+ checked={Boolean(
+ selectedNcdType && selectedNcdType !== NewCodePeriodSettingType.INHERITED
+ )}
+ className="huge-spacer-top"
+ onCheck={() => setSelectedNcdType(NewCodePeriodSettingType.PREVIOUS_VERSION)}
+ value="specific"
+ >
+ {translate('new_code_definition.specific_setting')}
+ </Radio>
+ </div>
+
+ <div className="big-spacer-left big-spacer-right project-baseline-setting">
+ <div className="display-flex-row big-spacer-bottom" role="radiogroup">
+ <NewCodeDefinitionPreviousVersionOption
+ disabled={Boolean(
+ !selectedNcdType || selectedNcdType === NewCodePeriodSettingType.INHERITED
+ )}
+ onSelect={setSelectedNcdType}
+ selected={selectedNcdType === NewCodePeriodSettingType.PREVIOUS_VERSION}
+ />
+
+ <NewCodeDefinitionDaysOption
+ days={days}
+ disabled={Boolean(
+ !selectedNcdType || selectedNcdType === NewCodePeriodSettingType.INHERITED
+ )}
+ isChanged={isChanged}
+ isValid={isCompliant}
+ onChangeDays={setDays}
+ onSelect={setSelectedNcdType}
+ selected={selectedNcdType === NewCodePeriodSettingType.NUMBER_OF_DAYS}
+ />
+
+ <RadioCard
+ disabled={Boolean(
+ !selectedNcdType || selectedNcdType === NewCodePeriodSettingType.INHERITED
+ )}
+ onClick={() => setSelectedNcdType(NewCodePeriodSettingType.REFERENCE_BRANCH)}
+ selected={selectedNcdType === NewCodePeriodSettingType.REFERENCE_BRANCH}
+ title={translate('new_code_definition.reference_branch')}
+ >
+ <div>
+ <p className="sw-mb-3">
+ {translate('new_code_definition.reference_branch.description')}
+ </p>
+ <p className="sw-mb-4">{translate('new_code_definition.reference_branch.usecase')}</p>
+ {selectedNcdType === NewCodePeriodSettingType.REFERENCE_BRANCH && (
+ <Alert variant="info">
+ {translate('new_code_definition.reference_branch.notice')}
+ </Alert>
+ )}
+ </div>
+ </RadioCard>
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts
index cd5fcc34769..f31661e0c7f 100644
--- a/server/sonar-web/src/main/js/types/types.ts
+++ b/server/sonar-web/src/main/js/types/types.ts
@@ -404,6 +404,12 @@ export interface NewCodePeriod {
inherited?: boolean;
}
+export interface NewCodePeriodWithCompliance {
+ type?: NewCodePeriodSettingType;
+ value?: string;
+ isCompliant: boolean;
+}
+
export interface NewCodePeriodBranch extends NewCodePeriod {
projectKey: string;
branchKey: string;
@@ -414,6 +420,7 @@ export enum NewCodePeriodSettingType {
NUMBER_OF_DAYS = 'NUMBER_OF_DAYS',
SPECIFIC_ANALYSIS = 'SPECIFIC_ANALYSIS',
REFERENCE_BRANCH = 'REFERENCE_BRANCH',
+ INHERITED = 'INHERITED',
}
export interface Paging {
@@ -617,6 +624,7 @@ export interface Snippet {
export interface SnippetGroup extends SnippetsByComponent {
locations: FlowLocation[];
}
+
export interface SnippetsByComponent {
component: SourceViewerFile;
sources: { [line: number]: SourceLine };