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);
}
* 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;
githubSettings: AlmSettingsInstance[];
gitlabSettings: AlmSettingsInstance[];
loading: boolean;
+ isProjectSetupDone: boolean;
creatingAlmDefinition?: AlmKeys;
+ selectedNcd: NewCodePeriodWithCompliance | null;
}
const PROJECT_MODE_FOR_ALM_KEY = {
export class CreateProjectPage extends React.PureComponent<CreateProjectPageProps, State> {
mounted = false;
+ createProjectFnRef: CreateProjectApiCallback | null = null;
+
state: State = {
azureSettings: [],
bitbucketSettings: [],
githubSettings: [],
gitlabSettings: [],
loading: true,
+ isProjectSetupDone: false,
+ selectedNcd: null,
};
componentDidMount() {
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 });
};
}
};
+ handleNcdChanged = (ncd: NewCodePeriodWithCompliance) => {
+ this.setState({
+ selectedNcd: ncd,
+ });
+ };
+
+ handleGoBack = () => {
+ this.setState({ isProjectSetupDone: false });
+ };
+
renderProjectCreation(mode?: CreateProjectModes) {
const {
appState: { canAdmin },
return (
<ManualProjectCreate
branchesEnabled={branchSupportEnabled}
- onProjectCreate={this.handleProjectCreate}
+ onProjectSetupDone={this.handleProjectSetupDone}
/>
);
}
}
}
+ 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 (
<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}
--- /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 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} />);
+}
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')),
getValue: jest.fn().mockResolvedValue({ value: 'main' }),
}));
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
-
-beforeAll(() => {
- newCodePeriodHandler = new NewCodePeriodsServiceMock();
-});
-
beforeEach(() => {
jest.clearAllMocks();
- newCodePeriodHandler.reset();
});
it('should show branch information', 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(
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(
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(
})
);
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(
function renderManualProjectCreate(props: Partial<ManualProjectCreate['props']> = {}) {
renderComponent(
- <ManualProjectCreate branchesEnabled={false} onProjectCreate={jest.fn()} {...props} />
+ <ManualProjectCreate branchesEnabled={false} onProjectSetupDone={jest.fn()} {...props} />
);
}
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 {
mainBranchName: string;
mainBranchNameError?: string;
mainBranchNameTouched: boolean;
- submitting: boolean;
}
const DEBOUNCE_DELAY = 250;
this.state = {
projectKey: '',
projectName: '',
- submitting: false,
projectKeyTouched: false,
projectNameTouched: false,
mainBranchName: 'main',
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,
+ })
);
}
};
mainBranchName,
mainBranchNameError,
mainBranchNameTouched,
- submitting,
} = this.state;
const { branchesEnabled } = this.props;
<>
<CreateProjectPageHeader title={translate('onboarding.create_project.setup_manually')} />
- <InstanceNewCodeDefinitionComplianceWarning />
-
<form id="create-project-manual" onSubmit={this.handleFormSubmit}>
<MandatoryFieldsExplanation className="big-spacer-bottom" />
/>
</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 && (
* 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',
GitHub = 'github',
GitLab = 'gitlab',
}
+
+export type CreateProjectApiCallback = (
+ newCodeDefinitionType?: NewCodePeriodSettingType,
+ newCodeDefinitionValue?: string
+) => Promise<{ project: ProjectBase }>;
+++ /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 * 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';
-
-export interface Props {
- className?: string;
- days: string;
- disabled?: boolean;
- isChanged: boolean;
- isValid: boolean;
- onChangeDays: (value: string) => void;
- onSelect: (selection: NewCodePeriodSettingType) => void;
- selected: boolean;
-}
-
-export default function BaselineSettingDays(props: Props) {
- const { className, days, disabled, isChanged, isValid, onChangeDays, onSelect, selected } = props;
-
- return (
- <RadioCard
- className={className}
- disabled={disabled}
- onClick={() => onSelect(NewCodePeriodSettingType.NUMBER_OF_DAYS)}
- selected={selected}
- title={translate('baseline.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>
- </div>
- {selected && (
- <>
- <MandatoryFieldsExplanation />
-
- <ValidationInput
- labelHtmlFor="baseline_number_of_days"
- isInvalid={!isValid}
- isValid={isChanged && isValid}
- errorPlacement={ValidationInputErrorPlacement.Bottom}
- error={translateWithParameters(
- 'baseline.number_days.invalid',
- MIN_NUMBER_OF_DAYS,
- MAX_NUMBER_OF_DAYS
- )}
- label={translate('baseline.specify_days')}
- required
- >
- <input
- id="baseline_number_of_days"
- onChange={(e) => onChangeDays(e.currentTarget.value)}
- type="text"
- value={days}
- />
- </ValidationInput>
- </>
- )}
- </>
- </RadioCard>
- );
-}
+++ /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 * as React from 'react';
-import RadioCard from '../../../components/controls/RadioCard';
-import { translate } from '../../../helpers/l10n';
-import { NewCodePeriodSettingType } from '../../../types/types';
-
-export interface Props {
- disabled?: boolean;
- isDefault?: boolean;
- onSelect: (selection: NewCodePeriodSettingType) => void;
- selected: boolean;
-}
-
-export default function BaselineSettingPreviousVersion(props: Props) {
- const { disabled, isDefault, onSelect, selected } = props;
- return (
- <RadioCard
- disabled={disabled}
- onClick={() => onSelect(NewCodePeriodSettingType.PREVIOUS_VERSION)}
- selected={selected}
- title={
- translate('baseline.previous_version') + (isDefault ? ` (${translate('default')})` : '')
- }
- >
- <div>
- <p>{translate('baseline.previous_version.description')}</p>
- <p className="sw-mt-3">{translate('baseline.previous_version.usecase')}</p>
- </div>
- </RadioCard>
- );
-}
*/
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';
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';
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}
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;
</>
);
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:
*/
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';
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}: </span>
- <span>
- {description} {useCase}
- </span>
- </div>
- );
-}
-
function branchToOption(b: Branch) {
return { label: b.name, value: b.name, isMain: b.isMain };
}
selected,
} = props;
- const isGeneralSettingCompliant = isNewCodeDefinitionCompliant(generalSetting);
+ const isGlobalNcdCompliant = isNewCodeDefinitionCompliant(generalSetting);
const { isChanged, isValid } = validateSetting({
analysis,
<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')
}
</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
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}
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';
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());
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 () => {
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' }),
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() {
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;
<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}
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' }),
--- /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 * 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}: </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>
+ )}
+ </>
+ );
+}
--- /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 * as React from 'react';
+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;
+ days: string;
+ disabled?: boolean;
+ isChanged: boolean;
+ isValid: boolean;
+ onChangeDays: (value: string) => void;
+ onSelect: (selection: NewCodePeriodSettingType) => void;
+ selected: boolean;
+}
+
+export default function NewCodeDefinitionDaysOption(props: Props) {
+ const { className, days, disabled, isChanged, isValid, onChangeDays, onSelect, selected } = props;
+
+ return (
+ <RadioCard
+ className={className}
+ disabled={disabled}
+ onClick={() => onSelect(NewCodePeriodSettingType.NUMBER_OF_DAYS)}
+ selected={selected}
+ title={translate('new_code_definition.number_days')}
+ >
+ <>
+ <div>
+ <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 && (
+ <>
+ <MandatoryFieldsExplanation />
+
+ <ValidationInput
+ labelHtmlFor="baseline_number_of_days"
+ isInvalid={!isValid}
+ isValid={isChanged && isValid}
+ errorPlacement={ValidationInputErrorPlacement.Bottom}
+ error={translateWithParameters(
+ 'new_code_definition.number_days.invalid',
+ MIN_NUMBER_OF_DAYS,
+ MAX_NUMBER_OF_DAYS
+ )}
+ label={translate('new_code_definition.number_days.specify_days')}
+ required
+ >
+ <input
+ id="baseline_number_of_days"
+ onChange={(e) => onChangeDays(e.currentTarget.value)}
+ type="text"
+ value={days}
+ />
+ </ValidationInput>
+ </>
+ )}
+ </>
+ </RadioCard>
+ );
+}
--- /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 * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import { NewCodePeriodSettingType } from '../../types/types';
+import RadioCard from '../controls/RadioCard';
+
+interface Props {
+ disabled?: boolean;
+ isDefault?: boolean;
+ onSelect: (selection: NewCodePeriodSettingType) => void;
+ selected: boolean;
+}
+
+export default function NewCodeDefinitionPreviousVersionOption({
+ disabled,
+ isDefault,
+ onSelect,
+ selected,
+}: Props) {
+ return (
+ <RadioCard
+ disabled={disabled}
+ onClick={() => onSelect(NewCodePeriodSettingType.PREVIOUS_VERSION)}
+ selected={selected}
+ title={
+ translate('new_code_definition.previous_version') +
+ (isDefault ? ` (${translate('default')})` : '')
+ }
+ >
+ <div>
+ <p>{translate('new_code_definition.previous_version.description')}</p>
+ <p className="sw-mt-3">{translate('new_code_definition.previous_version.usecase')}</p>
+ </div>
+ </RadioCard>
+ );
+}
--- /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 { 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>
+ </>
+ );
+}
inherited?: boolean;
}
+export interface NewCodePeriodWithCompliance {
+ type?: NewCodePeriodSettingType;
+ value?: string;
+ isCompliant: boolean;
+}
+
export interface NewCodePeriodBranch extends NewCodePeriod {
projectKey: string;
branchKey: string;
NUMBER_OF_DAYS = 'NUMBER_OF_DAYS',
SPECIFIC_ANALYSIS = 'SPECIFIC_ANALYSIS',
REFERENCE_BRANCH = 'REFERENCE_BRANCH',
+ INHERITED = 'INHERITED',
}
export interface Paging {
export interface SnippetGroup extends SnippetsByComponent {
locations: FlowLocation[];
}
+
export interface SnippetsByComponent {
component: SourceViewerFile;
sources: { [line: number]: SourceLine };
navigation=Navigation
never=Never
new=New
+next=Next
new_name=New name
next_=next
none=None
project_baseline.compliance.warning.explanation.admin=Please update the global new code definition under {link} before switching back to it.
project_baseline.warning.explanation.action.admin.link=General Settings > New Code
-baseline.previous_version=Previous version
-baseline.previous_version.usecase=Recommended for projects following regular versions or releases.
-baseline.previous_version.description=Any code that has changed since the previous version is considered new code.
-baseline.number_days=Number of days
-baseline.number_days.usecase=Recommended for projects following continuous delivery.
-baseline.number_days.description=Any code that has changed in the last x days is considered new code. If no action is taken on a new issue after x days, this issue will become part of the overall code.
-baseline.number_days.invalid=Please provide a whole number between {0} and {1}
baseline.number_days.compliance_warning.title=Your new code definition is not compliant with the Clean as You Code methodology
baseline.number_days.compliance_warning.content.global=We recommend that you update this new code definition so that new projects and existing projects that do not use a specific New Code definition benefit from the Clean as You Code methodology by default.
baseline.number_days.compliance_warning.content.project=We recommend that you update this new code definition so that your project benefits from the Clean as You Code methodology.
baseline.reference_branch.usecase=Recommended for projects using feature branches.
baseline.reference_branch.description2=The branch you select as the reference branch will need its own new code definition to prevent it from using itself as a reference.
-baseline.specify_days=Specify a number of days
baseline.last_analysis_before=Last analysis before
baseline.next_analysis_notice=Changes will take effect after the next analysis
footer.web_api=Web API
+#------------------------------------------------------------------------------
+#
+# NEW CODE DEFINITION
+#
+#------------------------------------------------------------------------------
+new_code_definition.question=What should be the baseline for new code for this project?
+new_code_definition.global_setting=Use the global setting
+new_code_definition.specific_setting=Define a specific setting for this project
+
+new_code_definition.compliance.warning.title.global=Your global new code definition is not compliant with the Clean as You Code methodology
+new_code_definition.compliance.warning.explanation=Please ask an administrator to update the global new code definition before switching back to it.
+new_code_definition.compliance.warning.explanation.admin=Please update the global new code definition under {link} before switching back to it.
+new_code_definition.compliance.warning.explanation.action.admin.link=General Settings > New Code
+
+new_code_definition.previous_version=Previous version
+new_code_definition.previous_version.usecase=Recommended for projects following regular versions or releases.
+new_code_definition.previous_version.description=Any code that has changed since the previous version is considered new code.
+
+new_code_definition.number_days=Number of days
+new_code_definition.number_days.specify_days=Specify a number of days
+new_code_definition.number_days.usecase=Recommended for projects following continuous delivery.
+new_code_definition.number_days.description=Any code that has changed in the last x days is considered new code. If no action is taken on a new issue after x days, this issue will become part of the overall code.
+new_code_definition.number_days.invalid=Please provide a whole number between {0} and {1}
+
+new_code_definition.reference_branch=Reference branch
+new_code_definition.reference_branch.description=Choose a branch as the baseline for the new code.
+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.
+
#------------------------------------------------------------------------------
#
# ONBOARDING
onboarding.create_project.gitlab.no_projects=No projects could be fetched from Gitlab. Contact your system administrator, or {link}.
onboarding.create_project.gitlab.link=See on GitLab
+onboarding.create_project.new_code_definition.title=Set up project for Clean as You Code
+onboarding.create_project.new_code_definition=New Code
+onboarding.create_project.new_code_definition.description=The new code definition sets which part of your code will be considered new code.
+onboarding.create_project.new_code_definition.description1=This helps you focus attention on the most recent changes to your project, enabling you to follow the Clean as You Code methodology.
+onboarding.create_project.new_code_definition.description2=Learn more: {link}
+onboarding.create_project.new_code_definition.description2.link=Defining New Code
+onboarding.create_project.new_code_definition.create_project=Create project
+
onboarding.create_project.new_code_option.warning.title=Your global new code definition is not compliant with the Clean as You Code methodology
onboarding.create_project.new_code_option.warning.explanation=New projects use the global new code definition by default. {action} so that new projects benefit from the Clean as You Code methodology by default.
onboarding.create_project.new_code_option.warning.explanation.action=We recommend that you ask an administrator of this SonarQube instance to update the global new code definition