--- /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 { cloneDeep } from 'lodash';
+import {
+ mockNewCodePeriod,
+ mockNewCodePeriodBranch,
+} from '../../helpers/mocks/new-code-definition';
+import {
+ NewCodeDefinition,
+ NewCodeDefinitionBranch,
+ NewCodeDefinitionType,
+} from '../../types/new-code-definition';
+import {
+ getNewCodeDefinition,
+ listBranchesNewCodeDefinition,
+ resetNewCodeDefinition,
+ setNewCodeDefinition,
+} from '../newCodeDefinition';
+
+jest.mock('../newCodeDefinition');
+export default class NewCodeDefinitionServiceMock {
+ #defaultNewCodePeriod = mockNewCodePeriod({ inherited: true });
+ #defaultListBranchesNewCode = [
+ mockNewCodePeriodBranch({ inherited: true, branchKey: 'main' }),
+ mockNewCodePeriodBranch({
+ branchKey: 'feature',
+ type: NewCodeDefinitionType.NumberOfDays,
+ value: '1',
+ }),
+ ];
+
+ #newCodePeriod: NewCodeDefinition;
+ #listBranchesNewCode: NewCodeDefinitionBranch[];
+
+ constructor() {
+ this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
+ this.#listBranchesNewCode = cloneDeep(this.#defaultListBranchesNewCode);
+ jest.mocked(getNewCodeDefinition).mockImplementation(this.handleGetNewCodePeriod);
+ jest.mocked(setNewCodeDefinition).mockImplementation(this.handleSetNewCodePeriod);
+ jest.mocked(resetNewCodeDefinition).mockImplementation(this.handleResetNewCodePeriod);
+ jest
+ .mocked(listBranchesNewCodeDefinition)
+ .mockImplementation(this.handleListBranchesNewCodePeriod);
+ }
+
+ handleGetNewCodePeriod = () => {
+ return this.reply(this.#newCodePeriod);
+ };
+
+ handleSetNewCodePeriod = (data: {
+ project?: string;
+ branch?: string;
+ type: NewCodeDefinitionType;
+ value?: string;
+ }) => {
+ const { type, value, branch } = data;
+ if (branch) {
+ const branchNewCode = this.#listBranchesNewCode.find(
+ (bNew) => bNew.branchKey === branch
+ ) as NewCodeDefinitionBranch;
+ branchNewCode.type = type;
+ branchNewCode.value = value;
+ } else {
+ this.#newCodePeriod = mockNewCodePeriod({ type, value });
+ }
+
+ return this.reply(undefined);
+ };
+
+ handleResetNewCodePeriod = (data: { project?: string; branch?: string }) => {
+ const { branch } = data;
+ if (branch) {
+ const index = this.#listBranchesNewCode.findIndex((bNew) => bNew.branchKey === branch);
+ if (index >= 0) {
+ Object.assign(this.#listBranchesNewCode[index], cloneDeep(this.#defaultNewCodePeriod));
+ }
+ } else {
+ this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
+ }
+
+ return this.reply(undefined);
+ };
+
+ handleListBranchesNewCodePeriod = () => {
+ return this.reply({ newCodePeriods: this.#listBranchesNewCode });
+ };
+
+ setNewCodePeriod = (newCodePeriod: NewCodeDefinition) => {
+ this.#newCodePeriod = newCodePeriod;
+ };
+
+ setListBranchesNewCode = (listBranchesNewCode: NewCodeDefinitionBranch[]) => {
+ this.#listBranchesNewCode = listBranchesNewCode;
+ };
+
+ reset = () => {
+ this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
+ this.#listBranchesNewCode = cloneDeep(this.#defaultListBranchesNewCode);
+ };
+
+ reply<T>(response: T): Promise<T> {
+ return Promise.resolve(cloneDeep(response));
+ }
+}
+++ /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 { cloneDeep } from 'lodash';
-import {
- mockNewCodePeriod,
- mockNewCodePeriodBranch,
-} from '../../helpers/mocks/new-code-definition';
-import {
- NewCodeDefinition,
- NewCodeDefinitionBranch,
- NewCodeDefinitionType,
-} from '../../types/new-code-definition';
-import {
- getNewCodePeriod,
- listBranchesNewCodePeriod,
- resetNewCodePeriod,
- setNewCodePeriod,
-} from '../newCodePeriod';
-
-jest.mock('../newCodePeriod');
-export default class NewCodePeriodsServiceMock {
- #defaultNewCodePeriod = mockNewCodePeriod({ inherited: true });
- #defaultListBranchesNewCode = [
- mockNewCodePeriodBranch({ inherited: true, branchKey: 'main' }),
- mockNewCodePeriodBranch({
- branchKey: 'feature',
- type: NewCodeDefinitionType.NumberOfDays,
- value: '1',
- }),
- ];
-
- #newCodePeriod: NewCodeDefinition;
- #listBranchesNewCode: NewCodeDefinitionBranch[];
-
- constructor() {
- this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
- this.#listBranchesNewCode = cloneDeep(this.#defaultListBranchesNewCode);
- jest.mocked(getNewCodePeriod).mockImplementation(this.handleGetNewCodePeriod);
- jest.mocked(setNewCodePeriod).mockImplementation(this.handleSetNewCodePeriod);
- jest.mocked(resetNewCodePeriod).mockImplementation(this.handleResetNewCodePeriod);
- jest.mocked(listBranchesNewCodePeriod).mockImplementation(this.handleListBranchesNewCodePeriod);
- }
-
- handleGetNewCodePeriod = () => {
- return this.reply(this.#newCodePeriod);
- };
-
- handleSetNewCodePeriod = (data: {
- project?: string;
- branch?: string;
- type: NewCodeDefinitionType;
- value?: string;
- }) => {
- const { type, value, branch } = data;
- if (branch) {
- const branchNewCode = this.#listBranchesNewCode.find(
- (bNew) => bNew.branchKey === branch
- ) as NewCodeDefinitionBranch;
- branchNewCode.type = type;
- branchNewCode.value = value;
- } else {
- this.#newCodePeriod = mockNewCodePeriod({ type, value });
- }
-
- return this.reply(undefined);
- };
-
- handleResetNewCodePeriod = (data: { project?: string; branch?: string }) => {
- const { branch } = data;
- if (branch) {
- const index = this.#listBranchesNewCode.findIndex((bNew) => bNew.branchKey === branch);
- if (index >= 0) {
- Object.assign(this.#listBranchesNewCode[index], cloneDeep(this.#defaultNewCodePeriod));
- }
- } else {
- this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
- }
-
- return this.reply(undefined);
- };
-
- handleListBranchesNewCodePeriod = () => {
- return this.reply({ newCodePeriods: this.#listBranchesNewCode });
- };
-
- setNewCodePeriod = (newCodePeriod: NewCodeDefinition) => {
- this.#newCodePeriod = newCodePeriod;
- };
-
- setListBranchesNewCode = (listBranchesNewCode: NewCodeDefinitionBranch[]) => {
- this.#listBranchesNewCode = listBranchesNewCode;
- };
-
- reset = () => {
- this.#newCodePeriod = cloneDeep(this.#defaultNewCodePeriod);
- this.#listBranchesNewCode = cloneDeep(this.#defaultListBranchesNewCode);
- };
-
- reply<T>(response: T): Promise<T> {
- return Promise.resolve(cloneDeep(response));
- }
-}
--- /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 { throwGlobalError } from '../helpers/error';
+import { getJSON, post } from '../helpers/request';
+import {
+ NewCodeDefinition,
+ NewCodeDefinitionBranch,
+ NewCodeDefinitionType,
+} from '../types/new-code-definition';
+
+export function getNewCodeDefinition(data?: {
+ project?: string;
+ branch?: string;
+}): Promise<Omit<NewCodeDefinition, 'effectiveValue'>> {
+ return getJSON('/api/new_code_periods/show', data).catch(throwGlobalError);
+}
+
+export function setNewCodeDefinition(data: {
+ project?: string;
+ branch?: string;
+ type: NewCodeDefinitionType;
+ value?: string;
+}): Promise<void> {
+ return post('/api/new_code_periods/set', data).catch(throwGlobalError);
+}
+
+export function resetNewCodeDefinition(data: { project?: string; branch?: string }): Promise<void> {
+ return post('/api/new_code_periods/unset', data).catch(throwGlobalError);
+}
+
+export function listBranchesNewCodeDefinition(data: {
+ project: string;
+}): Promise<{ newCodePeriods: NewCodeDefinitionBranch[] }> {
+ return getJSON('/api/new_code_periods/list', data).catch(throwGlobalError);
+}
+++ /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 { throwGlobalError } from '../helpers/error';
-import { getJSON, post } from '../helpers/request';
-import {
- NewCodeDefinition,
- NewCodeDefinitionBranch,
- NewCodeDefinitionType,
-} from '../types/new-code-definition';
-
-export function getNewCodePeriod(data?: {
- project?: string;
- branch?: string;
-}): Promise<Omit<NewCodeDefinition, 'effectiveValue'>> {
- return getJSON('/api/new_code_periods/show', data).catch(throwGlobalError);
-}
-
-export function setNewCodePeriod(data: {
- project?: string;
- branch?: string;
- type: NewCodeDefinitionType;
- value?: string;
-}): Promise<void> {
- return post('/api/new_code_periods/set', data).catch(throwGlobalError);
-}
-
-export function resetNewCodePeriod(data: { project?: string; branch?: string }): Promise<void> {
- return post('/api/new_code_periods/unset', data).catch(throwGlobalError);
-}
-
-export function listBranchesNewCodePeriod(data: {
- project: string;
-}): Promise<{ newCodePeriods: NewCodeDefinitionBranch[] }> {
- return getJSON('/api/new_code_periods/list', data).catch(throwGlobalError);
-}
import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/permissions/routes';
import projectActivityRoutes from '../../apps/projectActivity/routes';
-import projectBaselineRoutes from '../../apps/projectBaseline/routes';
import projectBranchesRoutes from '../../apps/projectBranches/routes';
import ProjectDeletionApp from '../../apps/projectDeletion/App';
import projectDumpRoutes from '../../apps/projectDump/routes';
import projectInfoRoutes from '../../apps/projectInformation/routes';
import ProjectKeyApp from '../../apps/projectKey/ProjectKeyApp';
import ProjectLinksApp from '../../apps/projectLinks/ProjectLinksApp';
+import projectNewCodeDefinitionRoutes from '../../apps/projectNewCode/routes';
import projectQualityGateRoutes from '../../apps/projectQualityGate/routes';
import projectQualityProfilesRoutes from '../../apps/projectQualityProfiles/routes';
import projectsRoutes from '../../apps/projects/routes';
element={<ProjectAdminPageExtension />}
/>
{backgroundTasksRoutes()}
- {projectBaselineRoutes()}
+ {projectNewCodeDefinitionRoutes()}
{projectBranchesRoutes()}
{projectDumpRoutes()}
{settingsRoutes()}
import { searchAzureRepositories } from '../../../../api/alm-integrations';
import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
import CreateProjectPage from '../CreateProjectPage';
let almIntegrationHandler: AlmIntegrationsServiceMock;
let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
const ui = {
azureCreateProjectButton: byText('onboarding.create_project.select_method.azure'),
});
almIntegrationHandler = new AlmIntegrationsServiceMock();
almSettingsHandler = new AlmSettingsServiceMock();
- newCodePeriodHandler = new NewCodePeriodsServiceMock();
+ newCodePeriodHandler = new NewCodeDefinitionServiceMock();
});
beforeEach(() => {
import { searchForBitbucketServerRepositories } from '../../../../api/alm-integrations';
import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
import CreateProjectPage from '../CreateProjectPage';
let almIntegrationHandler: AlmIntegrationsServiceMock;
let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
const ui = {
bitbucketServerCreateProjectButton: byText('onboarding.create_project.select_method.bitbucket'),
});
almIntegrationHandler = new AlmIntegrationsServiceMock();
almSettingsHandler = new AlmSettingsServiceMock();
- newCodePeriodHandler = new NewCodePeriodsServiceMock();
+ newCodePeriodHandler = new NewCodeDefinitionServiceMock();
});
beforeEach(() => {
import { searchForBitbucketCloudRepositories } from '../../../../api/alm-integrations';
import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
import CreateProjectPage from '../CreateProjectPage';
let almIntegrationHandler: AlmIntegrationsServiceMock;
let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
const ui = {
bitbucketCloudCreateProjectButton: byText(
});
almIntegrationHandler = new AlmIntegrationsServiceMock();
almSettingsHandler = new AlmSettingsServiceMock();
- newCodePeriodHandler = new NewCodePeriodsServiceMock();
+ newCodePeriodHandler = new NewCodeDefinitionServiceMock();
});
beforeEach(() => {
import * as React from 'react';
import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
import { mockAppState } from '../../../../helpers/testMocks';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { AlmKeys } from '../../../../types/alm-settings';
let almIntegrationHandler: AlmIntegrationsServiceMock;
let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
const original = window.location;
});
almIntegrationHandler = new AlmIntegrationsServiceMock();
almSettingsHandler = new AlmSettingsServiceMock();
- newCodePeriodHandler = new NewCodePeriodsServiceMock();
+ newCodePeriodHandler = new NewCodeDefinitionServiceMock();
});
beforeEach(() => {
import { getGithubRepositories } from '../../../../api/alm-integrations';
import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { byLabelText, byText } from '../../../../helpers/testSelector';
import CreateProjectPage from '../CreateProjectPage';
let almIntegrationHandler: AlmIntegrationsServiceMock;
let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
const ui = {
githubCreateProjectButton: byText('onboarding.create_project.select_method.github'),
});
almIntegrationHandler = new AlmIntegrationsServiceMock();
almSettingsHandler = new AlmSettingsServiceMock();
- newCodePeriodHandler = new NewCodePeriodsServiceMock();
+ newCodePeriodHandler = new NewCodeDefinitionServiceMock();
});
beforeEach(() => {
import { getGitlabProjects } from '../../../../api/alm-integrations';
import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
import CreateProjectPage from '../CreateProjectPage';
let almIntegrationHandler: AlmIntegrationsServiceMock;
let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
const ui = {
gitlabCreateProjectButton: byText('onboarding.create_project.select_method.gitlab'),
});
almIntegrationHandler = new AlmIntegrationsServiceMock();
almSettingsHandler = new AlmSettingsServiceMock();
- newCodePeriodHandler = new NewCodePeriodsServiceMock();
+ newCodePeriodHandler = new NewCodeDefinitionServiceMock();
});
beforeEach(() => {
import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
import * as React from 'react';
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
-import { getNewCodePeriod } from '../../../../api/newCodePeriod';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
+import { getNewCodeDefinition } from '../../../../api/newCodeDefinition';
import { mockProject } from '../../../../helpers/mocks/projects';
import { mockAppState } from '../../../../helpers/testMocks';
import { renderApp } from '../../../../helpers/testReactTestingUtils';
import CreateProjectPage, { CreateProjectPageProps } from '../CreateProjectPage';
jest.mock('../../../../api/alm-settings');
-jest.mock('../../../../api/newCodePeriod');
+jest.mock('../../../../api/newCodeDefinition');
jest.mock('../../../../api/project-management', () => ({
setupManualProjectCreation: jest
.fn()
}
let almSettingsHandler: AlmSettingsServiceMock;
-let newCodePeriodHandler: NewCodePeriodsServiceMock;
+let newCodePeriodHandler: NewCodeDefinitionServiceMock;
const original = window.location;
value: { replace: jest.fn() },
});
almSettingsHandler = new AlmSettingsServiceMock();
- newCodePeriodHandler = new NewCodePeriodsServiceMock();
+ newCodePeriodHandler = new NewCodeDefinitionServiceMock();
});
beforeEach(() => {
it('should select the global NCD when it is compliant', async () => {
jest
- .mocked(getNewCodePeriod)
+ .mocked(getNewCodeDefinition)
.mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '30' });
const user = userEvent.setup();
renderCreateProject();
it('global NCD option should be disabled if not compliant', async () => {
jest
- .mocked(getNewCodePeriod)
+ .mocked(getNewCodeDefinition)
.mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' });
const user = userEvent.setup();
renderCreateProject();
'should show warning message when global NCD is not compliant',
async ({ canAdmin, message }) => {
jest
- .mocked(getNewCodePeriod)
+ .mocked(getNewCodeDefinition)
.mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' });
const user = userEvent.setup();
renderCreateProject({ appState: mockAppState({ canAdmin }) });
'should override the global NCD and pick a compliant NCD',
async (option) => {
jest
- .mocked(getNewCodePeriod)
+ .mocked(getNewCodeDefinition)
.mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '96' });
const user = userEvent.setup();
renderCreateProject();
it('number of days ignores non-numeric inputs', async () => {
jest
- .mocked(getNewCodePeriod)
+ .mocked(getNewCodeDefinition)
.mockResolvedValue({ type: NewCodeDefinitionType.NumberOfDays, value: '60' });
const user = userEvent.setup();
renderCreateProject();
+++ /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 DocLink from '../../../components/common/DocLink';
-import Link from '../../../components/common/Link';
-import { translate } from '../../../helpers/l10n';
-
-export interface AppHeaderProps {
- canAdmin: boolean;
-}
-
-export default function AppHeader(props: AppHeaderProps) {
- const { canAdmin } = props;
-
- return (
- <header className="page-header">
- <h1 className="sw-mb-4">{translate('project_baseline.page')}</h1>
- <p className="sw-mb-2">{translate('project_baseline.page.description')}</p>
- <p className="sw-mb-2">{translate('settings.new_code_period.description1')}</p>
- <p className="sw-mb-2">
- {canAdmin && (
- <FormattedMessage
- defaultMessage={translate('project_baseline.page.description2')}
- id="project_baseline.page.description2"
- values={{
- link: (
- <Link to="/admin/settings?category=new_code_period">
- {translate('project_baseline.page.description2.link')}
- </Link>
- ),
- }}
- />
- )}
- </p>
-
- <p className="sw-mb-2">
- <FormattedMessage
- defaultMessage={translate('settings.new_code_period.description3')}
- id="settings.new_code_period.description3"
- values={{
- link: (
- <DocLink to="/project-administration/defining-new-code/">
- {translate('settings.new_code_period.description3.link')}
- </DocLink>
- ),
- }}
- />
- </p>
-
- <p className="sw-mt-4">
- <strong>{translate('project_baseline.page.question')}</strong>
- </p>
- </header>
- );
-}
+++ /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 { NewCodeDefinitionType } from '../../../types/new-code-definition';
-
-export interface Props {
- onSelect: (selection: NewCodeDefinitionType) => void;
- selected: boolean;
-}
-
-export default function BaselineSettingAnalysis({ onSelect, selected }: Props) {
- return (
- <RadioCard
- noRadio
- disabled
- onClick={() => onSelect(NewCodeDefinitionType.SpecificAnalysis)}
- selected={selected}
- title={translate('baseline.specific_analysis')}
- >
- <p className="big-spacer-bottom">{translate('baseline.specific_analysis.description')}</p>
- </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 { SelectionCard } from 'design-system';
-import * as React from 'react';
-import { components, OptionProps } from 'react-select';
-import Select from '../../../components/controls/Select';
-import Tooltip from '../../../components/controls/Tooltip';
-import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
-import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { NewCodeDefinitionType } from '../../../types/new-code-definition';
-
-export interface BaselineSettingReferenceBranchProps {
- branchList: BranchOption[];
- className?: string;
- disabled?: boolean;
- onChangeReferenceBranch: (value: string) => void;
- onSelect: (selection: NewCodeDefinitionType) => void;
- referenceBranch: string;
- selected: boolean;
- settingLevel: 'project' | 'branch';
-}
-
-export interface BranchOption {
- isDisabled?: boolean;
- isInvalid?: boolean;
- isMain: boolean;
- label: string;
- value: string;
-}
-
-function renderBranchOption(props: OptionProps<BranchOption, false>) {
- const { data: option } = props;
-
- return (
- <components.Option {...props}>
- {option.isInvalid ? (
- <Tooltip
- overlay={translateWithParameters(
- 'baseline.reference_branch.does_not_exist',
- option.value
- )}
- >
- <span>
- {option.value} <AlertErrorIcon />
- </span>
- </Tooltip>
- ) : (
- <>
- <span
- title={
- option.isDisabled
- ? translate('baseline.reference_branch.cannot_be_itself')
- : undefined
- }
- >
- {option.value}
- </span>
- {option.isMain && (
- <div className="badge spacer-left">{translate('branches.main_branch')}</div>
- )}
- </>
- )}
- </components.Option>
- );
-}
-
-export default function BaselineSettingReferenceBranch(props: BaselineSettingReferenceBranchProps) {
- const { branchList, className, disabled, referenceBranch, selected, settingLevel } = props;
-
- const currentBranch = branchList.find((b) => b.value === referenceBranch) || {
- label: referenceBranch,
- value: referenceBranch,
- isMain: false,
- isInvalid: true,
- };
-
- return (
- <SelectionCard
- className={className}
- disabled={disabled}
- onClick={() => props.onSelect(NewCodeDefinitionType.ReferenceBranch)}
- selected={selected}
- title={translate('baseline.reference_branch')}
- >
- <>
- <div>
- <p className="sw-mb-3">{translate('baseline.reference_branch.description')}</p>
- <p className="sw-mb-4">{translate('baseline.reference_branch.usecase')}</p>
- </div>
- {selected && (
- <>
- {settingLevel === 'project' && (
- <p className="spacer-top">{translate('baseline.reference_branch.description2')}</p>
- )}
- <div className="big-spacer-top display-flex-column">
- <MandatoryFieldsExplanation className="spacer-bottom" />
- <label className="text-middle" htmlFor="reference_branch">
- <strong>{translate('baseline.reference_branch.choose')}</strong>
- <MandatoryFieldMarker />
- </label>
- <Select
- className="little-spacer-top spacer-bottom"
- options={branchList}
- aria-label={translate('baseline.reference_branch.choose')}
- onChange={(option: BranchOption) => props.onChangeReferenceBranch(option.value)}
- value={currentBranch}
- components={{
- Option: renderBranchOption,
- }}
- />
- </div>
- </>
- )}
- </>
- </SelectionCard>
- );
-}
+++ /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 { subDays } from 'date-fns';
-import { throttle } from 'lodash';
-import * as React from 'react';
-import { getProjectActivity } from '../../../api/projectActivity';
-import { parseDate, toShortISO8601String } from '../../../helpers/dates';
-import { Analysis, ParsedAnalysis } from '../../../types/project-activity';
-import { Dict } from '../../../types/types';
-import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';
-
-interface Props {
- analysis: string;
- branch: string;
- component: string;
- onSelectAnalysis: (analysis: ParsedAnalysis) => void;
-}
-
-interface State {
- analyses: ParsedAnalysis[];
- loading: boolean;
- range: number;
- scroll: number;
-}
-
-const STICKY_BADGE_SCROLL_OFFSET = 10;
-
-export default class BranchAnalysisList extends React.PureComponent<Props, State> {
- mounted = false;
- badges: Dict<HTMLDivElement> = {};
- state: State = {
- analyses: [],
- loading: true,
- range: 30,
- scroll: 0,
- };
-
- constructor(props: Props) {
- super(props);
- this.updateScroll = throttle(this.updateScroll, 20);
- }
-
- componentDidMount() {
- this.mounted = true;
- this.fetchAnalyses(true);
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- scrollToSelected() {
- document.querySelector('.branch-analysis.selected')?.scrollIntoView({
- block: 'center',
- behavior: 'smooth',
- });
- }
-
- fetchAnalyses(initial = false) {
- const { analysis, branch, component } = this.props;
- const { range } = this.state;
- this.setState({ loading: true });
-
- return getProjectActivity({
- branch,
- project: component,
- from: range ? toShortISO8601String(subDays(new Date(), range)) : undefined,
- }).then((result: { analyses: Analysis[] }) => {
- // If the selected analysis wasn't found in the default 30 days range, redo the search
- if (initial && analysis && !result.analyses.find((a) => a.key === analysis)) {
- this.handleRangeChange({ value: 0 });
- return;
- }
-
- this.setState(
- {
- analyses: result.analyses.map((analysis) => ({
- ...analysis,
- date: parseDate(analysis.date),
- })) as ParsedAnalysis[],
- loading: false,
- },
- () => {
- this.scrollToSelected();
- }
- );
- });
- }
-
- handleScroll = (e: React.SyntheticEvent<HTMLDivElement>) => {
- if (e.currentTarget) {
- this.updateScroll(e.currentTarget.scrollTop);
- }
- };
-
- updateScroll = (scroll: number) => {
- this.setState({ scroll });
- };
-
- registerBadgeNode = (version: string) => (el: HTMLDivElement) => {
- if (el) {
- if (!el.getAttribute('originOffsetTop')) {
- el.setAttribute('originOffsetTop', String(el.offsetTop));
- }
- this.badges[version] = el;
- }
- };
-
- shouldStick = (version: string) => {
- const badge = this.badges[version];
- return (
- !!badge &&
- Number(badge.getAttribute('originOffsetTop')) < this.state.scroll + STICKY_BADGE_SCROLL_OFFSET
- );
- };
-
- handleRangeChange = ({ value }: { value: number }) => {
- this.setState({ range: value }, () => {
- this.fetchAnalyses().catch(() => {
- /* noop */
- });
- });
- };
-
- render() {
- const { analysis, onSelectAnalysis } = this.props;
- const { analyses, loading, range } = this.state;
-
- return (
- <BranchAnalysisListRenderer
- analyses={analyses}
- handleRangeChange={this.handleRangeChange}
- handleScroll={this.handleScroll}
- loading={loading}
- onSelectAnalysis={onSelectAnalysis}
- range={range}
- registerBadgeNode={this.registerBadgeNode}
- selectedAnalysisKey={analysis}
- shouldStick={this.shouldStick}
- />
- );
- }
-}
+++ /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 classNames from 'classnames';
-import * as React from 'react';
-import { injectIntl, IntlShape, WrappedComponentProps } from 'react-intl';
-import Radio from '../../../components/controls/Radio';
-import Select from '../../../components/controls/Select';
-import Tooltip from '../../../components/controls/Tooltip';
-import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter';
-import TimeFormatter from '../../../components/intl/TimeFormatter';
-import Spinner from '../../../components/ui/Spinner';
-import { parseDate, toShortISO8601String } from '../../../helpers/dates';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { ParsedAnalysis } from '../../../types/project-activity';
-import Events from '../../projectActivity/components/Events';
-import { getAnalysesByVersionByDay } from '../../projectActivity/utils';
-
-export interface BranchAnalysisListRendererProps {
- analyses: ParsedAnalysis[];
- handleRangeChange: ({ value }: { value: number }) => void;
- handleScroll: (e: React.SyntheticEvent<HTMLDivElement>) => void;
- loading: boolean;
- onSelectAnalysis: (analysis: ParsedAnalysis) => void;
- range: number;
- registerBadgeNode: (version: string) => (el: HTMLDivElement) => void;
- selectedAnalysisKey: string;
- shouldStick: (version: string) => boolean;
-}
-
-function renderAnalysis(args: {
- analysis: ParsedAnalysis;
- isFirst: boolean;
- onSelectAnalysis: (analysis: ParsedAnalysis) => void;
- selectedAnalysisKey: string;
- intl: IntlShape;
-}) {
- const { analysis, isFirst, selectedAnalysisKey, intl } = args;
- return (
- <li
- className={classNames('branch-analysis', {
- selected: analysis.key === selectedAnalysisKey,
- })}
- data-date={parseDate(analysis.date).valueOf()}
- key={analysis.key}
- >
- <div className="branch-analysis-time spacer-right">
- <TimeFormatter date={parseDate(analysis.date)} long={false}>
- {(formattedTime) => (
- <time className="text-middle" dateTime={parseDate(analysis.date).toISOString()}>
- {formattedTime}
- </time>
- )}
- </TimeFormatter>
- </div>
-
- {analysis.events.length > 0 && (
- <Events analysisKey={analysis.key} events={analysis.events} isFirst={isFirst} />
- )}
-
- <div className="analysis-selection-button">
- <Radio
- checked={analysis.key === selectedAnalysisKey}
- ariaLabel={translateWithParameters(
- 'baseline.branch_analyses.analysis_for_x',
- `${intl.formatDate(analysis.date, longFormatterOption)}, ${intl.formatTime(
- analysis.date
- )}`
- )}
- onCheck={() => {}}
- value=""
- disabled
- />
- </div>
- </li>
- );
-}
-
-function BranchAnalysisListRenderer(
- props: BranchAnalysisListRendererProps & WrappedComponentProps
-) {
- const { analyses, loading, range, selectedAnalysisKey, intl } = props;
-
- const byVersionByDay = React.useMemo(
- () =>
- getAnalysesByVersionByDay(analyses, {
- category: '',
- }),
- [analyses]
- );
-
- const hasFilteredData =
- byVersionByDay.length > 1 ||
- (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
-
- const options = [
- {
- label: translate('baseline.branch_analyses.ranges.30days'),
- value: 30,
- },
- {
- label: translate('baseline.branch_analyses.ranges.allTime'),
- value: 0,
- },
- ];
-
- return (
- <>
- <div className="spacer-bottom">
- <label htmlFor="branch-analysis-from-input" className="spacer-right">
- {translate('baseline.analysis_from')}
- </label>
- <Select
- blurInputOnSelect
- inputId="branch-analysis-from-input"
- className="input-medium spacer-left"
- onChange={props.handleRangeChange}
- options={options}
- isSearchable={false}
- value={options.filter((o) => o.value === range)}
- />
- </div>
- <div className="branch-analysis-list-wrapper">
- <div className="bordered branch-analysis-list" onScroll={props.handleScroll}>
- <Spinner className="big-spacer-top" loading={loading} />
-
- {!loading && !hasFilteredData ? (
- <div className="big-spacer-top big-spacer-bottom strong">
- {translate('baseline.no_analyses')}
- </div>
- ) : (
- <ul>
- {byVersionByDay.map((version, idx) => {
- const days = Object.keys(version.byDay);
- if (days.length <= 0) {
- return null;
- }
- return (
- <li key={version.key || 'noversion'}>
- {version.version && (
- <div
- className={classNames('branch-analysis-version-badge', {
- first: idx === 0,
- sticky: props.shouldStick(version.version),
- })}
- ref={props.registerBadgeNode(version.version)}
- >
- <Tooltip
- mouseEnterDelay={0.5}
- overlay={`${translate('version')} ${version.version}`}
- >
- <span className="badge">{version.version}</span>
- </Tooltip>
- </div>
- )}
- <ul className="branch-analysis-days-list">
- {days.map((day) => (
- <li
- className="branch-analysis-day"
- data-day={toShortISO8601String(Number(day))}
- key={day}
- >
- <div className="branch-analysis-date">
- <DateFormatter date={Number(day)} long />
- </div>
- <ul className="branch-analysis-analyses-list">
- {version.byDay[day]?.map((analysis) =>
- renderAnalysis({
- analysis,
- selectedAnalysisKey,
- isFirst: analyses[0].key === analysis.key,
- onSelectAnalysis: props.onSelectAnalysis,
- intl,
- })
- )}
- </ul>
- </li>
- ))}
- </ul>
- </li>
- );
- })}
- </ul>
- )}
- </div>
- </div>
- </>
- );
-}
-
-export default injectIntl(BranchAnalysisListRenderer);
+++ /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 { setNewCodePeriod } from '../../../api/newCodePeriod';
-import Modal from '../../../components/controls/Modal';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-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 Spinner from '../../../components/ui/Spinner';
-import { toISO8601WithOffsetString } from '../../../helpers/dates';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { getNumberOfDaysDefaultValue } from '../../../helpers/new-code-definition';
-import { Branch, BranchWithNewCodePeriod } from '../../../types/branch-like';
-import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
-import { getSettingValue, validateSetting } from '../utils';
-import BaselineSettingAnalysis from './BaselineSettingAnalysis';
-import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
-import BranchAnalysisList from './BranchAnalysisList';
-
-interface Props {
- branch: BranchWithNewCodePeriod;
- branchList: Branch[];
- component: string;
- onClose: (branch?: string, newSetting?: NewCodeDefinition) => void;
- inheritedSetting: NewCodeDefinition;
- generalSetting: NewCodeDefinition;
-}
-
-interface State {
- analysis: string;
- analysisDate?: Date;
- days: string;
- isChanged: boolean;
- referenceBranch: string;
- saving: boolean;
- selected?: NewCodeDefinitionType;
-}
-
-export default class BranchBaselineSettingModal extends React.PureComponent<Props, State> {
- mounted = false;
-
- constructor(props: Props) {
- super(props);
-
- const { branch, branchList, inheritedSetting, generalSetting } = props;
- const otherBranches = branchList.filter((b) => b.name !== branch.name);
- const defaultBranch = otherBranches.length > 0 ? otherBranches[0].name : '';
-
- this.state = {
- analysis: this.getValueFromProps(NewCodeDefinitionType.SpecificAnalysis) || '',
- days:
- this.getValueFromProps(NewCodeDefinitionType.NumberOfDays) ||
- getNumberOfDaysDefaultValue(generalSetting, inheritedSetting),
- isChanged: false,
- referenceBranch:
- this.getValueFromProps(NewCodeDefinitionType.ReferenceBranch) || defaultBranch,
- saving: false,
- selected: branch.newCodePeriod?.type,
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- getValueFromProps(type: NewCodeDefinitionType) {
- return this.props.branch.newCodePeriod && this.props.branch.newCodePeriod.type === type
- ? this.props.branch.newCodePeriod.value
- : null;
- }
-
- branchToOption = (b: Branch) => ({
- label: b.name,
- value: b.name,
- isMain: b.isMain,
- isDisabled: b.name === this.props.branch.name, // cannot itself be used as a reference branch
- });
-
- handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
- e.preventDefault();
-
- const { branch, component } = this.props;
- const { analysis, analysisDate, days, referenceBranch, selected: type } = this.state;
-
- const value = getSettingValue({ type, analysis, days, referenceBranch });
-
- if (type) {
- this.setState({ saving: true });
- setNewCodePeriod({
- project: component,
- type,
- value,
- branch: branch.name,
- }).then(
- () => {
- if (this.mounted) {
- this.setState({
- saving: false,
- isChanged: false,
- });
- this.props.onClose(branch.name, {
- type,
- value,
- effectiveValue: analysisDate && toISO8601WithOffsetString(analysisDate),
- });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({
- saving: false,
- });
- }
- }
- );
- }
- };
-
- requestClose = () => this.props.onClose();
-
- handleSelectDays = (days: string) => this.setState({ days, isChanged: true });
-
- handleSelectReferenceBranch = (referenceBranch: string) =>
- this.setState({ referenceBranch, isChanged: true });
-
- handleSelectSetting = (selected: NewCodeDefinitionType) => {
- this.setState((currentState) => ({ selected, isChanged: selected !== currentState.selected }));
- };
-
- render() {
- const { branch, branchList } = this.props;
- const { analysis, days, isChanged, referenceBranch, saving, selected } = this.state;
-
- const header = translateWithParameters('baseline.new_code_period_for_branch_x', branch.name);
-
- const currentSetting = branch.newCodePeriod?.type;
- const currentSettingValue = branch.newCodePeriod?.value;
-
- const isValid = validateSetting({
- days,
- referenceBranch,
- selected,
- });
-
- return (
- <Modal contentLabel={header} onRequestClose={this.requestClose} size="large">
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
- <form onSubmit={this.handleSubmit}>
- <div className="modal-body modal-container branch-baseline-setting-modal">
- <p className="sw-mb-3">{translate('baseline.new_code_period_for_branch_x.question')}</p>
- <NewCodeDefinitionWarning
- newCodeDefinitionType={currentSetting}
- newCodeDefinitionValue={currentSettingValue}
- isBranchSupportEnabled
- level="branch"
- />
- <div className="display-flex-column huge-spacer-bottom sw-gap-4" role="radiogroup">
- <NewCodeDefinitionPreviousVersionOption
- isDefault={false}
- onSelect={this.handleSelectSetting}
- selected={selected === NewCodeDefinitionType.PreviousVersion}
- />
- <NewCodeDefinitionDaysOption
- days={days}
- isChanged={isChanged}
- isValid={isValid}
- onChangeDays={this.handleSelectDays}
- onSelect={this.handleSelectSetting}
- selected={selected === NewCodeDefinitionType.NumberOfDays}
- />
- <BaselineSettingReferenceBranch
- branchList={branchList.map(this.branchToOption)}
- onChangeReferenceBranch={this.handleSelectReferenceBranch}
- onSelect={this.handleSelectSetting}
- referenceBranch={referenceBranch}
- selected={selected === NewCodeDefinitionType.ReferenceBranch}
- settingLevel="branch"
- />
- {currentSetting === NewCodeDefinitionType.SpecificAnalysis && (
- <BaselineSettingAnalysis
- onSelect={noop}
- selected={selected === NewCodeDefinitionType.SpecificAnalysis}
- />
- )}
- </div>
- {selected === NewCodeDefinitionType.SpecificAnalysis && (
- <BranchAnalysisList
- analysis={analysis}
- branch={branch.name}
- component={this.props.component}
- onSelectAnalysis={noop}
- />
- )}
- </div>
- <footer className="modal-foot">
- <Spinner className="spacer-right" loading={saving} />
- <SubmitButton disabled={!isChanged || saving || !isValid}>
- {translate('save')}
- </SubmitButton>
- <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
- </footer>
- </form>
- </Modal>
- );
- }
-}
+++ /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 { listBranchesNewCodePeriod, resetNewCodePeriod } from '../../../api/newCodePeriod';
-import Spinner from '../../../components/ui/Spinner';
-import { isBranch, sortBranches } from '../../../helpers/branch-like';
-import { translate } from '../../../helpers/l10n';
-import { DEFAULT_NEW_CODE_DEFINITION_TYPE } from '../../../helpers/new-code-definition';
-import { Branch, BranchLike, BranchWithNewCodePeriod } from '../../../types/branch-like';
-import { NewCodeDefinition } from '../../../types/new-code-definition';
-import { Component } from '../../../types/types';
-import BranchBaselineSettingModal from './BranchBaselineSettingModal';
-import BranchListRow from './BranchListRow';
-
-interface Props {
- branchList: Branch[];
- component: Component;
- inheritedSetting: NewCodeDefinition;
- generalSetting: NewCodeDefinition;
-}
-
-interface State {
- branches: BranchWithNewCodePeriod[];
- editedBranch?: BranchWithNewCodePeriod;
- loading: boolean;
-}
-
-export default class BranchList extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = {
- branches: [],
- loading: true,
- };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchBranches();
- }
-
- componentDidUpdate(prevProps: Props) {
- if (prevProps.branchList !== this.props.branchList) {
- this.fetchBranches();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- sortAndFilterBranches(branchLikes: BranchLike[] = []) {
- return sortBranches(branchLikes.filter(isBranch));
- }
-
- fetchBranches() {
- const project = this.props.component.key;
- this.setState({ loading: true });
-
- listBranchesNewCodePeriod({ project }).then(
- (branchSettings) => {
- const newCodePeriods = branchSettings.newCodePeriods
- ? branchSettings.newCodePeriods.filter((ncp) => !ncp.inherited)
- : [];
-
- const branchesWithBaseline = this.props.branchList.map((b) => {
- const newCodePeriod = newCodePeriods.find((ncp) => ncp.branchKey === b.name);
- if (!newCodePeriod) {
- return b;
- }
- const { type = DEFAULT_NEW_CODE_DEFINITION_TYPE, value, effectiveValue } = newCodePeriod;
- return {
- ...b,
- newCodePeriod: { type, value, effectiveValue },
- };
- });
-
- this.setState({ branches: branchesWithBaseline, loading: false });
- },
- () => {
- this.setState({ loading: false });
- }
- );
- }
-
- updateBranchNewCodePeriod = (branch: string, newSetting: NewCodeDefinition | undefined) => {
- const { branches } = this.state;
-
- const updated = branches.find((b) => b.name === branch);
- if (updated) {
- updated.newCodePeriod = newSetting;
- }
- return branches.slice(0);
- };
-
- openEditModal = (branch: BranchWithNewCodePeriod) => {
- this.setState({ editedBranch: branch });
- };
-
- closeEditModal = (branch?: string, newSetting?: NewCodeDefinition) => {
- if (branch) {
- this.setState({
- branches: this.updateBranchNewCodePeriod(branch, newSetting),
- editedBranch: undefined,
- });
- } else {
- this.setState({ editedBranch: undefined });
- }
- };
-
- resetToDefault = (branch: string) => {
- return resetNewCodePeriod({
- project: this.props.component.key,
- branch,
- }).then(() => {
- this.setState({ branches: this.updateBranchNewCodePeriod(branch, undefined) });
- });
- };
-
- render() {
- const { branchList, inheritedSetting, generalSetting } = this.props;
- const { branches, editedBranch, loading } = this.state;
-
- if (branches.length < 1) {
- return null;
- }
-
- if (loading) {
- return <Spinner />;
- }
-
- return (
- <>
- <table className="data zebra">
- <thead>
- <tr>
- <th>{translate('branch_list.branch')}</th>
- <th className="nowrap huge-spacer-right">
- {translate('branch_list.current_setting')}
- </th>
- <th className="thin nowrap">{translate('branch_list.actions')}</th>
- </tr>
- </thead>
- <tbody>
- {branches.map((branch) => (
- <BranchListRow
- branch={branch}
- existingBranches={branchList.map((b) => b.name)}
- inheritedSetting={inheritedSetting}
- key={branch.name}
- onOpenEditModal={this.openEditModal}
- onResetToDefault={this.resetToDefault}
- />
- ))}
- </tbody>
- </table>
- {editedBranch && (
- <BranchBaselineSettingModal
- branch={editedBranch}
- branchList={branchList}
- component={this.props.component.key}
- onClose={this.closeEditModal}
- inheritedSetting={inheritedSetting}
- generalSetting={generalSetting}
- />
- )}
- </>
- );
- }
-}
+++ /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 ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
-import Tooltip from '../../../components/controls/Tooltip';
-import BranchLikeIcon from '../../../components/icons/BranchLikeIcon';
-import WarningIcon from '../../../components/icons/WarningIcon';
-import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { isNewCodeDefinitionCompliant } from '../../../helpers/new-code-definition';
-import { BranchWithNewCodePeriod } from '../../../types/branch-like';
-import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
-
-export interface BranchListRowProps {
- branch: BranchWithNewCodePeriod;
- existingBranches: Array<string>;
- inheritedSetting: NewCodeDefinition;
- onOpenEditModal: (branch: BranchWithNewCodePeriod) => void;
- onResetToDefault: (branchName: string) => void;
-}
-
-function renderNewCodePeriodSetting(newCodePeriod: NewCodeDefinition) {
- switch (newCodePeriod.type) {
- case NewCodeDefinitionType.SpecificAnalysis:
- return (
- <>
- {`${translate('baseline.specific_analysis')}: `}
- {newCodePeriod.effectiveValue ? (
- <DateTimeFormatter date={newCodePeriod.effectiveValue} />
- ) : (
- '?'
- )}
- </>
- );
- case NewCodeDefinitionType.NumberOfDays:
- return `${translate('new_code_definition.number_days')}: ${newCodePeriod.value}`;
- case NewCodeDefinitionType.PreviousVersion:
- return translate('new_code_definition.previous_version');
- case NewCodeDefinitionType.ReferenceBranch:
- return `${translate('baseline.reference_branch')}: ${newCodePeriod.value}`;
- default:
- return newCodePeriod.type;
- }
-}
-
-function branchInheritsItselfAsReference(
- branch: BranchWithNewCodePeriod,
- inheritedSetting: NewCodeDefinition
-) {
- return (
- !branch.newCodePeriod &&
- inheritedSetting.type === NewCodeDefinitionType.ReferenceBranch &&
- branch.name === inheritedSetting.value
- );
-}
-
-function referenceBranchDoesNotExist(
- branch: BranchWithNewCodePeriod,
- existingBranches: Array<string>
-) {
- return (
- branch.newCodePeriod &&
- branch.newCodePeriod.value &&
- branch.newCodePeriod.type === NewCodeDefinitionType.ReferenceBranch &&
- !existingBranches.includes(branch.newCodePeriod.value)
- );
-}
-
-export default function BranchListRow(props: BranchListRowProps) {
- const { branch, existingBranches, inheritedSetting } = props;
-
- let settingWarning: string | undefined;
- if (branchInheritsItselfAsReference(branch, inheritedSetting)) {
- settingWarning = translateWithParameters(
- 'baseline.reference_branch.invalid_branch_setting',
- branch.name
- );
- } else if (referenceBranchDoesNotExist(branch, existingBranches)) {
- settingWarning = translateWithParameters(
- 'baseline.reference_branch.does_not_exist',
- branch.newCodePeriod?.value || ''
- );
- }
-
- const isCompliant = isNewCodeDefinitionCompliant(inheritedSetting);
-
- return (
- <tr className={settingWarning ? 'branch-setting-warning' : ''}>
- <td className="nowrap">
- <BranchLikeIcon branchLike={branch} className="little-spacer-right" />
- {branch.name}
- {branch.isMain && (
- <div className="badge spacer-left">{translate('branches.main_branch')}</div>
- )}
- </td>
- <td className="huge-spacer-right nowrap">
- <Tooltip overlay={settingWarning}>
- <span>
- {settingWarning && <WarningIcon className="little-spacer-right" />}
- {branch.newCodePeriod
- ? renderNewCodePeriodSetting(branch.newCodePeriod)
- : translate('branch_list.default_setting')}
- </span>
- </Tooltip>
- </td>
- <td className="text-right">
- <ActionsDropdown
- label={translateWithParameters('branch_list.show_actions_for_x', branch.name)}
- >
- <ActionsDropdownItem onClick={() => props.onOpenEditModal(branch)}>
- {translate('edit')}
- </ActionsDropdownItem>
- {branch.newCodePeriod && (
- <ActionsDropdownItem
- disabled={!isCompliant}
- onClick={() => props.onResetToDefault(branch.name)}
- tooltipOverlay={
- isCompliant ? null : translate('project_baseline.compliance.warning.title.project')
- }
- >
- {translate('reset_to_default')}
- </ActionsDropdownItem>
- )}
- </ActionsDropdown>
- </td>
- </tr>
- );
-}
+++ /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 classNames from 'classnames';
-import { debounce } from 'lodash';
-import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
-import { getNewCodePeriod, resetNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod';
-import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
-import withAvailableFeatures, {
- WithAvailableFeaturesProps,
-} from '../../../app/components/available-features/withAvailableFeatures';
-import withComponentContext from '../../../app/components/componentContext/withComponentContext';
-import Suggestions from '../../../components/embed-docs-modal/Suggestions';
-import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
-import Spinner from '../../../components/ui/Spinner';
-import { isBranch, sortBranches } from '../../../helpers/branch-like';
-import { translate } from '../../../helpers/l10n';
-import {
- DEFAULT_NEW_CODE_DEFINITION_TYPE,
- getNumberOfDaysDefaultValue,
-} from '../../../helpers/new-code-definition';
-import { withBranchLikes } from '../../../queries/branch';
-import { AppState } from '../../../types/appstate';
-import { Branch, BranchLike } from '../../../types/branch-like';
-import { Feature } from '../../../types/features';
-import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
-import { Component } from '../../../types/types';
-import '../styles.css';
-import { getSettingValue } from '../utils';
-import AppHeader from './AppHeader';
-import BranchList from './BranchList';
-import ProjectBaselineSelector from './ProjectBaselineSelector';
-
-interface Props extends WithAvailableFeaturesProps {
- branchLike: Branch;
- branchLikes: BranchLike[];
- component: Component;
- appState: AppState;
-}
-
-interface State {
- analysis?: string;
- branchList: Branch[];
- currentSetting?: NewCodeDefinitionType;
- currentSettingValue?: string;
- days: string;
- generalSetting?: NewCodeDefinition;
- isChanged: boolean;
- loading: boolean;
- overrideGeneralSetting?: boolean;
- referenceBranch?: string;
- saving: boolean;
- selected?: NewCodeDefinitionType;
- success?: boolean;
-}
-
-class ProjectBaselineApp extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = {
- branchList: [],
- days: getNumberOfDaysDefaultValue(),
- isChanged: false,
- loading: true,
- saving: false,
- };
-
- // We use debounce as we could have multiple save in less that 3sec.
- resetSuccess = debounce(() => this.setState({ success: undefined }), 3000);
-
- componentDidMount() {
- this.mounted = true;
- this.fetchLeakPeriodSetting();
- this.sortAndFilterBranches(this.props.branchLikes);
- }
-
- componentDidUpdate(prevProps: Props) {
- if (prevProps.branchLikes !== this.props.branchLikes) {
- this.sortAndFilterBranches(this.props.branchLikes);
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- getUpdatedState(params: {
- currentSetting?: NewCodeDefinitionType;
- currentSettingValue?: string;
- generalSetting: NewCodeDefinition;
- }) {
- const { currentSetting, currentSettingValue, generalSetting } = params;
- const { referenceBranch } = this.state;
-
- const defaultDays = getNumberOfDaysDefaultValue(generalSetting);
-
- return {
- loading: false,
- currentSetting,
- currentSettingValue,
- generalSetting,
- isChanged: false,
- selected: currentSetting || generalSetting.type,
- overrideGeneralSetting: Boolean(currentSetting),
- days:
- (currentSetting === NewCodeDefinitionType.NumberOfDays && currentSettingValue) ||
- defaultDays,
- analysis:
- (currentSetting === NewCodeDefinitionType.SpecificAnalysis && currentSettingValue) || '',
- referenceBranch:
- (currentSetting === NewCodeDefinitionType.ReferenceBranch && currentSettingValue) ||
- referenceBranch,
- };
- }
-
- sortAndFilterBranches(branchLikes: BranchLike[] = []) {
- const branchList = sortBranches(branchLikes.filter(isBranch));
- this.setState({ branchList, referenceBranch: branchList[0]?.name });
- }
-
- fetchLeakPeriodSetting() {
- const { branchLike, component } = this.props;
-
- this.setState({ loading: true });
-
- Promise.all([
- getNewCodePeriod(),
- getNewCodePeriod({
- branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name,
- project: component.key,
- }),
- ]).then(
- ([generalSetting, setting]) => {
- if (this.mounted) {
- if (!generalSetting.type) {
- generalSetting = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE };
- }
- const currentSettingValue = setting.value;
- const currentSetting = setting.inherited
- ? undefined
- : setting.type || DEFAULT_NEW_CODE_DEFINITION_TYPE;
-
- this.setState(
- this.getUpdatedState({
- generalSetting,
- currentSetting,
- currentSettingValue,
- })
- );
- }
- },
- () => {
- this.setState({ loading: false });
- }
- );
- }
-
- resetSetting = () => {
- this.setState({ saving: true });
- resetNewCodePeriod({ project: this.props.component.key }).then(
- () => {
- this.setState({
- saving: false,
- currentSetting: undefined,
- isChanged: false,
- selected: undefined,
- success: true,
- });
- this.resetSuccess();
- },
- () => {
- this.setState({ saving: false });
- }
- );
- };
-
- handleSelectDays = (days: string) => this.setState({ days, isChanged: true });
-
- handleSelectReferenceBranch = (referenceBranch: string) => {
- this.setState({ referenceBranch, isChanged: true });
- };
-
- handleCancel = () =>
- this.setState(
- ({
- generalSetting = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE },
- currentSetting,
- currentSettingValue,
- }) => this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue })
- );
-
- handleSelectSetting = (selected?: NewCodeDefinitionType) => {
- this.setState((currentState) => ({
- selected,
- isChanged: selected !== currentState.selected,
- }));
- };
-
- handleToggleSpecificSetting = (overrideGeneralSetting: boolean) =>
- this.setState((currentState) => ({
- overrideGeneralSetting,
- isChanged: currentState.overrideGeneralSetting !== overrideGeneralSetting,
- }));
-
- handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
- e.preventDefault();
-
- const { component } = this.props;
- const { days, selected: type, referenceBranch, overrideGeneralSetting } = this.state;
-
- if (!overrideGeneralSetting) {
- this.resetSetting();
- return;
- }
-
- const value = getSettingValue({ type, days, referenceBranch });
-
- if (type) {
- this.setState({ saving: true });
- setNewCodePeriod({
- project: component.key,
- type,
- value,
- }).then(
- () => {
- this.setState({
- saving: false,
- currentSetting: type,
- currentSettingValue: value || undefined,
- isChanged: false,
- success: true,
- });
- this.resetSuccess();
- },
- () => {
- this.setState({ saving: false });
- }
- );
- }
- };
-
- render() {
- const { appState, component, branchLike } = this.props;
- const {
- analysis,
- branchList,
- currentSetting,
- days,
- generalSetting,
- isChanged,
- loading,
- currentSettingValue,
- overrideGeneralSetting,
- referenceBranch,
- saving,
- selected,
- success,
- } = this.state;
- const branchSupportEnabled = this.props.hasFeature(Feature.BranchSupport);
-
- return (
- <>
- <Suggestions suggestions="project_baseline" />
- <Helmet defer={false} title={translate('project_baseline.page')} />
- <div className="page page-limited">
- <AppHeader canAdmin={!!appState.canAdmin} />
- <Spinner loading={loading} />
-
- {!loading && (
- <div className="panel-white project-baseline">
- {branchSupportEnabled && <h2>{translate('project_baseline.default_setting')}</h2>}
-
- {generalSetting && overrideGeneralSetting !== undefined && (
- <ProjectBaselineSelector
- analysis={analysis}
- branch={branchLike}
- branchList={branchList}
- branchesEnabled={branchSupportEnabled}
- canAdmin={appState.canAdmin}
- component={component.key}
- currentSetting={currentSetting}
- currentSettingValue={currentSettingValue}
- days={days}
- generalSetting={generalSetting}
- isChanged={isChanged}
- onCancel={this.handleCancel}
- onSelectDays={this.handleSelectDays}
- onSelectReferenceBranch={this.handleSelectReferenceBranch}
- onSelectSetting={this.handleSelectSetting}
- onSubmit={this.handleSubmit}
- onToggleSpecificSetting={this.handleToggleSpecificSetting}
- overrideGeneralSetting={overrideGeneralSetting}
- referenceBranch={referenceBranch}
- saving={saving}
- selected={selected}
- />
- )}
-
- <div className={classNames('spacer-top', { invisible: saving || !success })}>
- <span className="text-success">
- <AlertSuccessIcon className="spacer-right" />
- {translate('settings.state.saved')}
- </span>
- </div>
- {generalSetting && branchSupportEnabled && (
- <div className="huge-spacer-top branch-baseline-selector">
- <hr />
- <h2>{translate('project_baseline.configure_branches')}</h2>
- <BranchList
- branchList={branchList}
- component={component}
- inheritedSetting={
- currentSetting
- ? {
- type: currentSetting,
- value: currentSettingValue,
- }
- : generalSetting
- }
- generalSetting={generalSetting}
- />
- </div>
- )}
- </div>
- )}
- </div>
- </>
- );
- }
-}
-
-export default withComponentContext(
- withAvailableFeatures(withAppStateContext(withBranchLikes(ProjectBaselineApp)))
-);
+++ /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 classNames from 'classnames';
-import { RadioButton } from 'design-system';
-import { noop } from 'lodash';
-import * as React from 'react';
-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 Spinner from '../../../components/ui/Spinner';
-import { translate } from '../../../helpers/l10n';
-import { isNewCodeDefinitionCompliant } from '../../../helpers/new-code-definition';
-import { Branch } from '../../../types/branch-like';
-import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
-import { validateSetting } from '../utils';
-import BaselineSettingAnalysis from './BaselineSettingAnalysis';
-import BaselineSettingReferenceBranch from './BaselineSettingReferenceBranch';
-import BranchAnalysisList from './BranchAnalysisList';
-
-export interface ProjectBaselineSelectorProps {
- analysis?: string;
- branch?: Branch;
- branchList: Branch[];
- branchesEnabled?: boolean;
- canAdmin: boolean | undefined;
- component: string;
- currentSetting?: NewCodeDefinitionType;
- currentSettingValue?: string;
- days: string;
- generalSetting: NewCodeDefinition;
- isChanged: boolean;
- onCancel: () => void;
- onSelectDays: (value: string) => void;
- onSelectReferenceBranch: (value: string) => void;
- onSelectSetting: (value?: NewCodeDefinitionType) => void;
- onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void;
- onToggleSpecificSetting: (selection: boolean) => void;
- referenceBranch?: string;
- saving: boolean;
- selected?: NewCodeDefinitionType;
- overrideGeneralSetting: boolean;
-}
-
-function branchToOption(b: Branch) {
- return { label: b.name, value: b.name, isMain: b.isMain };
-}
-
-export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) {
- const {
- analysis,
- branch,
- branchList,
- branchesEnabled,
- canAdmin,
- component,
- currentSetting,
- currentSettingValue,
- days,
- generalSetting,
- isChanged,
- overrideGeneralSetting,
- referenceBranch,
- saving,
- selected,
- } = props;
-
- const isGlobalNcdCompliant = isNewCodeDefinitionCompliant(generalSetting);
-
- const isValid = validateSetting({
- days,
- overrideGeneralSetting,
- referenceBranch,
- selected,
- });
-
- if (branch === undefined) {
- return null;
- }
-
- return (
- <form className="project-baseline-selector" onSubmit={props.onSubmit}>
- <div className="big-spacer-top spacer-bottom" role="radiogroup">
- <RadioButton
- checked={!overrideGeneralSetting}
- className="big-spacer-bottom"
- disabled={!isGlobalNcdCompliant}
- onCheck={() => props.onToggleSpecificSetting(false)}
- value="general"
- >
- <Tooltip
- overlay={
- isGlobalNcdCompliant
- ? null
- : translate('project_baseline.compliance.warning.title.global')
- }
- >
- <span>{translate('project_baseline.global_setting')}</span>
- </Tooltip>
- </RadioButton>
-
- <div className="sw-ml-4">
- <GlobalNewCodeDefinitionDescription
- globalNcd={generalSetting}
- isGlobalNcdCompliant={isGlobalNcdCompliant}
- canAdmin={canAdmin}
- />
- </div>
-
- <RadioButton
- checked={overrideGeneralSetting}
- className="huge-spacer-top"
- onCheck={() => props.onToggleSpecificSetting(true)}
- value="specific"
- >
- {translate('project_baseline.specific_setting')}
- </RadioButton>
- </div>
-
- <div className="big-spacer-left big-spacer-right project-baseline-setting">
- <NewCodeDefinitionWarning
- newCodeDefinitionType={currentSetting}
- newCodeDefinitionValue={currentSettingValue}
- isBranchSupportEnabled={branchesEnabled}
- level="project"
- />
- <div className="display-flex-column big-spacer-bottom sw-gap-4" role="radiogroup">
- <NewCodeDefinitionPreviousVersionOption
- disabled={!overrideGeneralSetting}
- onSelect={props.onSelectSetting}
- selected={overrideGeneralSetting && selected === NewCodeDefinitionType.PreviousVersion}
- />
- <NewCodeDefinitionDaysOption
- days={days}
- disabled={!overrideGeneralSetting}
- isChanged={isChanged}
- isValid={isValid}
- onChangeDays={props.onSelectDays}
- onSelect={props.onSelectSetting}
- selected={overrideGeneralSetting && selected === NewCodeDefinitionType.NumberOfDays}
- />
- {branchesEnabled && (
- <BaselineSettingReferenceBranch
- branchList={branchList.map(branchToOption)}
- disabled={!overrideGeneralSetting}
- onChangeReferenceBranch={props.onSelectReferenceBranch}
- onSelect={props.onSelectSetting}
- referenceBranch={referenceBranch || ''}
- selected={
- overrideGeneralSetting && selected === NewCodeDefinitionType.ReferenceBranch
- }
- settingLevel="project"
- />
- )}
- {!branchesEnabled && currentSetting === NewCodeDefinitionType.SpecificAnalysis && (
- <BaselineSettingAnalysis
- onSelect={noop}
- selected={
- overrideGeneralSetting && selected === NewCodeDefinitionType.SpecificAnalysis
- }
- />
- )}
- </div>
- {!branchesEnabled &&
- overrideGeneralSetting &&
- selected === NewCodeDefinitionType.SpecificAnalysis && (
- <BranchAnalysisList
- analysis={analysis || ''}
- branch={branch.name}
- component={component}
- onSelectAnalysis={noop}
- />
- )}
- </div>
- <div className={classNames('big-spacer-top', { invisible: !isChanged })}>
- <Alert variant="info" className="spacer-bottom">
- {translate('baseline.next_analysis_notice')}
- </Alert>
- <Spinner className="spacer-right" loading={saving} />
- <SubmitButton disabled={saving || !isValid || !isChanged}>{translate('save')}</SubmitButton>
- <ResetButtonLink className="spacer-left" onClick={props.onCancel}>
- {translate('cancel')}
- </ResetButtonLink>
- </div>
- </form>
- );
-}
+++ /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 { within } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { first, last } from 'lodash';
-import selectEvent from 'react-select-event';
-import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
-import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import { mockNewCodePeriodBranch } from '../../../../helpers/mocks/new-code-definition';
-import { mockAppState } from '../../../../helpers/testMocks';
-import {
- RenderContext,
- renderAppWithComponentContext,
-} from '../../../../helpers/testReactTestingUtils';
-import { byRole, byText } from '../../../../helpers/testSelector';
-import { Feature } from '../../../../types/features';
-import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
-import routes from '../../routes';
-
-jest.mock('../../../../api/newCodePeriod');
-jest.mock('../../../../api/projectActivity');
-jest.mock('../../../../api/branches');
-
-const codePeriodsMock = new NewCodePeriodsServiceMock();
-const projectActivityMock = new ProjectActivityServiceMock();
-const branchHandler = new BranchesServiceMock();
-
-afterEach(() => {
- branchHandler.reset();
- codePeriodsMock.reset();
- projectActivityMock.reset();
-});
-
-it('renders correctly without branch support feature', async () => {
- const { ui } = getPageObjects();
- renderProjectBaselineApp();
- await ui.appIsLoaded();
-
- expect(await ui.generalSettingRadio.find()).toBeChecked();
- expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
-
- // User is not admin
- expect(ui.generalSettingsLink.query()).not.toBeInTheDocument();
-
- // Specific branch setting is not rendered without feature branch
- expect(ui.branchListHeading.query()).not.toBeInTheDocument();
- expect(ui.referenceBranchRadio.query()).not.toBeInTheDocument();
-});
-
-it('prevents selection of global setting if it is not compliant and warns non-admin about it', async () => {
- codePeriodsMock.setNewCodePeriod({
- type: NewCodeDefinitionType.NumberOfDays,
- value: '99',
- inherited: true,
- });
-
- const { ui } = getPageObjects();
- renderProjectBaselineApp();
- await ui.appIsLoaded();
-
- expect(await ui.generalSettingRadio.find()).toBeChecked();
- expect(ui.generalSettingRadio.get()).toBeDisabled();
- expect(ui.complianceWarning.get()).toBeVisible();
-});
-
-it('prevents selection of global setting if it is not compliant and warns admin about it', async () => {
- codePeriodsMock.setNewCodePeriod({
- type: NewCodeDefinitionType.NumberOfDays,
- value: '99',
- inherited: true,
- });
-
- const { ui } = getPageObjects();
- renderProjectBaselineApp({ appState: mockAppState({ canAdmin: true }) });
- await ui.appIsLoaded();
-
- expect(await ui.generalSettingRadio.find()).toBeChecked();
- expect(ui.generalSettingRadio.get()).toBeDisabled();
- expect(ui.complianceWarningAdmin.get()).toBeVisible();
- expect(ui.complianceWarning.query()).not.toBeInTheDocument();
-});
-
-it('renders correctly with branch support feature', async () => {
- const { ui } = getPageObjects();
- renderProjectBaselineApp({
- featureList: [Feature.BranchSupport],
- appState: mockAppState({ canAdmin: true }),
- });
- await ui.appIsLoaded();
-
- expect(await ui.generalSettingRadio.find()).toBeChecked();
- expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
-
- // User is admin
- expect(ui.generalSettingsLink.get()).toBeInTheDocument();
-
- // Specific branch setting is rendered with feature support branch
- expect(ui.branchListHeading.get()).toBeInTheDocument();
- expect(ui.referenceBranchRadio.get()).toBeInTheDocument();
-});
-
-it('can set previous version specific setting', async () => {
- const { ui, user } = getPageObjects();
- renderProjectBaselineApp();
- await ui.appIsLoaded();
-
- expect(await ui.previousVersionRadio.find()).toHaveClass('disabled');
- await ui.setPreviousVersionSetting();
- expect(ui.previousVersionRadio.get()).toBeChecked();
-
- // Save changes
- await user.click(ui.saveButton.get());
-
- expect(ui.saved.get()).toBeInTheDocument();
-
- // Set general setting
- await user.click(ui.generalSettingRadio.get());
- expect(ui.previousVersionRadio.get()).toHaveClass('disabled');
- await user.click(ui.saveButton.get());
- expect(ui.saved.get()).toBeInTheDocument();
-});
-
-it('can set number of days specific setting', async () => {
- const { ui, user } = getPageObjects();
- renderProjectBaselineApp();
- await ui.appIsLoaded();
-
- expect(await ui.numberDaysRadio.find()).toHaveClass('disabled');
- await ui.setNumberDaysSetting('10');
- expect(ui.numberDaysRadio.get()).toBeChecked();
-
- // Reset to initial state
- await user.click(ui.cancelButton.get());
- expect(ui.generalSettingRadio.get()).toBeChecked();
- expect(ui.numberDaysRadio.get()).toHaveClass('disabled');
-
- // Save changes
- await ui.setNumberDaysSetting('10');
- await user.click(ui.saveButton.get());
-
- expect(ui.saved.get()).toBeInTheDocument();
-});
-
-it('can set reference branch specific setting', async () => {
- const { ui, user } = getPageObjects();
- renderProjectBaselineApp({
- featureList: [Feature.BranchSupport],
- });
- await ui.appIsLoaded();
-
- expect(await ui.referenceBranchRadio.find()).toHaveClass('disabled');
- await ui.setReferenceBranchSetting('main');
- expect(ui.referenceBranchRadio.get()).toBeChecked();
-
- // Save changes
- await user.click(ui.saveButton.get());
-
- expect(ui.saved.get()).toBeInTheDocument();
-});
-
-it('cannot set specific analysis setting', async () => {
- const { ui } = getPageObjects();
- codePeriodsMock.setNewCodePeriod({
- type: NewCodeDefinitionType.SpecificAnalysis,
- value: 'analysis_id',
- });
- renderProjectBaselineApp();
- await ui.appIsLoaded();
-
- expect(await ui.specificAnalysisRadio.find()).toBeChecked();
- expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
- expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
-
- await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
-
- expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
- expect(ui.saveButton.get()).toBeDisabled();
-});
-
-it('renders correctly branch modal', async () => {
- const { ui } = getPageObjects();
- renderProjectBaselineApp({
- featureList: [Feature.BranchSupport],
- });
- await ui.appIsLoaded();
-
- await ui.openBranchSettingModal('main');
-
- expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
-});
-
-it('can set a previous version setting for branch', async () => {
- const { ui, user } = getPageObjects();
- renderProjectBaselineApp({
- featureList: [Feature.BranchSupport],
- });
- await ui.appIsLoaded();
- await ui.setBranchPreviousVersionSetting('main');
-
- expect(
- within(byRole('table').get()).getByText('new_code_definition.previous_version')
- ).toBeInTheDocument();
-
- await user.click(await ui.branchActionsButton('main').find());
-
- expect(ui.resetToDefaultButton.get()).toBeInTheDocument();
- await user.click(ui.resetToDefaultButton.get());
-
- expect(
- first(within(byRole('table').get()).getAllByText('branch_list.default_setting'))
- ).toBeInTheDocument();
-});
-
-it('can set a number of days setting for branch', async () => {
- const { ui } = getPageObjects();
- renderProjectBaselineApp({
- featureList: [Feature.BranchSupport],
- });
- await ui.appIsLoaded();
-
- await ui.setBranchNumberOfDaysSetting('main', '15');
-
- expect(
- within(byRole('table').get()).getByText('new_code_definition.number_days: 15')
- ).toBeInTheDocument();
-});
-
-it('cannot set a specific analysis setting for branch', async () => {
- const { ui } = getPageObjects();
- codePeriodsMock.setListBranchesNewCode([
- mockNewCodePeriodBranch({
- branchKey: 'main',
- type: NewCodeDefinitionType.SpecificAnalysis,
- value: 'analysis_id',
- }),
- ]);
- renderProjectBaselineApp({
- featureList: [Feature.BranchSupport],
- });
- await ui.appIsLoaded();
-
- await ui.openBranchSettingModal('main');
-
- expect(ui.specificAnalysisRadio.get()).toBeChecked();
- expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
- expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
-
- await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
-
- expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
- expect(last(ui.saveButton.getAll())).toBeDisabled();
-});
-
-it('can set a reference branch setting for branch', async () => {
- const { ui } = getPageObjects();
- renderProjectBaselineApp({
- featureList: [Feature.BranchSupport],
- });
- await ui.appIsLoaded();
-
- await ui.setBranchReferenceToBranchSetting('main', 'normal-branch');
-
- expect(
- byRole('table').byText('baseline.reference_branch: normal-branch').get()
- ).toBeInTheDocument();
-});
-
-function renderProjectBaselineApp(context: RenderContext = {}, params?: string) {
- return renderAppWithComponentContext(
- 'baseline',
- routes,
- {
- ...context,
- navigateTo: params ? `baseline?id=my-project&${params}` : 'baseline?id=my-project',
- },
- {
- component: mockComponent(),
- }
- );
-}
-
-function getPageObjects() {
- const user = userEvent.setup();
-
- const ui = {
- pageHeading: byRole('heading', { name: 'project_baseline.page' }),
- branchTableHeading: byText('branch_list.branch'),
- branchListHeading: byRole('heading', { name: 'project_baseline.default_setting' }),
- 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: /new_code_definition.previous_version.description/,
- }),
- numberDaysRadio: byRole('radio', { name: /new_code_definition.number_days.description/ }),
- numberDaysInput: byRole('spinbutton'),
- referenceBranchRadio: byRole('radio', { name: /baseline.reference_branch.description/ }),
- chooseBranchSelect: byRole('combobox', { name: 'baseline.reference_branch.choose' }),
- specificAnalysisRadio: byRole('radio', { name: /baseline.specific_analysis.description/ }),
- specificAnalysisWarning: byText('baseline.specific_analysis.compliance_warning.title'),
- analysisFromSelect: byRole('combobox', { name: 'baseline.analysis_from' }),
- analysisListItem: byRole('radio', { name: /baseline.branch_analyses.analysis_for_x/ }),
- saveButton: byRole('button', { name: 'save' }),
- cancelButton: byRole('button', { name: 'cancel' }),
- branchActionsButton: (branch: string) =>
- byRole('button', { name: `branch_list.show_actions_for_x.${branch}` }),
- editButton: byRole('button', { name: 'edit' }),
- resetToDefaultButton: byRole('button', { name: 'reset_to_default' }),
- saved: byText('settings.state.saved'),
- complianceWarningAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
- complianceWarning: byText('new_code_definition.compliance.warning.explanation'),
- };
-
- async function appIsLoaded() {
- expect(await ui.pageHeading.find()).toBeInTheDocument();
- }
-
- async function setPreviousVersionSetting() {
- await user.click(ui.specificSettingRadio.get());
- await user.click(ui.previousVersionRadio.get());
- }
-
- async function setBranchPreviousVersionSetting(branch: string) {
- await openBranchSettingModal(branch);
- await user.click(last(ui.previousVersionRadio.getAll()) as HTMLElement);
- await user.click(last(ui.saveButton.getAll()) as HTMLElement);
- }
-
- async function setNumberDaysSetting(value: string) {
- await user.click(ui.specificSettingRadio.get());
- await user.click(ui.numberDaysRadio.get());
- await user.clear(ui.numberDaysInput.get());
- await user.type(ui.numberDaysInput.get(), value);
- }
-
- async function setBranchNumberOfDaysSetting(branch: string, value: string) {
- await openBranchSettingModal(branch);
- await user.click(last(ui.numberDaysRadio.getAll()) as HTMLElement);
- await user.clear(ui.numberDaysInput.get());
- await user.type(ui.numberDaysInput.get(), value);
- await user.click(last(ui.saveButton.getAll()) as HTMLElement);
- }
-
- async function setReferenceBranchSetting(branch: string) {
- await user.click(ui.specificSettingRadio.get());
- await user.click(ui.referenceBranchRadio.get());
- await selectEvent.select(ui.chooseBranchSelect.get(), branch);
- }
-
- async function setBranchReferenceToBranchSetting(branch: string, branchRef: string) {
- await openBranchSettingModal(branch);
- await user.click(last(ui.referenceBranchRadio.getAll()) as HTMLElement);
- await selectEvent.select(ui.chooseBranchSelect.get(), branchRef);
- await user.click(last(ui.saveButton.getAll()) as HTMLElement);
- }
-
- async function openBranchSettingModal(branch: string) {
- await user.click(await ui.branchActionsButton(branch).find());
- await user.click(ui.editButton.get());
- }
-
- return {
- ui: {
- ...ui,
- appIsLoaded,
- setNumberDaysSetting,
- setPreviousVersionSetting,
- setReferenceBranchSetting,
- setBranchPreviousVersionSetting,
- setBranchNumberOfDaysSetting,
- setBranchReferenceToBranchSetting,
- openBranchSettingModal,
- },
- user,
- };
-}
+++ /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 { NewCodeDefinitionType } from '../../../../types/new-code-definition';
-import { getSettingValue, validateSetting } from '../../utils';
-
-describe('getSettingValue', () => {
- const state = {
- analysis: 'analysis',
- days: '35',
- referenceBranch: 'branch-4.2',
- };
-
- it('should work for Days', () => {
- expect(getSettingValue({ ...state, type: NewCodeDefinitionType.NumberOfDays })).toBe(
- state.days
- );
- });
-
- it('should work for Analysis', () => {
- expect(getSettingValue({ ...state, type: NewCodeDefinitionType.SpecificAnalysis })).toBe(
- state.analysis
- );
- });
-
- it('should work for Previous version', () => {
- expect(
- getSettingValue({ ...state, type: NewCodeDefinitionType.PreviousVersion })
- ).toBeUndefined();
- });
-
- it('should work for Reference branch', () => {
- expect(getSettingValue({ ...state, type: NewCodeDefinitionType.ReferenceBranch })).toBe(
- state.referenceBranch
- );
- });
-});
-
-describe('validateSettings', () => {
- it('should validate at branch level', () => {
- expect(validateSetting({ days: '' })).toEqual(false);
- expect(
- validateSetting({
- days: '12',
- selected: NewCodeDefinitionType.NumberOfDays,
- })
- ).toEqual(true);
- expect(
- validateSetting({
- days: 'nope',
- selected: NewCodeDefinitionType.NumberOfDays,
- })
- ).toEqual(false);
- expect(
- validateSetting({
- days: '',
- selected: NewCodeDefinitionType.SpecificAnalysis,
- })
- ).toEqual(false);
- expect(
- validateSetting({
- days: '',
- referenceBranch: 'master',
- selected: NewCodeDefinitionType.ReferenceBranch,
- })
- ).toEqual(true);
- expect(
- validateSetting({
- days: '',
- referenceBranch: '',
- selected: NewCodeDefinitionType.ReferenceBranch,
- })
- ).toEqual(false);
- });
-
- it('should validate at project level', () => {
- expect(validateSetting({ days: '', overrideGeneralSetting: false })).toEqual(true);
- expect(
- validateSetting({
- selected: NewCodeDefinitionType.PreviousVersion,
- days: '',
- overrideGeneralSetting: true,
- })
- ).toEqual(true);
- expect(
- validateSetting({
- selected: NewCodeDefinitionType.NumberOfDays,
- days: '',
- overrideGeneralSetting: true,
- })
- ).toEqual(false);
- expect(
- validateSetting({
- selected: NewCodeDefinitionType.NumberOfDays,
- days: '12',
- overrideGeneralSetting: true,
- })
- ).toEqual(true);
- });
-});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import React from 'react';
-import { Route } from 'react-router-dom';
-import ProjectBaselineApp from './components/ProjectBaselineApp';
-
-const routes = () => <Route path="baseline" element={<ProjectBaselineApp />} />;
-
-export default routes;
+++ /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.
- */
-.project-baseline {
- padding: calc(4 * var(--gridSize));
-}
-
-.project-baseline-setting {
- display: flex;
- flex-direction: column;
- max-height: 60vh;
- padding-top: 2px;
-}
-
-.branch-baseline-selector > hr {
- margin: 0 calc(-4 * var(--gridSize)) calc(4 * var(--gridSize));
-}
-
-.branch-baseline-setting-modal {
- min-height: 450px;
- display: flex;
- flex-direction: column;
-}
-
-.branch-analysis-list-wrapper {
- display: flex;
- flex-direction: column;
- overflow: hidden;
- position: relative;
- min-height: 200px;
-}
-
-.branch-analysis-list {
- overflow-y: auto;
- padding-left: 12px;
- padding-right: 15px;
- min-height: 50px;
-}
-
-.branch-analysis-list > ul {
- padding-top: 18px;
-}
-
-.branch-analysis-date {
- margin-bottom: 16px;
- font-size: 15px;
- font-weight: bold;
-}
-
-.branch-analysis-day {
- margin-top: var(--gridSize);
- margin-bottom: calc(3 * var(--gridSize));
-}
-
-.branch-analysis {
- display: flex;
- justify-content: space-between;
- padding: var(--gridSize);
- border-top: 1px solid var(--barBorderColor);
- border-bottom: 1px solid var(--barBorderColor);
- cursor: not-allowed;
-}
-
-.branch-analysis + .branch-analysis {
- border-top: none;
-}
-
-.branch-analysis:hover {
- background-color: var(--disableGrayBg);
-}
-
-.branch-analysis > .project-activity-events {
- flex: 1 0 50%;
-}
-
-.branch-analysis-time {
- width: 150px;
-}
-
-.branch-analysis-version-badge {
- margin-left: -12px;
- padding-top: var(--gridSize);
- padding-bottom: var(--gridSize);
- background-color: white;
-}
-
-.branch-analysis-version-badge.sticky + .branch-analysis-days-list {
- padding-top: 36px;
-}
-
-.branch-analysis-version-badge.sticky,
-.branch-analysis-version-badge.first {
- position: absolute;
- top: 1px;
- left: 13px;
- right: 16px;
- padding-top: calc(3 * var(--gridSize));
- z-index: var(--belowNormalZIndex);
-}
-
-.branch-analysis-version-badge .badge {
- max-width: 385px;
- border-radius: 0 2px 2px 0;
- font-weight: bold;
- font-size: var(--smallFontSize);
- letter-spacing: 0;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.branch-setting-warning {
- background-color: var(--alertBackgroundWarning) !important;
-}
-
-.project-activity-event-icon.VERSION {
- color: var(--blue);
-}
-
-.project-activity-event-icon.QUALITY_GATE {
- color: var(--purple);
-}
-
-.project-activity-event-icon.QUALITY_PROFILE {
- color: #cccccc;
-}
-
-.project-activity-event-icon.DEFINITION_CHANGE {
- color: #33a759;
-}
-
-.project-activity-event-icon.OTHER {
- color: #442d1b;
-}
+++ /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 { isNewCodeDefinitionCompliant } from '../../helpers/new-code-definition';
-import { NewCodeDefinitionType } from '../../types/new-code-definition';
-
-export function getSettingValue({
- analysis,
- days,
- referenceBranch,
- type,
-}: {
- analysis?: string;
- days?: string;
- referenceBranch?: string;
- type?: NewCodeDefinitionType;
-}) {
- switch (type) {
- case NewCodeDefinitionType.NumberOfDays:
- return days;
- case NewCodeDefinitionType.ReferenceBranch:
- return referenceBranch;
- case NewCodeDefinitionType.SpecificAnalysis:
- return analysis;
- default:
- return undefined;
- }
-}
-
-export function validateSetting(state: {
- days: string;
- overrideGeneralSetting?: boolean;
- referenceBranch?: string;
- selected?: NewCodeDefinitionType;
-}) {
- const { days, overrideGeneralSetting, referenceBranch = '', selected } = state;
-
- return (
- overrideGeneralSetting === false ||
- (!!selected &&
- isNewCodeDefinitionCompliant({
- type: selected,
- value: days,
- }) &&
- (selected !== NewCodeDefinitionType.ReferenceBranch || referenceBranch.length > 0))
- );
-}
--- /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 DocLink from '../../../components/common/DocLink';
+import Link from '../../../components/common/Link';
+import { translate } from '../../../helpers/l10n';
+
+export interface AppHeaderProps {
+ canAdmin: boolean;
+}
+
+export default function AppHeader(props: AppHeaderProps) {
+ const { canAdmin } = props;
+
+ return (
+ <header className="page-header">
+ <h1 className="sw-mb-4">{translate('project_baseline.page')}</h1>
+ <p className="sw-mb-2">{translate('project_baseline.page.description')}</p>
+ <p className="sw-mb-2">{translate('settings.new_code_period.description1')}</p>
+ <p className="sw-mb-2">
+ {canAdmin && (
+ <FormattedMessage
+ defaultMessage={translate('project_baseline.page.description2')}
+ id="project_baseline.page.description2"
+ values={{
+ link: (
+ <Link to="/admin/settings?category=new_code_period">
+ {translate('project_baseline.page.description2.link')}
+ </Link>
+ ),
+ }}
+ />
+ )}
+ </p>
+
+ <p className="sw-mb-2">
+ <FormattedMessage
+ defaultMessage={translate('settings.new_code_period.description3')}
+ id="settings.new_code_period.description3"
+ values={{
+ link: (
+ <DocLink to="/project-administration/defining-new-code/">
+ {translate('settings.new_code_period.description3.link')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
+
+ <p className="sw-mt-4">
+ <strong>{translate('project_baseline.page.question')}</strong>
+ </p>
+ </header>
+ );
+}
--- /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 { subDays } from 'date-fns';
+import { throttle } from 'lodash';
+import * as React from 'react';
+import { getProjectActivity } from '../../../api/projectActivity';
+import { parseDate, toShortISO8601String } from '../../../helpers/dates';
+import { Analysis, ParsedAnalysis } from '../../../types/project-activity';
+import { Dict } from '../../../types/types';
+import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';
+
+interface Props {
+ analysis: string;
+ branch: string;
+ component: string;
+ onSelectAnalysis: (analysis: ParsedAnalysis) => void;
+}
+
+interface State {
+ analyses: ParsedAnalysis[];
+ loading: boolean;
+ range: number;
+ scroll: number;
+}
+
+const STICKY_BADGE_SCROLL_OFFSET = 10;
+
+export default class BranchAnalysisList extends React.PureComponent<Props, State> {
+ mounted = false;
+ badges: Dict<HTMLDivElement> = {};
+ state: State = {
+ analyses: [],
+ loading: true,
+ range: 30,
+ scroll: 0,
+ };
+
+ constructor(props: Props) {
+ super(props);
+ this.updateScroll = throttle(this.updateScroll, 20);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchAnalyses(true);
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ scrollToSelected() {
+ document.querySelector('.branch-analysis.selected')?.scrollIntoView({
+ block: 'center',
+ behavior: 'smooth',
+ });
+ }
+
+ fetchAnalyses(initial = false) {
+ const { analysis, branch, component } = this.props;
+ const { range } = this.state;
+ this.setState({ loading: true });
+
+ return getProjectActivity({
+ branch,
+ project: component,
+ from: range ? toShortISO8601String(subDays(new Date(), range)) : undefined,
+ }).then((result: { analyses: Analysis[] }) => {
+ // If the selected analysis wasn't found in the default 30 days range, redo the search
+ if (initial && analysis && !result.analyses.find((a) => a.key === analysis)) {
+ this.handleRangeChange({ value: 0 });
+ return;
+ }
+
+ this.setState(
+ {
+ analyses: result.analyses.map((analysis) => ({
+ ...analysis,
+ date: parseDate(analysis.date),
+ })) as ParsedAnalysis[],
+ loading: false,
+ },
+ () => {
+ this.scrollToSelected();
+ }
+ );
+ });
+ }
+
+ handleScroll = (e: React.SyntheticEvent<HTMLDivElement>) => {
+ if (e.currentTarget) {
+ this.updateScroll(e.currentTarget.scrollTop);
+ }
+ };
+
+ updateScroll = (scroll: number) => {
+ this.setState({ scroll });
+ };
+
+ registerBadgeNode = (version: string) => (el: HTMLDivElement) => {
+ if (el) {
+ if (!el.getAttribute('originOffsetTop')) {
+ el.setAttribute('originOffsetTop', String(el.offsetTop));
+ }
+ this.badges[version] = el;
+ }
+ };
+
+ shouldStick = (version: string) => {
+ const badge = this.badges[version];
+ return (
+ !!badge &&
+ Number(badge.getAttribute('originOffsetTop')) < this.state.scroll + STICKY_BADGE_SCROLL_OFFSET
+ );
+ };
+
+ handleRangeChange = ({ value }: { value: number }) => {
+ this.setState({ range: value }, () => {
+ this.fetchAnalyses().catch(() => {
+ /* noop */
+ });
+ });
+ };
+
+ render() {
+ const { analysis, onSelectAnalysis } = this.props;
+ const { analyses, loading, range } = this.state;
+
+ return (
+ <BranchAnalysisListRenderer
+ analyses={analyses}
+ handleRangeChange={this.handleRangeChange}
+ handleScroll={this.handleScroll}
+ loading={loading}
+ onSelectAnalysis={onSelectAnalysis}
+ range={range}
+ registerBadgeNode={this.registerBadgeNode}
+ selectedAnalysisKey={analysis}
+ shouldStick={this.shouldStick}
+ />
+ );
+ }
+}
--- /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 classNames from 'classnames';
+import * as React from 'react';
+import { injectIntl, IntlShape, WrappedComponentProps } from 'react-intl';
+import Radio from '../../../components/controls/Radio';
+import Select from '../../../components/controls/Select';
+import Tooltip from '../../../components/controls/Tooltip';
+import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter';
+import TimeFormatter from '../../../components/intl/TimeFormatter';
+import Spinner from '../../../components/ui/Spinner';
+import { parseDate, toShortISO8601String } from '../../../helpers/dates';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { ParsedAnalysis } from '../../../types/project-activity';
+import Events from '../../projectActivity/components/Events';
+import { getAnalysesByVersionByDay } from '../../projectActivity/utils';
+
+export interface BranchAnalysisListRendererProps {
+ analyses: ParsedAnalysis[];
+ handleRangeChange: ({ value }: { value: number }) => void;
+ handleScroll: (e: React.SyntheticEvent<HTMLDivElement>) => void;
+ loading: boolean;
+ onSelectAnalysis: (analysis: ParsedAnalysis) => void;
+ range: number;
+ registerBadgeNode: (version: string) => (el: HTMLDivElement) => void;
+ selectedAnalysisKey: string;
+ shouldStick: (version: string) => boolean;
+}
+
+function renderAnalysis(args: {
+ analysis: ParsedAnalysis;
+ isFirst: boolean;
+ onSelectAnalysis: (analysis: ParsedAnalysis) => void;
+ selectedAnalysisKey: string;
+ intl: IntlShape;
+}) {
+ const { analysis, isFirst, selectedAnalysisKey, intl } = args;
+ return (
+ <li
+ className={classNames('branch-analysis', {
+ selected: analysis.key === selectedAnalysisKey,
+ })}
+ data-date={parseDate(analysis.date).valueOf()}
+ key={analysis.key}
+ >
+ <div className="branch-analysis-time spacer-right">
+ <TimeFormatter date={parseDate(analysis.date)} long={false}>
+ {(formattedTime) => (
+ <time className="text-middle" dateTime={parseDate(analysis.date).toISOString()}>
+ {formattedTime}
+ </time>
+ )}
+ </TimeFormatter>
+ </div>
+
+ {analysis.events.length > 0 && (
+ <Events analysisKey={analysis.key} events={analysis.events} isFirst={isFirst} />
+ )}
+
+ <div className="analysis-selection-button">
+ <Radio
+ checked={analysis.key === selectedAnalysisKey}
+ ariaLabel={translateWithParameters(
+ 'baseline.branch_analyses.analysis_for_x',
+ `${intl.formatDate(analysis.date, longFormatterOption)}, ${intl.formatTime(
+ analysis.date
+ )}`
+ )}
+ onCheck={() => {}}
+ value=""
+ disabled
+ />
+ </div>
+ </li>
+ );
+}
+
+function BranchAnalysisListRenderer(
+ props: BranchAnalysisListRendererProps & WrappedComponentProps
+) {
+ const { analyses, loading, range, selectedAnalysisKey, intl } = props;
+
+ const byVersionByDay = React.useMemo(
+ () =>
+ getAnalysesByVersionByDay(analyses, {
+ category: '',
+ }),
+ [analyses]
+ );
+
+ const hasFilteredData =
+ byVersionByDay.length > 1 ||
+ (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
+
+ const options = [
+ {
+ label: translate('baseline.branch_analyses.ranges.30days'),
+ value: 30,
+ },
+ {
+ label: translate('baseline.branch_analyses.ranges.allTime'),
+ value: 0,
+ },
+ ];
+
+ return (
+ <>
+ <div className="spacer-bottom">
+ <label htmlFor="branch-analysis-from-input" className="spacer-right">
+ {translate('baseline.analysis_from')}
+ </label>
+ <Select
+ blurInputOnSelect
+ inputId="branch-analysis-from-input"
+ className="input-medium spacer-left"
+ onChange={props.handleRangeChange}
+ options={options}
+ isSearchable={false}
+ value={options.filter((o) => o.value === range)}
+ />
+ </div>
+ <div className="branch-analysis-list-wrapper">
+ <div className="bordered branch-analysis-list" onScroll={props.handleScroll}>
+ <Spinner className="big-spacer-top" loading={loading} />
+
+ {!loading && !hasFilteredData ? (
+ <div className="big-spacer-top big-spacer-bottom strong">
+ {translate('baseline.no_analyses')}
+ </div>
+ ) : (
+ <ul>
+ {byVersionByDay.map((version, idx) => {
+ const days = Object.keys(version.byDay);
+ if (days.length <= 0) {
+ return null;
+ }
+ return (
+ <li key={version.key || 'noversion'}>
+ {version.version && (
+ <div
+ className={classNames('branch-analysis-version-badge', {
+ first: idx === 0,
+ sticky: props.shouldStick(version.version),
+ })}
+ ref={props.registerBadgeNode(version.version)}
+ >
+ <Tooltip
+ mouseEnterDelay={0.5}
+ overlay={`${translate('version')} ${version.version}`}
+ >
+ <span className="badge">{version.version}</span>
+ </Tooltip>
+ </div>
+ )}
+ <ul className="branch-analysis-days-list">
+ {days.map((day) => (
+ <li
+ className="branch-analysis-day"
+ data-day={toShortISO8601String(Number(day))}
+ key={day}
+ >
+ <div className="branch-analysis-date">
+ <DateFormatter date={Number(day)} long />
+ </div>
+ <ul className="branch-analysis-analyses-list">
+ {version.byDay[day]?.map((analysis) =>
+ renderAnalysis({
+ analysis,
+ selectedAnalysisKey,
+ isFirst: analyses[0].key === analysis.key,
+ onSelectAnalysis: props.onSelectAnalysis,
+ intl,
+ })
+ )}
+ </ul>
+ </li>
+ ))}
+ </ul>
+ </li>
+ );
+ })}
+ </ul>
+ )}
+ </div>
+ </div>
+ </>
+ );
+}
+
+export default injectIntl(BranchAnalysisListRenderer);
--- /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 {
+ listBranchesNewCodeDefinition,
+ resetNewCodeDefinition,
+} from '../../../api/newCodeDefinition';
+import Spinner from '../../../components/ui/Spinner';
+import { isBranch, sortBranches } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import { DEFAULT_NEW_CODE_DEFINITION_TYPE } from '../../../helpers/new-code-definition';
+import { Branch, BranchLike, BranchWithNewCodePeriod } from '../../../types/branch-like';
+import { NewCodeDefinition } from '../../../types/new-code-definition';
+import { Component } from '../../../types/types';
+import BranchListRow from './BranchListRow';
+import BranchNewCodeDefinitionSettingModal from './BranchNewCodeDefinitionSettingModal';
+
+interface Props {
+ branchList: Branch[];
+ component: Component;
+ inheritedSetting: NewCodeDefinition;
+ generalSetting: NewCodeDefinition;
+}
+
+interface State {
+ branches: BranchWithNewCodePeriod[];
+ editedBranch?: BranchWithNewCodePeriod;
+ loading: boolean;
+}
+
+export default class BranchList extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {
+ branches: [],
+ loading: true,
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchBranches();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.branchList !== this.props.branchList) {
+ this.fetchBranches();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ sortAndFilterBranches(branchLikes: BranchLike[] = []) {
+ return sortBranches(branchLikes.filter(isBranch));
+ }
+
+ fetchBranches() {
+ const project = this.props.component.key;
+ this.setState({ loading: true });
+
+ listBranchesNewCodeDefinition({ project }).then(
+ (branchSettings) => {
+ const newCodePeriods = branchSettings.newCodePeriods
+ ? branchSettings.newCodePeriods.filter((ncp) => !ncp.inherited)
+ : [];
+
+ const branchesWithBaseline = this.props.branchList.map((b) => {
+ const newCodePeriod = newCodePeriods.find((ncp) => ncp.branchKey === b.name);
+ if (!newCodePeriod) {
+ return b;
+ }
+ const { type = DEFAULT_NEW_CODE_DEFINITION_TYPE, value, effectiveValue } = newCodePeriod;
+ return {
+ ...b,
+ newCodePeriod: { type, value, effectiveValue },
+ };
+ });
+
+ this.setState({ branches: branchesWithBaseline, loading: false });
+ },
+ () => {
+ this.setState({ loading: false });
+ }
+ );
+ }
+
+ updateBranchNewCodePeriod = (branch: string, newSetting: NewCodeDefinition | undefined) => {
+ const { branches } = this.state;
+
+ const updated = branches.find((b) => b.name === branch);
+ if (updated) {
+ updated.newCodePeriod = newSetting;
+ }
+ return branches.slice(0);
+ };
+
+ openEditModal = (branch: BranchWithNewCodePeriod) => {
+ this.setState({ editedBranch: branch });
+ };
+
+ closeEditModal = (branch?: string, newSetting?: NewCodeDefinition) => {
+ if (branch) {
+ this.setState({
+ branches: this.updateBranchNewCodePeriod(branch, newSetting),
+ editedBranch: undefined,
+ });
+ } else {
+ this.setState({ editedBranch: undefined });
+ }
+ };
+
+ resetToDefault = (branch: string) => {
+ return resetNewCodeDefinition({
+ project: this.props.component.key,
+ branch,
+ }).then(() => {
+ this.setState({ branches: this.updateBranchNewCodePeriod(branch, undefined) });
+ });
+ };
+
+ render() {
+ const { branchList, inheritedSetting, generalSetting } = this.props;
+ const { branches, editedBranch, loading } = this.state;
+
+ if (branches.length < 1) {
+ return null;
+ }
+
+ if (loading) {
+ return <Spinner />;
+ }
+
+ return (
+ <>
+ <table className="data zebra">
+ <thead>
+ <tr>
+ <th>{translate('branch_list.branch')}</th>
+ <th className="nowrap huge-spacer-right">
+ {translate('branch_list.current_setting')}
+ </th>
+ <th className="thin nowrap">{translate('branch_list.actions')}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {branches.map((branch) => (
+ <BranchListRow
+ branch={branch}
+ existingBranches={branchList.map((b) => b.name)}
+ inheritedSetting={inheritedSetting}
+ key={branch.name}
+ onOpenEditModal={this.openEditModal}
+ onResetToDefault={this.resetToDefault}
+ />
+ ))}
+ </tbody>
+ </table>
+ {editedBranch && (
+ <BranchNewCodeDefinitionSettingModal
+ branch={editedBranch}
+ branchList={branchList}
+ component={this.props.component.key}
+ onClose={this.closeEditModal}
+ inheritedSetting={inheritedSetting}
+ generalSetting={generalSetting}
+ />
+ )}
+ </>
+ );
+ }
+}
--- /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 ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
+import Tooltip from '../../../components/controls/Tooltip';
+import BranchLikeIcon from '../../../components/icons/BranchLikeIcon';
+import WarningIcon from '../../../components/icons/WarningIcon';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { isNewCodeDefinitionCompliant } from '../../../helpers/new-code-definition';
+import { BranchWithNewCodePeriod } from '../../../types/branch-like';
+import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
+
+export interface BranchListRowProps {
+ branch: BranchWithNewCodePeriod;
+ existingBranches: Array<string>;
+ inheritedSetting: NewCodeDefinition;
+ onOpenEditModal: (branch: BranchWithNewCodePeriod) => void;
+ onResetToDefault: (branchName: string) => void;
+}
+
+function renderNewCodePeriodSetting(newCodePeriod: NewCodeDefinition) {
+ switch (newCodePeriod.type) {
+ case NewCodeDefinitionType.SpecificAnalysis:
+ return (
+ <>
+ {`${translate('baseline.specific_analysis')}: `}
+ {newCodePeriod.effectiveValue ? (
+ <DateTimeFormatter date={newCodePeriod.effectiveValue} />
+ ) : (
+ '?'
+ )}
+ </>
+ );
+ case NewCodeDefinitionType.NumberOfDays:
+ return `${translate('new_code_definition.number_days')}: ${newCodePeriod.value}`;
+ case NewCodeDefinitionType.PreviousVersion:
+ return translate('new_code_definition.previous_version');
+ case NewCodeDefinitionType.ReferenceBranch:
+ return `${translate('baseline.reference_branch')}: ${newCodePeriod.value}`;
+ default:
+ return newCodePeriod.type;
+ }
+}
+
+function branchInheritsItselfAsReference(
+ branch: BranchWithNewCodePeriod,
+ inheritedSetting: NewCodeDefinition
+) {
+ return (
+ !branch.newCodePeriod &&
+ inheritedSetting.type === NewCodeDefinitionType.ReferenceBranch &&
+ branch.name === inheritedSetting.value
+ );
+}
+
+function referenceBranchDoesNotExist(
+ branch: BranchWithNewCodePeriod,
+ existingBranches: Array<string>
+) {
+ return (
+ branch.newCodePeriod &&
+ branch.newCodePeriod.value &&
+ branch.newCodePeriod.type === NewCodeDefinitionType.ReferenceBranch &&
+ !existingBranches.includes(branch.newCodePeriod.value)
+ );
+}
+
+export default function BranchListRow(props: BranchListRowProps) {
+ const { branch, existingBranches, inheritedSetting } = props;
+
+ let settingWarning: string | undefined;
+ if (branchInheritsItselfAsReference(branch, inheritedSetting)) {
+ settingWarning = translateWithParameters(
+ 'baseline.reference_branch.invalid_branch_setting',
+ branch.name
+ );
+ } else if (referenceBranchDoesNotExist(branch, existingBranches)) {
+ settingWarning = translateWithParameters(
+ 'baseline.reference_branch.does_not_exist',
+ branch.newCodePeriod?.value || ''
+ );
+ }
+
+ const isCompliant = isNewCodeDefinitionCompliant(inheritedSetting);
+
+ return (
+ <tr className={settingWarning ? 'branch-setting-warning' : ''}>
+ <td className="nowrap">
+ <BranchLikeIcon branchLike={branch} className="little-spacer-right" />
+ {branch.name}
+ {branch.isMain && (
+ <div className="badge spacer-left">{translate('branches.main_branch')}</div>
+ )}
+ </td>
+ <td className="huge-spacer-right nowrap">
+ <Tooltip overlay={settingWarning}>
+ <span>
+ {settingWarning && <WarningIcon className="little-spacer-right" />}
+ {branch.newCodePeriod
+ ? renderNewCodePeriodSetting(branch.newCodePeriod)
+ : translate('branch_list.default_setting')}
+ </span>
+ </Tooltip>
+ </td>
+ <td className="text-right">
+ <ActionsDropdown
+ label={translateWithParameters('branch_list.show_actions_for_x', branch.name)}
+ >
+ <ActionsDropdownItem onClick={() => props.onOpenEditModal(branch)}>
+ {translate('edit')}
+ </ActionsDropdownItem>
+ {branch.newCodePeriod && (
+ <ActionsDropdownItem
+ disabled={!isCompliant}
+ onClick={() => props.onResetToDefault(branch.name)}
+ tooltipOverlay={
+ isCompliant ? null : translate('project_baseline.compliance.warning.title.project')
+ }
+ >
+ {translate('reset_to_default')}
+ </ActionsDropdownItem>
+ )}
+ </ActionsDropdown>
+ </td>
+ </tr>
+ );
+}
--- /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 { setNewCodeDefinition } from '../../../api/newCodeDefinition';
+import Modal from '../../../components/controls/Modal';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+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 Spinner from '../../../components/ui/Spinner';
+import { toISO8601WithOffsetString } from '../../../helpers/dates';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { getNumberOfDaysDefaultValue } from '../../../helpers/new-code-definition';
+import { Branch, BranchWithNewCodePeriod } from '../../../types/branch-like';
+import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
+import { getSettingValue, validateSetting } from '../utils';
+import BranchAnalysisList from './BranchAnalysisList';
+import NewCodeDefinitionSettingAnalysis from './NewCodeDefinitionSettingAnalysis';
+import NewCodeDefinitionSettingReferenceBranch from './NewCodeDefinitionSettingReferenceBranch';
+
+interface Props {
+ branch: BranchWithNewCodePeriod;
+ branchList: Branch[];
+ component: string;
+ onClose: (branch?: string, newSetting?: NewCodeDefinition) => void;
+ inheritedSetting: NewCodeDefinition;
+ generalSetting: NewCodeDefinition;
+}
+
+interface State {
+ analysis: string;
+ analysisDate?: Date;
+ days: string;
+ isChanged: boolean;
+ referenceBranch: string;
+ saving: boolean;
+ selected?: NewCodeDefinitionType;
+}
+
+export default class BranchNewCodeDefinitionSettingModal extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+
+ const { branch, branchList, inheritedSetting, generalSetting } = props;
+ const otherBranches = branchList.filter((b) => b.name !== branch.name);
+ const defaultBranch = otherBranches.length > 0 ? otherBranches[0].name : '';
+
+ this.state = {
+ analysis: this.getValueFromProps(NewCodeDefinitionType.SpecificAnalysis) || '',
+ days:
+ this.getValueFromProps(NewCodeDefinitionType.NumberOfDays) ||
+ getNumberOfDaysDefaultValue(generalSetting, inheritedSetting),
+ isChanged: false,
+ referenceBranch:
+ this.getValueFromProps(NewCodeDefinitionType.ReferenceBranch) || defaultBranch,
+ saving: false,
+ selected: branch.newCodePeriod?.type,
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ getValueFromProps(type: NewCodeDefinitionType) {
+ return this.props.branch.newCodePeriod && this.props.branch.newCodePeriod.type === type
+ ? this.props.branch.newCodePeriod.value
+ : null;
+ }
+
+ branchToOption = (b: Branch) => ({
+ label: b.name,
+ value: b.name,
+ isMain: b.isMain,
+ isDisabled: b.name === this.props.branch.name, // cannot itself be used as a reference branch
+ });
+
+ handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+ e.preventDefault();
+
+ const { branch, component } = this.props;
+ const { analysis, analysisDate, days, referenceBranch, selected: type } = this.state;
+
+ const value = getSettingValue({ type, analysis, days, referenceBranch });
+
+ if (type) {
+ this.setState({ saving: true });
+ setNewCodeDefinition({
+ project: component,
+ type,
+ value,
+ branch: branch.name,
+ }).then(
+ () => {
+ if (this.mounted) {
+ this.setState({
+ saving: false,
+ isChanged: false,
+ });
+ this.props.onClose(branch.name, {
+ type,
+ value,
+ effectiveValue: analysisDate && toISO8601WithOffsetString(analysisDate),
+ });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({
+ saving: false,
+ });
+ }
+ }
+ );
+ }
+ };
+
+ requestClose = () => this.props.onClose();
+
+ handleSelectDays = (days: string) => this.setState({ days, isChanged: true });
+
+ handleSelectReferenceBranch = (referenceBranch: string) =>
+ this.setState({ referenceBranch, isChanged: true });
+
+ handleSelectSetting = (selected: NewCodeDefinitionType) => {
+ this.setState((currentState) => ({ selected, isChanged: selected !== currentState.selected }));
+ };
+
+ render() {
+ const { branch, branchList } = this.props;
+ const { analysis, days, isChanged, referenceBranch, saving, selected } = this.state;
+
+ const header = translateWithParameters('baseline.new_code_period_for_branch_x', branch.name);
+
+ const currentSetting = branch.newCodePeriod?.type;
+ const currentSettingValue = branch.newCodePeriod?.value;
+
+ const isValid = validateSetting({
+ days,
+ referenceBranch,
+ selected,
+ });
+
+ return (
+ <Modal contentLabel={header} onRequestClose={this.requestClose} size="large">
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body modal-container branch-baseline-setting-modal">
+ <p className="sw-mb-3">{translate('baseline.new_code_period_for_branch_x.question')}</p>
+ <NewCodeDefinitionWarning
+ newCodeDefinitionType={currentSetting}
+ newCodeDefinitionValue={currentSettingValue}
+ isBranchSupportEnabled
+ level="branch"
+ />
+ <div className="display-flex-column huge-spacer-bottom sw-gap-4" role="radiogroup">
+ <NewCodeDefinitionPreviousVersionOption
+ isDefault={false}
+ onSelect={this.handleSelectSetting}
+ selected={selected === NewCodeDefinitionType.PreviousVersion}
+ />
+ <NewCodeDefinitionDaysOption
+ days={days}
+ isChanged={isChanged}
+ isValid={isValid}
+ onChangeDays={this.handleSelectDays}
+ onSelect={this.handleSelectSetting}
+ selected={selected === NewCodeDefinitionType.NumberOfDays}
+ />
+ <NewCodeDefinitionSettingReferenceBranch
+ branchList={branchList.map(this.branchToOption)}
+ onChangeReferenceBranch={this.handleSelectReferenceBranch}
+ onSelect={this.handleSelectSetting}
+ referenceBranch={referenceBranch}
+ selected={selected === NewCodeDefinitionType.ReferenceBranch}
+ settingLevel="branch"
+ />
+ {currentSetting === NewCodeDefinitionType.SpecificAnalysis && (
+ <NewCodeDefinitionSettingAnalysis
+ onSelect={noop}
+ selected={selected === NewCodeDefinitionType.SpecificAnalysis}
+ />
+ )}
+ </div>
+ {selected === NewCodeDefinitionType.SpecificAnalysis && (
+ <BranchAnalysisList
+ analysis={analysis}
+ branch={branch.name}
+ component={this.props.component}
+ onSelectAnalysis={noop}
+ />
+ )}
+ </div>
+ <footer className="modal-foot">
+ <Spinner className="spacer-right" loading={saving} />
+ <SubmitButton disabled={!isChanged || saving || !isValid}>
+ {translate('save')}
+ </SubmitButton>
+ <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}
--- /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 { NewCodeDefinitionType } from '../../../types/new-code-definition';
+
+export interface Props {
+ onSelect: (selection: NewCodeDefinitionType) => void;
+ selected: boolean;
+}
+
+export default function NewCodeDefinitionSettingAnalysis({ onSelect, selected }: Props) {
+ return (
+ <RadioCard
+ noRadio
+ disabled
+ onClick={() => onSelect(NewCodeDefinitionType.SpecificAnalysis)}
+ selected={selected}
+ title={translate('baseline.specific_analysis')}
+ >
+ <p className="big-spacer-bottom">{translate('baseline.specific_analysis.description')}</p>
+ </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 { SelectionCard } from 'design-system';
+import * as React from 'react';
+import { components, OptionProps } from 'react-select';
+import Select from '../../../components/controls/Select';
+import Tooltip from '../../../components/controls/Tooltip';
+import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
+import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
+import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { NewCodeDefinitionType } from '../../../types/new-code-definition';
+
+export interface BaselineSettingReferenceBranchProps {
+ branchList: BranchOption[];
+ className?: string;
+ disabled?: boolean;
+ onChangeReferenceBranch: (value: string) => void;
+ onSelect: (selection: NewCodeDefinitionType) => void;
+ referenceBranch: string;
+ selected: boolean;
+ settingLevel: 'project' | 'branch';
+}
+
+export interface BranchOption {
+ isDisabled?: boolean;
+ isInvalid?: boolean;
+ isMain: boolean;
+ label: string;
+ value: string;
+}
+
+function renderBranchOption(props: OptionProps<BranchOption, false>) {
+ const { data: option } = props;
+
+ return (
+ <components.Option {...props}>
+ {option.isInvalid ? (
+ <Tooltip
+ overlay={translateWithParameters(
+ 'baseline.reference_branch.does_not_exist',
+ option.value
+ )}
+ >
+ <span>
+ {option.value} <AlertErrorIcon />
+ </span>
+ </Tooltip>
+ ) : (
+ <>
+ <span
+ title={
+ option.isDisabled
+ ? translate('baseline.reference_branch.cannot_be_itself')
+ : undefined
+ }
+ >
+ {option.value}
+ </span>
+ {option.isMain && (
+ <div className="badge spacer-left">{translate('branches.main_branch')}</div>
+ )}
+ </>
+ )}
+ </components.Option>
+ );
+}
+
+export default function NewCodeDefinitionSettingReferenceBranch(
+ props: BaselineSettingReferenceBranchProps
+) {
+ const { branchList, className, disabled, referenceBranch, selected, settingLevel } = props;
+
+ const currentBranch = branchList.find((b) => b.value === referenceBranch) || {
+ label: referenceBranch,
+ value: referenceBranch,
+ isMain: false,
+ isInvalid: true,
+ };
+
+ return (
+ <SelectionCard
+ className={className}
+ disabled={disabled}
+ onClick={() => props.onSelect(NewCodeDefinitionType.ReferenceBranch)}
+ selected={selected}
+ title={translate('baseline.reference_branch')}
+ >
+ <>
+ <div>
+ <p className="sw-mb-3">{translate('baseline.reference_branch.description')}</p>
+ <p className="sw-mb-4">{translate('baseline.reference_branch.usecase')}</p>
+ </div>
+ {selected && (
+ <>
+ {settingLevel === 'project' && (
+ <p className="spacer-top">{translate('baseline.reference_branch.description2')}</p>
+ )}
+ <div className="big-spacer-top display-flex-column">
+ <MandatoryFieldsExplanation className="spacer-bottom" />
+ <label className="text-middle" htmlFor="reference_branch">
+ <strong>{translate('baseline.reference_branch.choose')}</strong>
+ <MandatoryFieldMarker />
+ </label>
+ <Select
+ className="little-spacer-top spacer-bottom"
+ options={branchList}
+ aria-label={translate('baseline.reference_branch.choose')}
+ onChange={(option: BranchOption) => props.onChangeReferenceBranch(option.value)}
+ value={currentBranch}
+ components={{
+ Option: renderBranchOption,
+ }}
+ />
+ </div>
+ </>
+ )}
+ </>
+ </SelectionCard>
+ );
+}
--- /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 classNames from 'classnames';
+import { debounce } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import {
+ getNewCodeDefinition,
+ resetNewCodeDefinition,
+ setNewCodeDefinition,
+} from '../../../api/newCodeDefinition';
+import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
+import withAvailableFeatures, {
+ WithAvailableFeaturesProps,
+} from '../../../app/components/available-features/withAvailableFeatures';
+import withComponentContext from '../../../app/components/componentContext/withComponentContext';
+import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
+import Spinner from '../../../components/ui/Spinner';
+import { isBranch, sortBranches } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import {
+ DEFAULT_NEW_CODE_DEFINITION_TYPE,
+ getNumberOfDaysDefaultValue,
+} from '../../../helpers/new-code-definition';
+import { withBranchLikes } from '../../../queries/branch';
+import { AppState } from '../../../types/appstate';
+import { Branch, BranchLike } from '../../../types/branch-like';
+import { Feature } from '../../../types/features';
+import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
+import { Component } from '../../../types/types';
+import '../styles.css';
+import { getSettingValue } from '../utils';
+import AppHeader from './AppHeader';
+import BranchList from './BranchList';
+import ProjectNewCodeDefinitionSelector from './ProjectNewCodeDefinitionSelector';
+
+interface Props extends WithAvailableFeaturesProps {
+ branchLike: Branch;
+ branchLikes: BranchLike[];
+ component: Component;
+ appState: AppState;
+}
+
+interface State {
+ analysis?: string;
+ branchList: Branch[];
+ currentSetting?: NewCodeDefinitionType;
+ currentSettingValue?: string;
+ days: string;
+ generalSetting?: NewCodeDefinition;
+ isChanged: boolean;
+ loading: boolean;
+ overrideGeneralSetting?: boolean;
+ referenceBranch?: string;
+ saving: boolean;
+ selected?: NewCodeDefinitionType;
+ success?: boolean;
+}
+
+class ProjectNewCodeDefinitionApp extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {
+ branchList: [],
+ days: getNumberOfDaysDefaultValue(),
+ isChanged: false,
+ loading: true,
+ saving: false,
+ };
+
+ // We use debounce as we could have multiple save in less that 3sec.
+ resetSuccess = debounce(() => this.setState({ success: undefined }), 3000);
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchLeakPeriodSetting();
+ this.sortAndFilterBranches(this.props.branchLikes);
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.branchLikes !== this.props.branchLikes) {
+ this.sortAndFilterBranches(this.props.branchLikes);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ getUpdatedState(params: {
+ currentSetting?: NewCodeDefinitionType;
+ currentSettingValue?: string;
+ generalSetting: NewCodeDefinition;
+ }) {
+ const { currentSetting, currentSettingValue, generalSetting } = params;
+ const { referenceBranch } = this.state;
+
+ const defaultDays = getNumberOfDaysDefaultValue(generalSetting);
+
+ return {
+ loading: false,
+ currentSetting,
+ currentSettingValue,
+ generalSetting,
+ isChanged: false,
+ selected: currentSetting || generalSetting.type,
+ overrideGeneralSetting: Boolean(currentSetting),
+ days:
+ (currentSetting === NewCodeDefinitionType.NumberOfDays && currentSettingValue) ||
+ defaultDays,
+ analysis:
+ (currentSetting === NewCodeDefinitionType.SpecificAnalysis && currentSettingValue) || '',
+ referenceBranch:
+ (currentSetting === NewCodeDefinitionType.ReferenceBranch && currentSettingValue) ||
+ referenceBranch,
+ };
+ }
+
+ sortAndFilterBranches(branchLikes: BranchLike[] = []) {
+ const branchList = sortBranches(branchLikes.filter(isBranch));
+ this.setState({ branchList, referenceBranch: branchList[0]?.name });
+ }
+
+ fetchLeakPeriodSetting() {
+ const { branchLike, component } = this.props;
+
+ this.setState({ loading: true });
+
+ Promise.all([
+ getNewCodeDefinition(),
+ getNewCodeDefinition({
+ branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name,
+ project: component.key,
+ }),
+ ]).then(
+ ([generalSetting, setting]) => {
+ if (this.mounted) {
+ if (!generalSetting.type) {
+ generalSetting = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE };
+ }
+ const currentSettingValue = setting.value;
+ const currentSetting = setting.inherited
+ ? undefined
+ : setting.type || DEFAULT_NEW_CODE_DEFINITION_TYPE;
+
+ this.setState(
+ this.getUpdatedState({
+ generalSetting,
+ currentSetting,
+ currentSettingValue,
+ })
+ );
+ }
+ },
+ () => {
+ this.setState({ loading: false });
+ }
+ );
+ }
+
+ resetSetting = () => {
+ this.setState({ saving: true });
+ resetNewCodeDefinition({ project: this.props.component.key }).then(
+ () => {
+ this.setState({
+ saving: false,
+ currentSetting: undefined,
+ isChanged: false,
+ selected: undefined,
+ success: true,
+ });
+ this.resetSuccess();
+ },
+ () => {
+ this.setState({ saving: false });
+ }
+ );
+ };
+
+ handleSelectDays = (days: string) => this.setState({ days, isChanged: true });
+
+ handleSelectReferenceBranch = (referenceBranch: string) => {
+ this.setState({ referenceBranch, isChanged: true });
+ };
+
+ handleCancel = () =>
+ this.setState(
+ ({
+ generalSetting = { type: DEFAULT_NEW_CODE_DEFINITION_TYPE },
+ currentSetting,
+ currentSettingValue,
+ }) => this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue })
+ );
+
+ handleSelectSetting = (selected?: NewCodeDefinitionType) => {
+ this.setState((currentState) => ({
+ selected,
+ isChanged: selected !== currentState.selected,
+ }));
+ };
+
+ handleToggleSpecificSetting = (overrideGeneralSetting: boolean) =>
+ this.setState((currentState) => ({
+ overrideGeneralSetting,
+ isChanged: currentState.overrideGeneralSetting !== overrideGeneralSetting,
+ }));
+
+ handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+ e.preventDefault();
+
+ const { component } = this.props;
+ const { days, selected: type, referenceBranch, overrideGeneralSetting } = this.state;
+
+ if (!overrideGeneralSetting) {
+ this.resetSetting();
+ return;
+ }
+
+ const value = getSettingValue({ type, days, referenceBranch });
+
+ if (type) {
+ this.setState({ saving: true });
+ setNewCodeDefinition({
+ project: component.key,
+ type,
+ value,
+ }).then(
+ () => {
+ this.setState({
+ saving: false,
+ currentSetting: type,
+ currentSettingValue: value || undefined,
+ isChanged: false,
+ success: true,
+ });
+ this.resetSuccess();
+ },
+ () => {
+ this.setState({ saving: false });
+ }
+ );
+ }
+ };
+
+ render() {
+ const { appState, component, branchLike } = this.props;
+ const {
+ analysis,
+ branchList,
+ currentSetting,
+ days,
+ generalSetting,
+ isChanged,
+ loading,
+ currentSettingValue,
+ overrideGeneralSetting,
+ referenceBranch,
+ saving,
+ selected,
+ success,
+ } = this.state;
+ const branchSupportEnabled = this.props.hasFeature(Feature.BranchSupport);
+
+ return (
+ <>
+ <Suggestions suggestions="project_baseline" />
+ <Helmet defer={false} title={translate('project_baseline.page')} />
+ <div className="page page-limited">
+ <AppHeader canAdmin={!!appState.canAdmin} />
+ <Spinner loading={loading} />
+
+ {!loading && (
+ <div className="panel-white project-baseline">
+ {branchSupportEnabled && <h2>{translate('project_baseline.default_setting')}</h2>}
+
+ {generalSetting && overrideGeneralSetting !== undefined && (
+ <ProjectNewCodeDefinitionSelector
+ analysis={analysis}
+ branch={branchLike}
+ branchList={branchList}
+ branchesEnabled={branchSupportEnabled}
+ canAdmin={appState.canAdmin}
+ component={component.key}
+ currentSetting={currentSetting}
+ currentSettingValue={currentSettingValue}
+ days={days}
+ generalSetting={generalSetting}
+ isChanged={isChanged}
+ onCancel={this.handleCancel}
+ onSelectDays={this.handleSelectDays}
+ onSelectReferenceBranch={this.handleSelectReferenceBranch}
+ onSelectSetting={this.handleSelectSetting}
+ onSubmit={this.handleSubmit}
+ onToggleSpecificSetting={this.handleToggleSpecificSetting}
+ overrideGeneralSetting={overrideGeneralSetting}
+ referenceBranch={referenceBranch}
+ saving={saving}
+ selected={selected}
+ />
+ )}
+
+ <div className={classNames('spacer-top', { invisible: saving || !success })}>
+ <span className="text-success">
+ <AlertSuccessIcon className="spacer-right" />
+ {translate('settings.state.saved')}
+ </span>
+ </div>
+ {generalSetting && branchSupportEnabled && (
+ <div className="huge-spacer-top branch-baseline-selector">
+ <hr />
+ <h2>{translate('project_baseline.configure_branches')}</h2>
+ <BranchList
+ branchList={branchList}
+ component={component}
+ inheritedSetting={
+ currentSetting
+ ? {
+ type: currentSetting,
+ value: currentSettingValue,
+ }
+ : generalSetting
+ }
+ generalSetting={generalSetting}
+ />
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+ </>
+ );
+ }
+}
+
+export default withComponentContext(
+ withAvailableFeatures(withAppStateContext(withBranchLikes(ProjectNewCodeDefinitionApp)))
+);
--- /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 classNames from 'classnames';
+import { RadioButton } from 'design-system';
+import { noop } from 'lodash';
+import * as React from 'react';
+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 Spinner from '../../../components/ui/Spinner';
+import { translate } from '../../../helpers/l10n';
+import { isNewCodeDefinitionCompliant } from '../../../helpers/new-code-definition';
+import { Branch } from '../../../types/branch-like';
+import { NewCodeDefinition, NewCodeDefinitionType } from '../../../types/new-code-definition';
+import { validateSetting } from '../utils';
+import BranchAnalysisList from './BranchAnalysisList';
+import NewCodeDefinitionSettingAnalysis from './NewCodeDefinitionSettingAnalysis';
+import NewCodeDefinitionSettingReferenceBranch from './NewCodeDefinitionSettingReferenceBranch';
+
+export interface ProjectBaselineSelectorProps {
+ analysis?: string;
+ branch?: Branch;
+ branchList: Branch[];
+ branchesEnabled?: boolean;
+ canAdmin: boolean | undefined;
+ component: string;
+ currentSetting?: NewCodeDefinitionType;
+ currentSettingValue?: string;
+ days: string;
+ generalSetting: NewCodeDefinition;
+ isChanged: boolean;
+ onCancel: () => void;
+ onSelectDays: (value: string) => void;
+ onSelectReferenceBranch: (value: string) => void;
+ onSelectSetting: (value?: NewCodeDefinitionType) => void;
+ onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void;
+ onToggleSpecificSetting: (selection: boolean) => void;
+ referenceBranch?: string;
+ saving: boolean;
+ selected?: NewCodeDefinitionType;
+ overrideGeneralSetting: boolean;
+}
+
+function branchToOption(b: Branch) {
+ return { label: b.name, value: b.name, isMain: b.isMain };
+}
+
+export default function ProjectNewCodeDefinitionSelector(props: ProjectBaselineSelectorProps) {
+ const {
+ analysis,
+ branch,
+ branchList,
+ branchesEnabled,
+ canAdmin,
+ component,
+ currentSetting,
+ currentSettingValue,
+ days,
+ generalSetting,
+ isChanged,
+ overrideGeneralSetting,
+ referenceBranch,
+ saving,
+ selected,
+ } = props;
+
+ const isGlobalNcdCompliant = isNewCodeDefinitionCompliant(generalSetting);
+
+ const isValid = validateSetting({
+ days,
+ overrideGeneralSetting,
+ referenceBranch,
+ selected,
+ });
+
+ if (branch === undefined) {
+ return null;
+ }
+
+ return (
+ <form className="project-baseline-selector" onSubmit={props.onSubmit}>
+ <div className="big-spacer-top spacer-bottom" role="radiogroup">
+ <RadioButton
+ checked={!overrideGeneralSetting}
+ className="big-spacer-bottom"
+ disabled={!isGlobalNcdCompliant}
+ onCheck={() => props.onToggleSpecificSetting(false)}
+ value="general"
+ >
+ <Tooltip
+ overlay={
+ isGlobalNcdCompliant
+ ? null
+ : translate('project_baseline.compliance.warning.title.global')
+ }
+ >
+ <span>{translate('project_baseline.global_setting')}</span>
+ </Tooltip>
+ </RadioButton>
+
+ <div className="sw-ml-4">
+ <GlobalNewCodeDefinitionDescription
+ globalNcd={generalSetting}
+ isGlobalNcdCompliant={isGlobalNcdCompliant}
+ canAdmin={canAdmin}
+ />
+ </div>
+
+ <RadioButton
+ checked={overrideGeneralSetting}
+ className="huge-spacer-top"
+ onCheck={() => props.onToggleSpecificSetting(true)}
+ value="specific"
+ >
+ {translate('project_baseline.specific_setting')}
+ </RadioButton>
+ </div>
+
+ <div className="big-spacer-left big-spacer-right project-baseline-setting">
+ <NewCodeDefinitionWarning
+ newCodeDefinitionType={currentSetting}
+ newCodeDefinitionValue={currentSettingValue}
+ isBranchSupportEnabled={branchesEnabled}
+ level="project"
+ />
+ <div className="display-flex-column big-spacer-bottom sw-gap-4" role="radiogroup">
+ <NewCodeDefinitionPreviousVersionOption
+ disabled={!overrideGeneralSetting}
+ onSelect={props.onSelectSetting}
+ selected={overrideGeneralSetting && selected === NewCodeDefinitionType.PreviousVersion}
+ />
+ <NewCodeDefinitionDaysOption
+ days={days}
+ disabled={!overrideGeneralSetting}
+ isChanged={isChanged}
+ isValid={isValid}
+ onChangeDays={props.onSelectDays}
+ onSelect={props.onSelectSetting}
+ selected={overrideGeneralSetting && selected === NewCodeDefinitionType.NumberOfDays}
+ />
+ {branchesEnabled && (
+ <NewCodeDefinitionSettingReferenceBranch
+ branchList={branchList.map(branchToOption)}
+ disabled={!overrideGeneralSetting}
+ onChangeReferenceBranch={props.onSelectReferenceBranch}
+ onSelect={props.onSelectSetting}
+ referenceBranch={referenceBranch || ''}
+ selected={
+ overrideGeneralSetting && selected === NewCodeDefinitionType.ReferenceBranch
+ }
+ settingLevel="project"
+ />
+ )}
+ {!branchesEnabled && currentSetting === NewCodeDefinitionType.SpecificAnalysis && (
+ <NewCodeDefinitionSettingAnalysis
+ onSelect={noop}
+ selected={
+ overrideGeneralSetting && selected === NewCodeDefinitionType.SpecificAnalysis
+ }
+ />
+ )}
+ </div>
+ {!branchesEnabled &&
+ overrideGeneralSetting &&
+ selected === NewCodeDefinitionType.SpecificAnalysis && (
+ <BranchAnalysisList
+ analysis={analysis || ''}
+ branch={branch.name}
+ component={component}
+ onSelectAnalysis={noop}
+ />
+ )}
+ </div>
+ <div className={classNames('big-spacer-top', { invisible: !isChanged })}>
+ <Alert variant="info" className="spacer-bottom">
+ {translate('baseline.next_analysis_notice')}
+ </Alert>
+ <Spinner className="spacer-right" loading={saving} />
+ <SubmitButton disabled={saving || !isValid || !isChanged}>{translate('save')}</SubmitButton>
+ <ResetButtonLink className="spacer-left" onClick={props.onCancel}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </div>
+ </form>
+ );
+}
--- /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 { within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { first, last } from 'lodash';
+import selectEvent from 'react-select-event';
+import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
+import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockNewCodePeriodBranch } from '../../../../helpers/mocks/new-code-definition';
+import { mockAppState } from '../../../../helpers/testMocks';
+import {
+ RenderContext,
+ renderAppWithComponentContext,
+} from '../../../../helpers/testReactTestingUtils';
+import { byRole, byText } from '../../../../helpers/testSelector';
+import { Feature } from '../../../../types/features';
+import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
+import routes from '../../routes';
+
+jest.mock('../../../../api/newCodeDefinition');
+jest.mock('../../../../api/projectActivity');
+jest.mock('../../../../api/branches');
+
+const codePeriodsMock = new NewCodeDefinitionServiceMock();
+const projectActivityMock = new ProjectActivityServiceMock();
+const branchHandler = new BranchesServiceMock();
+
+afterEach(() => {
+ branchHandler.reset();
+ codePeriodsMock.reset();
+ projectActivityMock.reset();
+});
+
+it('renders correctly without branch support feature', async () => {
+ const { ui } = getPageObjects();
+ renderProjectBaselineApp();
+ await ui.appIsLoaded();
+
+ expect(await ui.generalSettingRadio.find()).toBeChecked();
+ expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
+
+ // User is not admin
+ expect(ui.generalSettingsLink.query()).not.toBeInTheDocument();
+
+ // Specific branch setting is not rendered without feature branch
+ expect(ui.branchListHeading.query()).not.toBeInTheDocument();
+ expect(ui.referenceBranchRadio.query()).not.toBeInTheDocument();
+});
+
+it('prevents selection of global setting if it is not compliant and warns non-admin about it', async () => {
+ codePeriodsMock.setNewCodePeriod({
+ type: NewCodeDefinitionType.NumberOfDays,
+ value: '99',
+ inherited: true,
+ });
+
+ const { ui } = getPageObjects();
+ renderProjectBaselineApp();
+ await ui.appIsLoaded();
+
+ expect(await ui.generalSettingRadio.find()).toBeChecked();
+ expect(ui.generalSettingRadio.get()).toBeDisabled();
+ expect(ui.complianceWarning.get()).toBeVisible();
+});
+
+it('prevents selection of global setting if it is not compliant and warns admin about it', async () => {
+ codePeriodsMock.setNewCodePeriod({
+ type: NewCodeDefinitionType.NumberOfDays,
+ value: '99',
+ inherited: true,
+ });
+
+ const { ui } = getPageObjects();
+ renderProjectBaselineApp({ appState: mockAppState({ canAdmin: true }) });
+ await ui.appIsLoaded();
+
+ expect(await ui.generalSettingRadio.find()).toBeChecked();
+ expect(ui.generalSettingRadio.get()).toBeDisabled();
+ expect(ui.complianceWarningAdmin.get()).toBeVisible();
+ expect(ui.complianceWarning.query()).not.toBeInTheDocument();
+});
+
+it('renders correctly with branch support feature', async () => {
+ const { ui } = getPageObjects();
+ renderProjectBaselineApp({
+ featureList: [Feature.BranchSupport],
+ appState: mockAppState({ canAdmin: true }),
+ });
+ await ui.appIsLoaded();
+
+ expect(await ui.generalSettingRadio.find()).toBeChecked();
+ expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
+
+ // User is admin
+ expect(ui.generalSettingsLink.get()).toBeInTheDocument();
+
+ // Specific branch setting is rendered with feature support branch
+ expect(ui.branchListHeading.get()).toBeInTheDocument();
+ expect(ui.referenceBranchRadio.get()).toBeInTheDocument();
+});
+
+it('can set previous version specific setting', async () => {
+ const { ui, user } = getPageObjects();
+ renderProjectBaselineApp();
+ await ui.appIsLoaded();
+
+ expect(await ui.previousVersionRadio.find()).toHaveClass('disabled');
+ await ui.setPreviousVersionSetting();
+ expect(ui.previousVersionRadio.get()).toBeChecked();
+
+ // Save changes
+ await user.click(ui.saveButton.get());
+
+ expect(ui.saved.get()).toBeInTheDocument();
+
+ // Set general setting
+ await user.click(ui.generalSettingRadio.get());
+ expect(ui.previousVersionRadio.get()).toHaveClass('disabled');
+ await user.click(ui.saveButton.get());
+ expect(ui.saved.get()).toBeInTheDocument();
+});
+
+it('can set number of days specific setting', async () => {
+ const { ui, user } = getPageObjects();
+ renderProjectBaselineApp();
+ await ui.appIsLoaded();
+
+ expect(await ui.numberDaysRadio.find()).toHaveClass('disabled');
+ await ui.setNumberDaysSetting('10');
+ expect(ui.numberDaysRadio.get()).toBeChecked();
+
+ // Reset to initial state
+ await user.click(ui.cancelButton.get());
+ expect(ui.generalSettingRadio.get()).toBeChecked();
+ expect(ui.numberDaysRadio.get()).toHaveClass('disabled');
+
+ // Save changes
+ await ui.setNumberDaysSetting('10');
+ await user.click(ui.saveButton.get());
+
+ expect(ui.saved.get()).toBeInTheDocument();
+});
+
+it('can set reference branch specific setting', async () => {
+ const { ui, user } = getPageObjects();
+ renderProjectBaselineApp({
+ featureList: [Feature.BranchSupport],
+ });
+ await ui.appIsLoaded();
+
+ expect(await ui.referenceBranchRadio.find()).toHaveClass('disabled');
+ await ui.setReferenceBranchSetting('main');
+ expect(ui.referenceBranchRadio.get()).toBeChecked();
+
+ // Save changes
+ await user.click(ui.saveButton.get());
+
+ expect(ui.saved.get()).toBeInTheDocument();
+});
+
+it('cannot set specific analysis setting', async () => {
+ const { ui } = getPageObjects();
+ codePeriodsMock.setNewCodePeriod({
+ type: NewCodeDefinitionType.SpecificAnalysis,
+ value: 'analysis_id',
+ });
+ renderProjectBaselineApp();
+ await ui.appIsLoaded();
+
+ expect(await ui.specificAnalysisRadio.find()).toBeChecked();
+ expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
+ expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
+
+ await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
+
+ expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
+ expect(ui.saveButton.get()).toBeDisabled();
+});
+
+it('renders correctly branch modal', async () => {
+ const { ui } = getPageObjects();
+ renderProjectBaselineApp({
+ featureList: [Feature.BranchSupport],
+ });
+ await ui.appIsLoaded();
+
+ await ui.openBranchSettingModal('main');
+
+ expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
+});
+
+it('can set a previous version setting for branch', async () => {
+ const { ui, user } = getPageObjects();
+ renderProjectBaselineApp({
+ featureList: [Feature.BranchSupport],
+ });
+ await ui.appIsLoaded();
+ await ui.setBranchPreviousVersionSetting('main');
+
+ expect(
+ within(byRole('table').get()).getByText('new_code_definition.previous_version')
+ ).toBeInTheDocument();
+
+ await user.click(await ui.branchActionsButton('main').find());
+
+ expect(ui.resetToDefaultButton.get()).toBeInTheDocument();
+ await user.click(ui.resetToDefaultButton.get());
+
+ expect(
+ first(within(byRole('table').get()).getAllByText('branch_list.default_setting'))
+ ).toBeInTheDocument();
+});
+
+it('can set a number of days setting for branch', async () => {
+ const { ui } = getPageObjects();
+ renderProjectBaselineApp({
+ featureList: [Feature.BranchSupport],
+ });
+ await ui.appIsLoaded();
+
+ await ui.setBranchNumberOfDaysSetting('main', '15');
+
+ expect(
+ within(byRole('table').get()).getByText('new_code_definition.number_days: 15')
+ ).toBeInTheDocument();
+});
+
+it('cannot set a specific analysis setting for branch', async () => {
+ const { ui } = getPageObjects();
+ codePeriodsMock.setListBranchesNewCode([
+ mockNewCodePeriodBranch({
+ branchKey: 'main',
+ type: NewCodeDefinitionType.SpecificAnalysis,
+ value: 'analysis_id',
+ }),
+ ]);
+ renderProjectBaselineApp({
+ featureList: [Feature.BranchSupport],
+ });
+ await ui.appIsLoaded();
+
+ await ui.openBranchSettingModal('main');
+
+ expect(ui.specificAnalysisRadio.get()).toBeChecked();
+ expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
+ expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
+
+ await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
+
+ expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
+ expect(last(ui.saveButton.getAll())).toBeDisabled();
+});
+
+it('can set a reference branch setting for branch', async () => {
+ const { ui } = getPageObjects();
+ renderProjectBaselineApp({
+ featureList: [Feature.BranchSupport],
+ });
+ await ui.appIsLoaded();
+
+ await ui.setBranchReferenceToBranchSetting('main', 'normal-branch');
+
+ expect(
+ byRole('table').byText('baseline.reference_branch: normal-branch').get()
+ ).toBeInTheDocument();
+});
+
+function renderProjectBaselineApp(context: RenderContext = {}, params?: string) {
+ return renderAppWithComponentContext(
+ 'baseline',
+ routes,
+ {
+ ...context,
+ navigateTo: params ? `baseline?id=my-project&${params}` : 'baseline?id=my-project',
+ },
+ {
+ component: mockComponent(),
+ }
+ );
+}
+
+function getPageObjects() {
+ const user = userEvent.setup();
+
+ const ui = {
+ pageHeading: byRole('heading', { name: 'project_baseline.page' }),
+ branchTableHeading: byText('branch_list.branch'),
+ branchListHeading: byRole('heading', { name: 'project_baseline.default_setting' }),
+ 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: /new_code_definition.previous_version.description/,
+ }),
+ numberDaysRadio: byRole('radio', { name: /new_code_definition.number_days.description/ }),
+ numberDaysInput: byRole('spinbutton'),
+ referenceBranchRadio: byRole('radio', { name: /baseline.reference_branch.description/ }),
+ chooseBranchSelect: byRole('combobox', { name: 'baseline.reference_branch.choose' }),
+ specificAnalysisRadio: byRole('radio', { name: /baseline.specific_analysis.description/ }),
+ specificAnalysisWarning: byText('baseline.specific_analysis.compliance_warning.title'),
+ analysisFromSelect: byRole('combobox', { name: 'baseline.analysis_from' }),
+ analysisListItem: byRole('radio', { name: /baseline.branch_analyses.analysis_for_x/ }),
+ saveButton: byRole('button', { name: 'save' }),
+ cancelButton: byRole('button', { name: 'cancel' }),
+ branchActionsButton: (branch: string) =>
+ byRole('button', { name: `branch_list.show_actions_for_x.${branch}` }),
+ editButton: byRole('button', { name: 'edit' }),
+ resetToDefaultButton: byRole('button', { name: 'reset_to_default' }),
+ saved: byText('settings.state.saved'),
+ complianceWarningAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
+ complianceWarning: byText('new_code_definition.compliance.warning.explanation'),
+ };
+
+ async function appIsLoaded() {
+ expect(await ui.pageHeading.find()).toBeInTheDocument();
+ }
+
+ async function setPreviousVersionSetting() {
+ await user.click(ui.specificSettingRadio.get());
+ await user.click(ui.previousVersionRadio.get());
+ }
+
+ async function setBranchPreviousVersionSetting(branch: string) {
+ await openBranchSettingModal(branch);
+ await user.click(last(ui.previousVersionRadio.getAll()) as HTMLElement);
+ await user.click(last(ui.saveButton.getAll()) as HTMLElement);
+ }
+
+ async function setNumberDaysSetting(value: string) {
+ await user.click(ui.specificSettingRadio.get());
+ await user.click(ui.numberDaysRadio.get());
+ await user.clear(ui.numberDaysInput.get());
+ await user.type(ui.numberDaysInput.get(), value);
+ }
+
+ async function setBranchNumberOfDaysSetting(branch: string, value: string) {
+ await openBranchSettingModal(branch);
+ await user.click(last(ui.numberDaysRadio.getAll()) as HTMLElement);
+ await user.clear(ui.numberDaysInput.get());
+ await user.type(ui.numberDaysInput.get(), value);
+ await user.click(last(ui.saveButton.getAll()) as HTMLElement);
+ }
+
+ async function setReferenceBranchSetting(branch: string) {
+ await user.click(ui.specificSettingRadio.get());
+ await user.click(ui.referenceBranchRadio.get());
+ await selectEvent.select(ui.chooseBranchSelect.get(), branch);
+ }
+
+ async function setBranchReferenceToBranchSetting(branch: string, branchRef: string) {
+ await openBranchSettingModal(branch);
+ await user.click(last(ui.referenceBranchRadio.getAll()) as HTMLElement);
+ await selectEvent.select(ui.chooseBranchSelect.get(), branchRef);
+ await user.click(last(ui.saveButton.getAll()) as HTMLElement);
+ }
+
+ async function openBranchSettingModal(branch: string) {
+ await user.click(await ui.branchActionsButton(branch).find());
+ await user.click(ui.editButton.get());
+ }
+
+ return {
+ ui: {
+ ...ui,
+ appIsLoaded,
+ setNumberDaysSetting,
+ setPreviousVersionSetting,
+ setReferenceBranchSetting,
+ setBranchPreviousVersionSetting,
+ setBranchNumberOfDaysSetting,
+ setBranchReferenceToBranchSetting,
+ openBranchSettingModal,
+ },
+ user,
+ };
+}
--- /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 { NewCodeDefinitionType } from '../../../../types/new-code-definition';
+import { getSettingValue, validateSetting } from '../../utils';
+
+describe('getSettingValue', () => {
+ const state = {
+ analysis: 'analysis',
+ days: '35',
+ referenceBranch: 'branch-4.2',
+ };
+
+ it('should work for Days', () => {
+ expect(getSettingValue({ ...state, type: NewCodeDefinitionType.NumberOfDays })).toBe(
+ state.days
+ );
+ });
+
+ it('should work for Analysis', () => {
+ expect(getSettingValue({ ...state, type: NewCodeDefinitionType.SpecificAnalysis })).toBe(
+ state.analysis
+ );
+ });
+
+ it('should work for Previous version', () => {
+ expect(
+ getSettingValue({ ...state, type: NewCodeDefinitionType.PreviousVersion })
+ ).toBeUndefined();
+ });
+
+ it('should work for Reference branch', () => {
+ expect(getSettingValue({ ...state, type: NewCodeDefinitionType.ReferenceBranch })).toBe(
+ state.referenceBranch
+ );
+ });
+});
+
+describe('validateSettings', () => {
+ it('should validate at branch level', () => {
+ expect(validateSetting({ days: '' })).toEqual(false);
+ expect(
+ validateSetting({
+ days: '12',
+ selected: NewCodeDefinitionType.NumberOfDays,
+ })
+ ).toEqual(true);
+ expect(
+ validateSetting({
+ days: 'nope',
+ selected: NewCodeDefinitionType.NumberOfDays,
+ })
+ ).toEqual(false);
+ expect(
+ validateSetting({
+ days: '',
+ selected: NewCodeDefinitionType.SpecificAnalysis,
+ })
+ ).toEqual(false);
+ expect(
+ validateSetting({
+ days: '',
+ referenceBranch: 'master',
+ selected: NewCodeDefinitionType.ReferenceBranch,
+ })
+ ).toEqual(true);
+ expect(
+ validateSetting({
+ days: '',
+ referenceBranch: '',
+ selected: NewCodeDefinitionType.ReferenceBranch,
+ })
+ ).toEqual(false);
+ });
+
+ it('should validate at project level', () => {
+ expect(validateSetting({ days: '', overrideGeneralSetting: false })).toEqual(true);
+ expect(
+ validateSetting({
+ selected: NewCodeDefinitionType.PreviousVersion,
+ days: '',
+ overrideGeneralSetting: true,
+ })
+ ).toEqual(true);
+ expect(
+ validateSetting({
+ selected: NewCodeDefinitionType.NumberOfDays,
+ days: '',
+ overrideGeneralSetting: true,
+ })
+ ).toEqual(false);
+ expect(
+ validateSetting({
+ selected: NewCodeDefinitionType.NumberOfDays,
+ days: '12',
+ overrideGeneralSetting: true,
+ })
+ ).toEqual(true);
+ });
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { Route } from 'react-router-dom';
+import ProjectNewCodeDefinitionApp from './components/ProjectNewCodeDefinitionApp';
+
+const routes = () => <Route path="baseline" element={<ProjectNewCodeDefinitionApp />} />;
+
+export default routes;
--- /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.
+ */
+.project-baseline {
+ padding: calc(4 * var(--gridSize));
+}
+
+.project-baseline-setting {
+ display: flex;
+ flex-direction: column;
+ max-height: 60vh;
+ padding-top: 2px;
+}
+
+.branch-baseline-selector > hr {
+ margin: 0 calc(-4 * var(--gridSize)) calc(4 * var(--gridSize));
+}
+
+.branch-baseline-setting-modal {
+ min-height: 450px;
+ display: flex;
+ flex-direction: column;
+}
+
+.branch-analysis-list-wrapper {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ position: relative;
+ min-height: 200px;
+}
+
+.branch-analysis-list {
+ overflow-y: auto;
+ padding-left: 12px;
+ padding-right: 15px;
+ min-height: 50px;
+}
+
+.branch-analysis-list > ul {
+ padding-top: 18px;
+}
+
+.branch-analysis-date {
+ margin-bottom: 16px;
+ font-size: 15px;
+ font-weight: bold;
+}
+
+.branch-analysis-day {
+ margin-top: var(--gridSize);
+ margin-bottom: calc(3 * var(--gridSize));
+}
+
+.branch-analysis {
+ display: flex;
+ justify-content: space-between;
+ padding: var(--gridSize);
+ border-top: 1px solid var(--barBorderColor);
+ border-bottom: 1px solid var(--barBorderColor);
+ cursor: not-allowed;
+}
+
+.branch-analysis + .branch-analysis {
+ border-top: none;
+}
+
+.branch-analysis:hover {
+ background-color: var(--disableGrayBg);
+}
+
+.branch-analysis > .project-activity-events {
+ flex: 1 0 50%;
+}
+
+.branch-analysis-time {
+ width: 150px;
+}
+
+.branch-analysis-version-badge {
+ margin-left: -12px;
+ padding-top: var(--gridSize);
+ padding-bottom: var(--gridSize);
+ background-color: white;
+}
+
+.branch-analysis-version-badge.sticky + .branch-analysis-days-list {
+ padding-top: 36px;
+}
+
+.branch-analysis-version-badge.sticky,
+.branch-analysis-version-badge.first {
+ position: absolute;
+ top: 1px;
+ left: 13px;
+ right: 16px;
+ padding-top: calc(3 * var(--gridSize));
+ z-index: var(--belowNormalZIndex);
+}
+
+.branch-analysis-version-badge .badge {
+ max-width: 385px;
+ border-radius: 0 2px 2px 0;
+ font-weight: bold;
+ font-size: var(--smallFontSize);
+ letter-spacing: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.branch-setting-warning {
+ background-color: var(--alertBackgroundWarning) !important;
+}
+
+.project-activity-event-icon.VERSION {
+ color: var(--blue);
+}
+
+.project-activity-event-icon.QUALITY_GATE {
+ color: var(--purple);
+}
+
+.project-activity-event-icon.QUALITY_PROFILE {
+ color: #cccccc;
+}
+
+.project-activity-event-icon.DEFINITION_CHANGE {
+ color: #33a759;
+}
+
+.project-activity-event-icon.OTHER {
+ color: #442d1b;
+}
--- /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 { isNewCodeDefinitionCompliant } from '../../helpers/new-code-definition';
+import { NewCodeDefinitionType } from '../../types/new-code-definition';
+
+export function getSettingValue({
+ analysis,
+ days,
+ referenceBranch,
+ type,
+}: {
+ analysis?: string;
+ days?: string;
+ referenceBranch?: string;
+ type?: NewCodeDefinitionType;
+}) {
+ switch (type) {
+ case NewCodeDefinitionType.NumberOfDays:
+ return days;
+ case NewCodeDefinitionType.ReferenceBranch:
+ return referenceBranch;
+ case NewCodeDefinitionType.SpecificAnalysis:
+ return analysis;
+ default:
+ return undefined;
+ }
+}
+
+export function validateSetting(state: {
+ days: string;
+ overrideGeneralSetting?: boolean;
+ referenceBranch?: string;
+ selected?: NewCodeDefinitionType;
+}) {
+ const { days, overrideGeneralSetting, referenceBranch = '', selected } = state;
+
+ return (
+ overrideGeneralSetting === false ||
+ (!!selected &&
+ isNewCodeDefinitionCompliant({
+ type: selected,
+ value: days,
+ }) &&
+ (selected !== NewCodeDefinitionType.ReferenceBranch || referenceBranch.length > 0))
+ );
+}
NEW_CODE_PERIOD_CATEGORY,
PULL_REQUEST_DECORATION_BINDING_CATEGORY,
} from '../constants';
-import AlmIntegration from './almIntegration/AlmIntegration';
import { AnalysisScope } from './AnalysisScope';
-import Authentication from './authentication/Authentication';
import Languages from './Languages';
-import NewCodePeriod from './NewCodePeriod';
+import NewCodeDefinition from './NewCodeDefinition';
+import AlmIntegration from './almIntegration/AlmIntegration';
+import Authentication from './authentication/Authentication';
import PullRequestDecorationBinding from './pullRequestDecorationBinding/PRDecorationBinding';
export interface AdditionalCategoryComponentProps {
}
function getNewCodePeriodComponent() {
- return <NewCodePeriod />;
+ return <NewCodeDefinition />;
}
function getAnalysisScopeComponent(props: AdditionalCategoryComponentProps) {
--- /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 { getNewCodeDefinition, setNewCodeDefinition } from '../../../api/newCodeDefinition';
+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 Spinner from '../../../components/ui/Spinner';
+import { translate } from '../../../helpers/l10n';
+import {
+ getNumberOfDaysDefaultValue,
+ isNewCodeDefinitionCompliant,
+} from '../../../helpers/new-code-definition';
+import { NewCodeDefinitionType } from '../../../types/new-code-definition';
+
+interface State {
+ currentSetting?: NewCodeDefinitionType;
+ days: string;
+ previousNonCompliantValue?: string;
+ ncdUpdatedAt?: number;
+ loading: boolean;
+ currentSettingValue?: string;
+ isChanged: boolean;
+ saving: boolean;
+ selected?: NewCodeDefinitionType;
+ success: boolean;
+}
+
+export default class NewCodeDefinition extends React.PureComponent<{}, State> {
+ mounted = false;
+ state: State = {
+ loading: true,
+ days: getNumberOfDaysDefaultValue(),
+ isChanged: false,
+ saving: false,
+ success: false,
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchNewCodePeriodSetting();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchNewCodePeriodSetting() {
+ getNewCodeDefinition()
+ .then(({ type, value, previousNonCompliantValue, updatedAt }) => {
+ this.setState(({ days }) => ({
+ currentSetting: type,
+ days: type === NewCodeDefinitionType.NumberOfDays ? String(value) : days,
+ loading: false,
+ currentSettingValue: value,
+ selected: type,
+ previousNonCompliantValue,
+ ncdUpdatedAt: updatedAt,
+ }));
+ })
+ .catch(() => {
+ this.setState({ loading: false });
+ });
+ }
+
+ onSelectDays = (days: string) => {
+ this.setState({ days, success: false, isChanged: true });
+ };
+
+ onSelectSetting = (selected: NewCodeDefinitionType) => {
+ this.setState((currentState) => ({
+ selected,
+ success: false,
+ isChanged: selected !== currentState.selected,
+ }));
+ };
+
+ onCancel = () => {
+ this.setState(({ currentSetting, currentSettingValue, days }) => ({
+ isChanged: false,
+ selected: currentSetting,
+ days:
+ currentSetting === NewCodeDefinitionType.NumberOfDays ? String(currentSettingValue) : days,
+ }));
+ };
+
+ onSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+ e.preventDefault();
+
+ const { days, selected } = this.state;
+
+ const type = selected;
+ const value = type === NewCodeDefinitionType.NumberOfDays ? days : undefined;
+
+ this.setState({ saving: true, success: false });
+ setNewCodeDefinition({
+ type: type as NewCodeDefinitionType,
+ value,
+ }).then(
+ () => {
+ if (this.mounted) {
+ this.setState({
+ saving: false,
+ currentSetting: type,
+ currentSettingValue: value || undefined,
+ isChanged: false,
+ success: true,
+ });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({
+ saving: false,
+ });
+ }
+ }
+ );
+ };
+
+ render() {
+ const {
+ currentSetting,
+ days,
+ previousNonCompliantValue,
+ ncdUpdatedAt,
+ loading,
+ isChanged,
+ currentSettingValue,
+ saving,
+ selected,
+ success,
+ } = this.state;
+
+ const isValid =
+ selected !== NewCodeDefinitionType.NumberOfDays ||
+ isNewCodeDefinitionCompliant({ type: NewCodeDefinitionType.NumberOfDays, value: days });
+
+ return (
+ <>
+ <h2
+ className="settings-sub-category-name settings-definition-name"
+ title={translate('settings.new_code_period.title')}
+ >
+ {translate('settings.new_code_period.title')}
+ </h2>
+
+ <ul className="settings-sub-categories-list">
+ <li>
+ <ul className="settings-definitions-list">
+ <li>
+ <div className="settings-definition">
+ <div className="settings-definition-left">
+ <div className="small">
+ <p className="sw-mb-2">
+ {translate('settings.new_code_period.description0')}
+ </p>
+ <p className="sw-mb-2">
+ {translate('settings.new_code_period.description1')}
+ </p>
+ <p className="sw-mb-2">
+ {translate('settings.new_code_period.description2')}
+ </p>
+
+ <p className="sw-mb-2">
+ <FormattedMessage
+ defaultMessage={translate('settings.new_code_period.description3')}
+ id="settings.new_code_period.description3"
+ values={{
+ link: (
+ <DocLink to="/project-administration/defining-new-code/">
+ {translate('settings.new_code_period.description3.link')}
+ </DocLink>
+ ),
+ }}
+ />
+ </p>
+
+ <p className="sw-mt-4">
+ <strong>{translate('settings.new_code_period.question')}</strong>
+ </p>
+ </div>
+ </div>
+
+ <div className="settings-definition-right">
+ <Spinner loading={loading}>
+ <form onSubmit={this.onSubmit}>
+ <NewCodeDefinitionPreviousVersionOption
+ isDefault
+ onSelect={this.onSelectSetting}
+ selected={selected === NewCodeDefinitionType.PreviousVersion}
+ />
+ <NewCodeDefinitionDaysOption
+ className="spacer-top sw-mb-4"
+ days={days}
+ previousNonCompliantValue={previousNonCompliantValue}
+ updatedAt={ncdUpdatedAt}
+ isChanged={isChanged}
+ isValid={isValid}
+ onChangeDays={this.onSelectDays}
+ onSelect={this.onSelectSetting}
+ selected={selected === NewCodeDefinitionType.NumberOfDays}
+ />
+ <NewCodeDefinitionWarning
+ newCodeDefinitionType={currentSetting}
+ newCodeDefinitionValue={currentSettingValue}
+ isBranchSupportEnabled={undefined}
+ level="global"
+ />
+ {isChanged && (
+ <div className="big-spacer-top">
+ <p className="spacer-bottom">
+ {translate('baseline.next_analysis_notice')}
+ </p>
+ <Spinner className="spacer-right" loading={saving} />
+ <SubmitButton disabled={saving || !isValid}>
+ {translate('save')}
+ </SubmitButton>
+ <ResetButtonLink className="spacer-left" onClick={this.onCancel}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </div>
+ )}
+ {!saving && !loading && success && (
+ <div className="big-spacer-top">
+ <span className="text-success">
+ <AlertSuccessIcon className="spacer-right" />
+ {translate('settings.state.saved')}
+ </span>
+ </div>
+ )}
+ </form>
+ </Spinner>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </>
+ );
+ }
+}
+++ /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 { 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 Spinner from '../../../components/ui/Spinner';
-import { translate } from '../../../helpers/l10n';
-import {
- getNumberOfDaysDefaultValue,
- isNewCodeDefinitionCompliant,
-} from '../../../helpers/new-code-definition';
-import { NewCodeDefinitionType } from '../../../types/new-code-definition';
-
-interface State {
- currentSetting?: NewCodeDefinitionType;
- days: string;
- previousNonCompliantValue?: string;
- ncdUpdatedAt?: number;
- loading: boolean;
- currentSettingValue?: string;
- isChanged: boolean;
- saving: boolean;
- selected?: NewCodeDefinitionType;
- success: boolean;
-}
-
-export default class NewCodePeriod extends React.PureComponent<{}, State> {
- mounted = false;
- state: State = {
- loading: true,
- days: getNumberOfDaysDefaultValue(),
- isChanged: false,
- saving: false,
- success: false,
- };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchNewCodePeriodSetting();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchNewCodePeriodSetting() {
- getNewCodePeriod()
- .then(({ type, value, previousNonCompliantValue, updatedAt }) => {
- this.setState(({ days }) => ({
- currentSetting: type,
- days: type === NewCodeDefinitionType.NumberOfDays ? String(value) : days,
- loading: false,
- currentSettingValue: value,
- selected: type,
- previousNonCompliantValue,
- ncdUpdatedAt: updatedAt,
- }));
- })
- .catch(() => {
- this.setState({ loading: false });
- });
- }
-
- onSelectDays = (days: string) => {
- this.setState({ days, success: false, isChanged: true });
- };
-
- onSelectSetting = (selected: NewCodeDefinitionType) => {
- this.setState((currentState) => ({
- selected,
- success: false,
- isChanged: selected !== currentState.selected,
- }));
- };
-
- onCancel = () => {
- this.setState(({ currentSetting, currentSettingValue, days }) => ({
- isChanged: false,
- selected: currentSetting,
- days:
- currentSetting === NewCodeDefinitionType.NumberOfDays ? String(currentSettingValue) : days,
- }));
- };
-
- onSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
- e.preventDefault();
-
- const { days, selected } = this.state;
-
- const type = selected;
- const value = type === NewCodeDefinitionType.NumberOfDays ? days : undefined;
-
- this.setState({ saving: true, success: false });
- setNewCodePeriod({
- type: type as NewCodeDefinitionType,
- value,
- }).then(
- () => {
- if (this.mounted) {
- this.setState({
- saving: false,
- currentSetting: type,
- currentSettingValue: value || undefined,
- isChanged: false,
- success: true,
- });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({
- saving: false,
- });
- }
- }
- );
- };
-
- render() {
- const {
- currentSetting,
- days,
- previousNonCompliantValue,
- ncdUpdatedAt,
- loading,
- isChanged,
- currentSettingValue,
- saving,
- selected,
- success,
- } = this.state;
-
- const isValid =
- selected !== NewCodeDefinitionType.NumberOfDays ||
- isNewCodeDefinitionCompliant({ type: NewCodeDefinitionType.NumberOfDays, value: days });
-
- return (
- <>
- <h2
- className="settings-sub-category-name settings-definition-name"
- title={translate('settings.new_code_period.title')}
- >
- {translate('settings.new_code_period.title')}
- </h2>
-
- <ul className="settings-sub-categories-list">
- <li>
- <ul className="settings-definitions-list">
- <li>
- <div className="settings-definition">
- <div className="settings-definition-left">
- <div className="small">
- <p className="sw-mb-2">
- {translate('settings.new_code_period.description0')}
- </p>
- <p className="sw-mb-2">
- {translate('settings.new_code_period.description1')}
- </p>
- <p className="sw-mb-2">
- {translate('settings.new_code_period.description2')}
- </p>
-
- <p className="sw-mb-2">
- <FormattedMessage
- defaultMessage={translate('settings.new_code_period.description3')}
- id="settings.new_code_period.description3"
- values={{
- link: (
- <DocLink to="/project-administration/defining-new-code/">
- {translate('settings.new_code_period.description3.link')}
- </DocLink>
- ),
- }}
- />
- </p>
-
- <p className="sw-mt-4">
- <strong>{translate('settings.new_code_period.question')}</strong>
- </p>
- </div>
- </div>
-
- <div className="settings-definition-right">
- <Spinner loading={loading}>
- <form onSubmit={this.onSubmit}>
- <NewCodeDefinitionPreviousVersionOption
- isDefault
- onSelect={this.onSelectSetting}
- selected={selected === NewCodeDefinitionType.PreviousVersion}
- />
- <NewCodeDefinitionDaysOption
- className="spacer-top sw-mb-4"
- days={days}
- previousNonCompliantValue={previousNonCompliantValue}
- updatedAt={ncdUpdatedAt}
- isChanged={isChanged}
- isValid={isValid}
- onChangeDays={this.onSelectDays}
- onSelect={this.onSelectSetting}
- selected={selected === NewCodeDefinitionType.NumberOfDays}
- />
- <NewCodeDefinitionWarning
- newCodeDefinitionType={currentSetting}
- newCodeDefinitionValue={currentSettingValue}
- isBranchSupportEnabled={undefined}
- level="global"
- />
- {isChanged && (
- <div className="big-spacer-top">
- <p className="spacer-bottom">
- {translate('baseline.next_analysis_notice')}
- </p>
- <Spinner className="spacer-right" loading={saving} />
- <SubmitButton disabled={saving || !isValid}>
- {translate('save')}
- </SubmitButton>
- <ResetButtonLink className="spacer-left" onClick={this.onCancel}>
- {translate('cancel')}
- </ResetButtonLink>
- </div>
- )}
- {!saving && !loading && success && (
- <div className="big-spacer-top">
- <span className="text-success">
- <AlertSuccessIcon className="spacer-right" />
- {translate('settings.state.saved')}
- </span>
- </div>
- )}
- </form>
- </Spinner>
- </div>
- </div>
- </li>
- </ul>
- </li>
- </ul>
- </>
- );
- }
-}
--- /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 { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { MessageTypes } from '../../../../api/messages';
+import MessagesServiceMock from '../../../../api/mocks/MessagesServiceMock';
+import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
+import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
+import NewCodeDefinition from '../NewCodeDefinition';
+
+let newCodeMock: NewCodeDefinitionServiceMock;
+let messagesMock: MessagesServiceMock;
+
+beforeAll(() => {
+ newCodeMock = new NewCodeDefinitionServiceMock();
+ messagesMock = new MessagesServiceMock();
+});
+
+afterEach(() => {
+ newCodeMock.reset();
+ messagesMock.reset();
+});
+
+const ui = {
+ newCodeTitle: byRole('heading', { name: 'settings.new_code_period.title' }),
+ savedMsg: byText('settings.state.saved'),
+ 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('spinbutton') /* spinbutton is the default role for a number input */,
+ saveButton: byRole('button', { name: 'save' }),
+ cancelButton: byRole('button', { name: 'cancel' }),
+ ncdAutoUpdateMessage: byText(/new_code_definition.auto_update.global.page.message/),
+ ncdAutoUpdateMessageDismiss: byLabelText('alert.dismiss'),
+};
+
+it('renders and behaves as expected', async () => {
+ const user = userEvent.setup();
+ renderNewCodePeriod();
+
+ expect(await ui.newCodeTitle.find()).toBeInTheDocument();
+ // Previous version should be checked by default
+ expect(ui.prevVersionRadio.get()).toBeChecked();
+
+ // Can select number of days
+ await user.click(ui.daysNumberRadio.get());
+ expect(ui.daysNumberRadio.get()).toBeChecked();
+
+ // Save should be disabled for zero
+ expect(ui.daysInput.get()).toHaveValue(30);
+ await user.clear(ui.daysInput.get());
+ await user.type(ui.daysInput.get(), '0');
+ expect(await ui.saveButton.find()).toBeDisabled();
+
+ // Save should not appear at all for NaN
+ await user.clear(ui.daysInput.get());
+ await user.type(ui.daysInput.get(), 'asdas');
+ expect(ui.saveButton.query()).toBeDisabled();
+
+ // Save enabled for valid days number
+ await user.clear(ui.daysInput.get());
+ await user.type(ui.daysInput.get(), '10');
+ expect(ui.saveButton.get()).toBeEnabled();
+
+ // Can cancel action
+ await user.click(ui.cancelButton.get());
+ expect(ui.prevVersionRadio.get()).toBeChecked();
+
+ // Can save change
+ await user.click(ui.daysNumberRadio.get());
+ await user.clear(ui.daysInput.get());
+ await user.type(ui.daysInput.get(), '10');
+ await user.click(ui.saveButton.get());
+ expect(ui.savedMsg.get()).toBeInTheDocument();
+
+ await user.click(ui.prevVersionRadio.get());
+ await user.click(ui.cancelButton.get());
+ await user.click(ui.prevVersionRadio.get());
+ await user.click(ui.saveButton.get());
+ expect(ui.savedMsg.get()).toBeInTheDocument();
+});
+
+it('renders and behaves properly when the current value is not compliant', async () => {
+ const user = userEvent.setup();
+ newCodeMock.setNewCodePeriod({ type: NewCodeDefinitionType.NumberOfDays, value: '91' });
+ renderNewCodePeriod();
+
+ expect(await ui.newCodeTitle.find()).toBeInTheDocument();
+ expect(ui.daysNumberRadio.get()).toBeChecked();
+ expect(ui.daysInput.get()).toHaveValue(91);
+
+ // Should warn about non compliant value
+ expect(screen.getByText('baseline.number_days.compliance_warning.title')).toBeInTheDocument();
+
+ await user.clear(ui.daysInput.get());
+ await user.type(ui.daysInput.get(), '92');
+
+ expect(ui.daysNumberErrorMessage.get()).toBeInTheDocument();
+});
+
+it('displays information message when NCD is automatically updated', async () => {
+ newCodeMock.setNewCodePeriod({
+ type: NewCodeDefinitionType.NumberOfDays,
+ value: '90',
+ previousNonCompliantValue: '120',
+ updatedAt: 1692279521904,
+ });
+ renderNewCodePeriod();
+
+ expect(await ui.ncdAutoUpdateMessage.find()).toBeVisible();
+});
+
+it('dismisses information message when NCD is automatically updated', async () => {
+ newCodeMock.setNewCodePeriod({
+ type: NewCodeDefinitionType.NumberOfDays,
+ value: '90',
+ previousNonCompliantValue: '120',
+ updatedAt: 1692279521904,
+ });
+ renderNewCodePeriod();
+
+ expect(await ui.ncdAutoUpdateMessage.find()).toBeVisible();
+
+ const user = userEvent.setup();
+ await user.click(ui.ncdAutoUpdateMessageDismiss.get());
+
+ expect(ui.ncdAutoUpdateMessage.query()).not.toBeInTheDocument();
+});
+
+it('does not display information message when NCD is automatically updated if message is already dismissed', () => {
+ newCodeMock.setNewCodePeriod({
+ type: NewCodeDefinitionType.NumberOfDays,
+ value: '90',
+ previousNonCompliantValue: '120',
+ updatedAt: 1692279521904,
+ });
+ messagesMock.setMessageDismissed({ messageType: MessageTypes.GlobalNcdPage90 });
+ renderNewCodePeriod();
+
+ expect(ui.ncdAutoUpdateMessage.query()).not.toBeInTheDocument();
+});
+
+function renderNewCodePeriod() {
+ return renderComponent(<NewCodeDefinition />);
+}
+++ /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 { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-import { MessageTypes } from '../../../../api/messages';
-import MessagesServiceMock from '../../../../api/mocks/MessagesServiceMock';
-import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
-import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
-import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
-import NewCodePeriod from '../NewCodePeriod';
-
-let newCodeMock: NewCodePeriodsServiceMock;
-let messagesMock: MessagesServiceMock;
-
-beforeAll(() => {
- newCodeMock = new NewCodePeriodsServiceMock();
- messagesMock = new MessagesServiceMock();
-});
-
-afterEach(() => {
- newCodeMock.reset();
- messagesMock.reset();
-});
-
-const ui = {
- newCodeTitle: byRole('heading', { name: 'settings.new_code_period.title' }),
- savedMsg: byText('settings.state.saved'),
- 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('spinbutton') /* spinbutton is the default role for a number input */,
- saveButton: byRole('button', { name: 'save' }),
- cancelButton: byRole('button', { name: 'cancel' }),
- ncdAutoUpdateMessage: byText(/new_code_definition.auto_update.global.page.message/),
- ncdAutoUpdateMessageDismiss: byLabelText('alert.dismiss'),
-};
-
-it('renders and behaves as expected', async () => {
- const user = userEvent.setup();
- renderNewCodePeriod();
-
- expect(await ui.newCodeTitle.find()).toBeInTheDocument();
- // Previous version should be checked by default
- expect(ui.prevVersionRadio.get()).toBeChecked();
-
- // Can select number of days
- await user.click(ui.daysNumberRadio.get());
- expect(ui.daysNumberRadio.get()).toBeChecked();
-
- // Save should be disabled for zero
- expect(ui.daysInput.get()).toHaveValue(30);
- await user.clear(ui.daysInput.get());
- await user.type(ui.daysInput.get(), '0');
- expect(await ui.saveButton.find()).toBeDisabled();
-
- // Save should not appear at all for NaN
- await user.clear(ui.daysInput.get());
- await user.type(ui.daysInput.get(), 'asdas');
- expect(ui.saveButton.query()).toBeDisabled();
-
- // Save enabled for valid days number
- await user.clear(ui.daysInput.get());
- await user.type(ui.daysInput.get(), '10');
- expect(ui.saveButton.get()).toBeEnabled();
-
- // Can cancel action
- await user.click(ui.cancelButton.get());
- expect(ui.prevVersionRadio.get()).toBeChecked();
-
- // Can save change
- await user.click(ui.daysNumberRadio.get());
- await user.clear(ui.daysInput.get());
- await user.type(ui.daysInput.get(), '10');
- await user.click(ui.saveButton.get());
- expect(ui.savedMsg.get()).toBeInTheDocument();
-
- await user.click(ui.prevVersionRadio.get());
- await user.click(ui.cancelButton.get());
- await user.click(ui.prevVersionRadio.get());
- await user.click(ui.saveButton.get());
- expect(ui.savedMsg.get()).toBeInTheDocument();
-});
-
-it('renders and behaves properly when the current value is not compliant', async () => {
- const user = userEvent.setup();
- newCodeMock.setNewCodePeriod({ type: NewCodeDefinitionType.NumberOfDays, value: '91' });
- renderNewCodePeriod();
-
- expect(await ui.newCodeTitle.find()).toBeInTheDocument();
- expect(ui.daysNumberRadio.get()).toBeChecked();
- expect(ui.daysInput.get()).toHaveValue(91);
-
- // Should warn about non compliant value
- expect(screen.getByText('baseline.number_days.compliance_warning.title')).toBeInTheDocument();
-
- await user.clear(ui.daysInput.get());
- await user.type(ui.daysInput.get(), '92');
-
- expect(ui.daysNumberErrorMessage.get()).toBeInTheDocument();
-});
-
-it('displays information message when NCD is automatically updated', async () => {
- newCodeMock.setNewCodePeriod({
- type: NewCodeDefinitionType.NumberOfDays,
- value: '90',
- previousNonCompliantValue: '120',
- updatedAt: 1692279521904,
- });
- renderNewCodePeriod();
-
- expect(await ui.ncdAutoUpdateMessage.find()).toBeVisible();
-});
-
-it('dismisses information message when NCD is automatically updated', async () => {
- newCodeMock.setNewCodePeriod({
- type: NewCodeDefinitionType.NumberOfDays,
- value: '90',
- previousNonCompliantValue: '120',
- updatedAt: 1692279521904,
- });
- renderNewCodePeriod();
-
- expect(await ui.ncdAutoUpdateMessage.find()).toBeVisible();
-
- const user = userEvent.setup();
- await user.click(ui.ncdAutoUpdateMessageDismiss.get());
-
- expect(ui.ncdAutoUpdateMessage.query()).not.toBeInTheDocument();
-});
-
-it('does not display information message when NCD is automatically updated if message is already dismissed', () => {
- newCodeMock.setNewCodePeriod({
- type: NewCodeDefinitionType.NumberOfDays,
- value: '90',
- previousNonCompliantValue: '120',
- updatedAt: 1692279521904,
- });
- messagesMock.setMessageDismissed({ messageType: MessageTypes.GlobalNcdPage90 });
- renderNewCodePeriod();
-
- expect(ui.ncdAutoUpdateMessage.query()).not.toBeInTheDocument();
-});
-
-function renderNewCodePeriod() {
- return renderComponent(<NewCodePeriod />);
-}
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { MessageTypes, checkMessageDismissed, setMessageDismissed } from '../../api/messages';
-import { getNewCodePeriod } from '../../api/newCodePeriod';
+import { getNewCodeDefinition } from '../../api/newCodeDefinition';
import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
import { NEW_CODE_PERIOD_CATEGORY } from '../../apps/settings/constants';
useEffect(() => {
async function fetchNewCodeDefinition() {
- const newCodeDefinition = await getNewCodePeriod(
+ const newCodeDefinition = await getNewCodeDefinition(
component && {
project: component.key,
}
} from 'design-system';
import { noop } from 'lodash';
import * as React from 'react';
-import { getNewCodePeriod } from '../../api/newCodePeriod';
+import { getNewCodeDefinition } from '../../api/newCodeDefinition';
import { translate } from '../../helpers/l10n';
import {
getNumberOfDaysDefaultValue,
React.useEffect(() => {
function fetchGlobalNcd() {
- getNewCodePeriod().then(setGlobalNcd, noop);
+ getNewCodeDefinition().then(setGlobalNcd, noop);
}
fetchGlobalNcd();
import { Route } from 'react-router-dom';
import { MessageTypes } from '../../../api/messages';
import MessagesServiceMock from '../../../api/mocks/MessagesServiceMock';
-import NewCodePeriodsServiceMock from '../../../api/mocks/NewCodePeriodsServiceMock';
+import NewCodeDefinitionServiceMock from '../../../api/mocks/NewCodeDefinitionServiceMock';
import { mockComponent } from '../../../helpers/mocks/component';
import { mockLoggedInUser } from '../../../helpers/testMocks';
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import { Component } from '../../../types/types';
import NCDAutoUpdateMessage from '../NCDAutoUpdateMessage';
-let newCodeDefinitionMock: NewCodePeriodsServiceMock;
+let newCodeDefinitionMock: NewCodeDefinitionServiceMock;
let messagesMock: MessagesServiceMock;
describe('Global NCD update notification banner', () => {
}
beforeAll(() => {
- newCodeDefinitionMock = new NewCodePeriodsServiceMock();
+ newCodeDefinitionMock = new NewCodeDefinitionServiceMock();
messagesMock = new MessagesServiceMock();
});
}
beforeAll(() => {
- newCodeDefinitionMock = new NewCodePeriodsServiceMock();
+ newCodeDefinitionMock = new NewCodeDefinitionServiceMock();
messagesMock = new MessagesServiceMock();
});