From: Andrey Luiz Date: Fri, 9 Jun 2023 13:58:56 +0000 (+0200) Subject: SONAR-19453 New code definition is made part of the manual project creation (#8486) X-Git-Tag: 10.1.0.73491~51 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=200ef3a99587a0679748e14d8889e61ebc1ef3d1;p=sonarqube.git SONAR-19453 New code definition is made part of the manual project creation (#8486) --- 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 { 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 { mounted = false; + createProjectFnRef: CreateProjectApiCallback | null = null; + state: State = { azureSettings: [], bitbucketSettings: [], @@ -75,6 +86,8 @@ export class CreateProjectPage extends React.PureComponent { + 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 { + 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 ); } @@ -251,9 +289,59 @@ export class CreateProjectPage extends React.PureComponent + + +

{translate('onboarding.create_project.new_code_definition')}

+

+ {translate('onboarding.create_project.new_code_definition.description')} +

+

+ {translate('onboarding.create_project.new_code_definition.description1')} +

+ +

+ + {translate('onboarding.create_project.new_code_definition.description2.link')} + + ), + }} + /> +

+ + + +
+ {translate('back')} + + {translate('onboarding.create_project.new_code_definition.create_project')} + +
+ + ); + } + 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
- {this.renderProjectCreation(mode)} + {isProjectSetupDone ? this.renderNcdSelection() : this.renderProjectCreation(mode)} + {creatingAlmDefinition && ( ({ + ...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 = {}) { + renderApp('project/create', ); +} 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 = {}) { renderComponent( - + ); } 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 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 - -
@@ -308,10 +296,7 @@ export default class ManualProjectCreate extends React.PureComponent - - {translate('set_up')} - - + {translate('next')} {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/BaselineSettingDays.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx deleted file mode 100644 index b92f1de972a..00000000000 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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 ( - onSelect(NewCodePeriodSettingType.NUMBER_OF_DAYS)} - selected={selected} - title={translate('baseline.number_days')} - > - <> -
-

{translate('baseline.number_days.description')}

-

{translate('baseline.number_days.usecase')}

-
- {selected && ( - <> - - - - onChangeDays(e.currentTarget.value)} - type="text" - value={days} - /> - - - )} - -
- ); -} diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx deleted file mode 100644 index f2e2656ff9c..00000000000 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 ( - onSelect(NewCodePeriodSettingType.PREVIOUS_VERSION)} - selected={selected} - title={ - translate('baseline.previous_version') + (isDefault ? ` (${translate('default')})` : '') - } - > -
-

{translate('baseline.previous_version.description')}

-

{translate('baseline.previous_version.usecase')}

-
-
- ); -} 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
- - ); 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 ( -
- {setting}:  - - {description} {useCase} - -
- ); -} - 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 props.onToggleSpecificSetting(false)} value="general" > -
- {!isGeneralSettingCompliant && ( - -

- {translate('project_baseline.compliance.warning.title.global')} -

-

- {canAdmin ? ( - - {translate('project_baseline.warning.explanation.action.admin.link')} - - ), - }} - /> - ) : ( - translate('project_baseline.compliance.warning.explanation') - )} -

-
- )} - {renderGeneralSetting(generalSetting)} +
+
- - { 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> {
- - { 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 ( + <> +
+ {setting}:  + + {description} {useCase} + +
+ {!isGlobalNcdCompliant && ( + +

+ {translate('new_code_definition.compliance.warning.title.global')} +

+

+ {canAdmin ? ( + + {translate( + 'new_code_definition.compliance.warning.explanation.action.admin.link' + )} + + ), + }} + /> + ) : ( + translate('new_code_definition.compliance.warning.explanation') + )} +

+
+ )} + + ); +} diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx new file mode 100644 index 00000000000..e6482306e27 --- /dev/null +++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionDaysOption.tsx @@ -0,0 +1,84 @@ +/* + * 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 ( + onSelect(NewCodePeriodSettingType.NUMBER_OF_DAYS)} + selected={selected} + title={translate('new_code_definition.number_days')} + > + <> +
+

{translate('new_code_definition.number_days.description')}

+

{translate('new_code_definition.number_days.usecase')}

+
+ {selected && ( + <> + + + + onChangeDays(e.currentTarget.value)} + type="text" + value={days} + /> + + + )} + +
+ ); +} diff --git a/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx new file mode 100644 index 00000000000..263dbc0429e --- /dev/null +++ b/server/sonar-web/src/main/js/components/new-code-definition/NewCodeDefinitionPreviousVersionOption.tsx @@ -0,0 +1,54 @@ +/* + * 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 ( + onSelect(NewCodePeriodSettingType.PREVIOUS_VERSION)} + selected={selected} + title={ + translate('new_code_definition.previous_version') + + (isDefault ? ` (${translate('default')})` : '') + } + > +
+

{translate('new_code_definition.previous_version.description')}

+

{translate('new_code_definition.previous_version.usecase')}

+
+
+ ); +} 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(null); + const [selectedNcdType, setSelectedNcdType] = React.useState( + null + ); + const [days, setDays] = React.useState(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 ( + <> +

+ {translate('new_code_definition.question')} +

+
+ setSelectedNcdType(NewCodePeriodSettingType.INHERITED)} + value="general" + > + + {translate('new_code_definition.global_setting')} + + + +
+ {globalNcd && ( + + )} +
+ + setSelectedNcdType(NewCodePeriodSettingType.PREVIOUS_VERSION)} + value="specific" + > + {translate('new_code_definition.specific_setting')} + +
+ +
+
+ + + + + setSelectedNcdType(NewCodePeriodSettingType.REFERENCE_BRANCH)} + selected={selectedNcdType === NewCodePeriodSettingType.REFERENCE_BRANCH} + title={translate('new_code_definition.reference_branch')} + > +
+

+ {translate('new_code_definition.reference_branch.description')} +

+

{translate('new_code_definition.reference_branch.usecase')}

+ {selectedNcdType === NewCodePeriodSettingType.REFERENCE_BRANCH && ( + + {translate('new_code_definition.reference_branch.notice')} + + )} +
+
+
+
+ + ); +} 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 }; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index f90095377ee..7bcc51b0e61 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -136,6 +136,7 @@ name=Name navigation=Navigation never=Never new=New +next=Next new_name=New name next_=next none=None @@ -645,13 +646,6 @@ project_baseline.compliance.warning.explanation=Please ask an administrator to u 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. @@ -669,7 +663,6 @@ baseline.reference_branch.description=Choose a branch as the baseline for the ne 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 @@ -3751,6 +3744,35 @@ footer.version_x=Version {0} 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 @@ -3890,6 +3912,14 @@ onboarding.create_project.gitlab.title=Gitlab project 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