--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { mockGitlabProject } from '../../helpers/mocks/alm-integrations';
+import { GitlabProject } from '../../types/alm-integration';
+import {
+ checkPersonalAccessTokenIsValid,
+ getGitlabProjects,
+ setAlmPersonalAccessToken,
+} from '../alm-integrations';
+
+export default class AlmIntegrationsServiceMock {
+ almInstancePATMap: { [key: string]: boolean } = {};
+ gitlabProjects: GitlabProject[];
+ defaultAlmInstancePATMap: { [key: string]: boolean } = {
+ 'conf-final-1': false,
+ 'conf-final-2': true,
+ };
+
+ defaultGitlabProjects: GitlabProject[] = [
+ mockGitlabProject({
+ name: 'Gitlab project 1',
+ id: '1',
+ sqProjectKey: 'key',
+ sqProjectName: 'Gitlab project 1',
+ }),
+ mockGitlabProject({ name: 'Gitlab project 2', id: '2' }),
+ mockGitlabProject({ name: 'Gitlab project 3', id: '3' }),
+ ];
+
+ constructor() {
+ this.almInstancePATMap = cloneDeep(this.defaultAlmInstancePATMap);
+ this.gitlabProjects = cloneDeep(this.defaultGitlabProjects);
+ (checkPersonalAccessTokenIsValid as jest.Mock).mockImplementation(
+ this.checkPersonalAccessTokenIsValid
+ );
+ (setAlmPersonalAccessToken as jest.Mock).mockImplementation(this.setAlmPersonalAccessToken);
+ (getGitlabProjects as jest.Mock).mockImplementation(this.getGitlabProjects);
+ }
+
+ checkPersonalAccessTokenIsValid = (conf: string) => {
+ return Promise.resolve({ status: this.almInstancePATMap[conf] });
+ };
+
+ setAlmPersonalAccessToken = (conf: string) => {
+ this.almInstancePATMap[conf] = true;
+ return Promise.resolve();
+ };
+
+ getGitlabProjects = () => {
+ return Promise.resolve({
+ projects: this.gitlabProjects,
+ projectsPaging: {
+ pageIndex: 1,
+ pageSize: 30,
+ total: 3,
+ },
+ });
+ };
+
+ setGitlabProjects(gitlabProjects: GitlabProject[]) {
+ this.gitlabProjects = gitlabProjects;
+ }
+
+ reset = () => {
+ this.almInstancePATMap = cloneDeep(this.defaultAlmInstancePATMap);
+ this.gitlabProjects = cloneDeep(this.defaultGitlabProjects);
+ };
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { mockAlmSettingsInstance } from '../../helpers/mocks/alm-settings';
+import { AlmKeys, AlmSettingsInstance } from '../../types/alm-settings';
+import { getAlmSettings } from '../alm-settings';
+
+export default class AlmSettingsServiceMock {
+ almSettings: AlmSettingsInstance[];
+ defaultSetting: AlmSettingsInstance[] = [
+ mockAlmSettingsInstance({ key: 'conf-final-1', alm: AlmKeys.GitLab }),
+ mockAlmSettingsInstance({ key: 'conf-final-2', alm: AlmKeys.GitLab }),
+ ];
+
+ constructor() {
+ this.almSettings = cloneDeep(this.defaultSetting);
+ (getAlmSettings as jest.Mock).mockImplementation(this.getAlmSettingsHandler);
+ }
+
+ getAlmSettingsHandler = () => {
+ return Promise.resolve(this.almSettings);
+ };
+
+ reset = () => {
+ this.almSettings = cloneDeep(this.defaultSetting);
+ };
+}
import { getBaseUrl } from '../../../helpers/system';
import { AlmKeys } from '../../../types/alm-settings';
import { AppState } from '../../../types/appstate';
+import { ALLOWED_MULTIPLE_CONFIGS } from './constants';
import { CreateProjectModes } from './types';
export interface CreateProjectModeSelectionProps {
const DEFAULT_ICON_SIZE = 50;
+function getErrorMessage(
+ hasTooManyConfig: boolean,
+ hasConfig: boolean,
+ canAdmin: boolean | undefined,
+ alm: AlmKeys
+) {
+ if (hasTooManyConfig) {
+ return translateWithParameters(
+ 'onboarding.create_project.too_many_alm_instances_X',
+ translate('alm', alm)
+ );
+ } else if (!hasConfig) {
+ return canAdmin
+ ? translate('onboarding.create_project.alm_not_configured.admin')
+ : translate('onboarding.create_project.alm_not_configured');
+ }
+}
+
+function getMode(
+ isBitbucketOption: boolean,
+ hasBitbucketCloudConf: boolean,
+ mode: CreateProjectModes
+) {
+ return isBitbucketOption && hasBitbucketCloudConf ? CreateProjectModes.BitbucketCloud : mode;
+}
+
function renderAlmOption(
props: CreateProjectModeSelectionProps,
alm: AlmKeys.Azure | AlmKeys.BitbucketServer | AlmKeys.GitHub | AlmKeys.GitLab,
? almCounts[AlmKeys.BitbucketServer] + almCounts[AlmKeys.BitbucketCloud]
: almCounts[alm];
const hasConfig = count > 0;
- const hasTooManyConfig = count > 1;
+ const hasTooManyConfig = count > 1 && !ALLOWED_MULTIPLE_CONFIGS.includes(alm);
const disabled = loadingBindings || hasTooManyConfig || (!hasConfig && !canAdmin);
const onClick = () => {
return props.onConfigMode(alm);
}
- return props.onSelectMode(
- isBitbucketOption && hasBitbucketCloudConf ? CreateProjectModes.BitbucketCloud : mode
- );
+ return props.onSelectMode(getMode(isBitbucketOption, hasBitbucketCloudConf, mode));
};
- let errorMessage = '';
-
- if (hasTooManyConfig) {
- errorMessage = translateWithParameters(
- 'onboarding.create_project.too_many_alm_instances_X',
- translate('alm', alm)
- );
- } else if (!hasConfig) {
- errorMessage = canAdmin
- ? translate('onboarding.create_project.alm_not_configured.admin')
- : translate('onboarding.create_project.alm_not_configured');
- }
+ const errorMessage = getErrorMessage(hasTooManyConfig, hasConfig, canAdmin, alm);
return (
<div className="display-flex-column">
location={location}
onProjectCreate={this.handleProjectCreate}
router={router}
- settings={gitlabSettings}
+ almInstances={gitlabSettings}
/>
);
}
canAdmin: boolean;
loadingBindings: boolean;
onProjectCreate: (projectKey: string) => void;
- settings: AlmSettingsInstance[];
+ almInstances: AlmSettingsInstance[];
location: Location;
router: Router;
}
resetPat: boolean;
searching: boolean;
searchQuery: string;
- settings?: AlmSettingsInstance;
+ selectedAlmInstance: AlmSettingsInstance;
showPersonalAccessTokenForm: boolean;
}
showPersonalAccessTokenForm: true,
searching: false,
searchQuery: '',
- settings: props.settings.length === 1 ? props.settings[0] : undefined,
+ selectedAlmInstance: props.almInstances[0],
};
}
}
componentDidUpdate(prevProps: Props) {
- if (prevProps.settings.length === 0 && this.props.settings.length > 0) {
- this.setState(
- { settings: this.props.settings.length === 1 ? this.props.settings[0] : undefined },
- () => this.fetchInitialData()
- );
+ const { almInstances } = this.props;
+ if (prevProps.almInstances.length === 0 && this.props.almInstances.length > 0) {
+ this.setState({ selectedAlmInstance: almInstances[0] }, () => this.fetchInitialData());
}
}
};
fetchProjects = async (pageIndex = 1, query?: string) => {
- const { settings } = this.state;
- if (!settings) {
+ const { selectedAlmInstance } = this.state;
+ if (!selectedAlmInstance) {
return Promise.resolve(undefined);
}
try {
return await getGitlabProjects({
- almSetting: settings.key,
+ almSetting: selectedAlmInstance.key,
page: pageIndex,
pageSize: GITLAB_PROJECTS_PAGESIZE,
query,
};
doImport = async (gitlabProjectId: string) => {
- const { settings } = this.state;
+ const { selectedAlmInstance } = this.state;
- if (!settings) {
+ if (!selectedAlmInstance) {
return Promise.resolve(undefined);
}
try {
return await importGitlabProject({
- almSetting: settings.key,
+ almSetting: selectedAlmInstance.key,
gitlabProjectId,
});
} catch (_) {
} = this.state;
const result = await this.fetchProjects(pageIndex + 1, searchQuery);
-
if (this.mounted) {
this.setState(({ projects = [], projectsPaging }) => ({
loadingMore: false,
this.setState({ searching: true, searchQuery });
const result = await this.fetchProjects(1, searchQuery);
-
if (this.mounted) {
this.setState(({ projects, projectsPaging }) => ({
searching: false,
await this.fetchInitialData();
};
+ onChangeConfig = (instance: AlmSettingsInstance) => {
+ this.setState({
+ selectedAlmInstance: instance,
+ showPersonalAccessTokenForm: true,
+ projects: undefined,
+ resetPat: false,
+ });
+ };
+
render() {
- const { canAdmin, loadingBindings, location } = this.props;
+ const { loadingBindings, location, almInstances, canAdmin } = this.props;
const {
importingGitlabProjectId,
loading,
resetPat,
searching,
searchQuery,
- settings,
+ selectedAlmInstance,
showPersonalAccessTokenForm,
} = this.state;
return (
<GitlabProjectCreateRenderer
- settings={settings}
canAdmin={canAdmin}
+ almInstances={almInstances}
+ selectedAlmInstance={selectedAlmInstance}
importingGitlabProjectId={importingGitlabProjectId}
loading={loading || loadingBindings}
loadingMore={loadingMore}
showPersonalAccessTokenForm={
showPersonalAccessTokenForm || Boolean(location.query.resetPat)
}
+ onChangeConfig={this.onChangeConfig}
/>
);
}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import AlmSettingsInstanceSelector from '../../../components/devops-platform/AlmSettingsInstanceSelector';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
import { GitlabProject } from '../../../types/alm-integration';
resetPat: boolean;
searching: boolean;
searchQuery: string;
- settings?: AlmSettingsInstance;
+ almInstances?: AlmSettingsInstance[];
+ selectedAlmInstance?: AlmSettingsInstance;
showPersonalAccessTokenForm?: boolean;
+ onChangeConfig: (instance: AlmSettingsInstance) => void;
}
export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) {
resetPat,
searching,
searchQuery,
- settings,
+ selectedAlmInstance,
+ almInstances,
showPersonalAccessTokenForm,
} = props;
}
/>
+ {almInstances && almInstances.length > 1 && (
+ <div className="display-flex-column huge-spacer-bottom">
+ <label htmlFor="alm-config-selector" className="spacer-bottom">
+ {translate('alm.configuration.selector.label')}
+ </label>
+ <AlmSettingsInstanceSelector
+ instances={almInstances}
+ onChange={props.onChangeConfig}
+ initialValue={selectedAlmInstance ? selectedAlmInstance.key : undefined}
+ classNames="abs-width-400"
+ inputId="alm-config-selector"
+ />
+ </div>
+ )}
+
{loading && <i className="spinner" />}
- {!loading && !settings && (
+ {!loading && !selectedAlmInstance && (
<WrongBindingCountAlert alm={AlmKeys.GitLab} canAdmin={!!canAdmin} />
)}
{!loading &&
- settings &&
+ selectedAlmInstance &&
(showPersonalAccessTokenForm ? (
<PersonalAccessTokenForm
- almSetting={settings}
+ almSetting={selectedAlmInstance}
resetPat={resetPat}
onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated}
/>
};
}
- async componentDidMount() {
+ componentDidMount() {
+ this.mounted = true;
+ this.checkPATAndUpdateView();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (this.props.almSetting !== prevProps.almSetting) {
+ this.checkPATAndUpdateView();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ checkPATAndUpdateView = async () => {
const {
almSetting: { key },
resetPat,
} = this.props;
- this.mounted = true;
// We don't need to check PAT if we want to reset
if (!resetPat) {
}
}
}
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
+ };
handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
className={classNames('input-super-large', {
'is-invalid': isInvalid,
})}
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
value={password}
onChange={this.handlePasswordChange}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { act, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import selectEvent from 'react-select-event';
+import { byLabelText, byRole, byText } from 'testing-library-selector';
+import AlmIntegrationsServiceMock from '../../../../api/mocks/AlmIntegrationsServiceMock';
+import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import CreateProjectPage from '../CreateProjectPage';
+
+jest.mock('../../../../api/alm-integrations');
+jest.mock('../../../../api/alm-settings');
+
+let almIntegrationHandler: AlmIntegrationsServiceMock;
+let almSettingsHandler: AlmSettingsServiceMock;
+
+const ui = {
+ gitlabCreateProjectButton: byText('onboarding.create_project.select_method.gitlab'),
+ personalAccessTokenInput: byRole('textbox', {
+ name: 'onboarding.create_project.enter_pat field_required',
+ }),
+ instanceSelector: byLabelText('alm.configuration.selector.label'),
+};
+
+beforeAll(() => {
+ almIntegrationHandler = new AlmIntegrationsServiceMock();
+ almSettingsHandler = new AlmSettingsServiceMock();
+});
+
+afterEach(() => {
+ almIntegrationHandler.reset();
+ almSettingsHandler.reset();
+});
+
+describe('Gitlab onboarding page', () => {
+ it('should ask for PAT when it is not set yet and show the import project feature afterwards', async () => {
+ const user = userEvent.setup();
+ renderCreateProject();
+ expect(ui.gitlabCreateProjectButton.get()).toBeInTheDocument();
+
+ await user.click(ui.gitlabCreateProjectButton.get());
+ expect(screen.getByText('onboarding.create_project.gitlab.title')).toBeInTheDocument();
+ expect(screen.getByText('alm.configuration.selector.label')).toBeInTheDocument();
+
+ expect(screen.getByText('onboarding.create_project.enter_pat')).toBeInTheDocument();
+ expect(screen.getByText('onboarding.create_project.pat_help.title')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'save' })).toBeInTheDocument();
+
+ await user.click(ui.personalAccessTokenInput.get());
+ await user.keyboard('secret');
+ await user.click(screen.getByRole('button', { name: 'save' }));
+
+ expect(screen.getByText('Gitlab project 1')).toBeInTheDocument();
+ expect(screen.getByText('Gitlab project 2')).toBeInTheDocument();
+ expect(screen.getAllByText('onboarding.create_project.set_up')).toHaveLength(2);
+ expect(screen.getByText('onboarding.create_project.repository_imported')).toBeInTheDocument();
+ });
+
+ it('should show import project feature when PAT is already set', async () => {
+ const user = userEvent.setup();
+ renderCreateProject();
+ await act(async () => {
+ await user.click(ui.gitlabCreateProjectButton.get());
+ await selectEvent.select(ui.instanceSelector.get(), [/conf-final-2/]);
+ });
+
+ expect(screen.getByText('Gitlab project 1')).toBeInTheDocument();
+ expect(screen.getByText('Gitlab project 2')).toBeInTheDocument();
+ });
+
+ it('should show no result message when there are no projects', async () => {
+ const user = userEvent.setup();
+ almIntegrationHandler.setGitlabProjects([]);
+ renderCreateProject();
+ await act(async () => {
+ await user.click(ui.gitlabCreateProjectButton.get());
+ await selectEvent.select(ui.instanceSelector.get(), [/conf-final-2/]);
+ });
+
+ expect(screen.getByText('onboarding.create_project.gitlab.no_projects')).toBeInTheDocument();
+ });
+});
+
+function renderCreateProject() {
+ renderApp('project/create', <CreateProjectPage />);
+}
});
it('should do nothing with missing settings', async () => {
- const wrapper = shallowRender({ settings: [] });
+ const wrapper = shallowRender({ almInstances: [] });
await wrapper.instance().handleLoadMore();
await wrapper.instance().handleSearch('whatever');
location={mockLocation()}
onProjectCreate={jest.fn()}
router={mockRouter()}
- settings={[mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: almSettingKey })]}
+ almInstances={[mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: almSettingKey })]}
{...props}
/>
);
it('should render correctly', () => {
expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
- expect(shallowRender({ settings: undefined })).toMatchSnapshot('invalid settings');
- expect(shallowRender({ canAdmin: true, settings: undefined })).toMatchSnapshot(
+ expect(shallowRender({ almInstances: undefined })).toMatchSnapshot('invalid settings');
+ expect(shallowRender({ almInstances: undefined })).toMatchSnapshot(
'invalid settings, admin user'
);
expect(shallowRender()).toMatchSnapshot('pat form');
onLoadMore={jest.fn()}
onPersonalAccessTokenCreated={jest.fn()}
onSearch={jest.fn()}
+ onChangeConfig={jest.fn()}
projects={undefined}
projectsPaging={{ pageIndex: 1, pageSize: 30, total: 0 }}
searching={false}
searchQuery=""
resetPat={false}
showPersonalAccessTokenForm={true}
- settings={mockAlmSettingsInstance({ alm: AlmKeys.GitLab })}
+ almInstances={[
+ mockAlmSettingsInstance({ alm: AlmKeys.GitLab }),
+ mockAlmSettingsInstance({ alm: AlmKeys.GitLab }),
+ mockAlmSettingsInstance({ alm: AlmKeys.GitHub }),
+ ]}
+ selectedAlmInstance={mockAlmSettingsInstance({ alm: AlmKeys.GitLab })}
{...props}
/>
);
// Submit button disabled by default.
expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
- change(wrapper.find('#personal_access_token'), 'token');
+ change(wrapper.find('input#personal_access_token_validation'), 'token');
expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
// Submit button enabled if there's a value.
(checkPersonalAccessTokenIsValid as jest.Mock).mockRejectedValueOnce({});
- change(wrapper.find('#personal_access_token'), 'token');
+ change(wrapper.find('input#personal_access_token_validation'), 'token');
change(wrapper.find('#username'), 'username');
// Expect correct calls to be made when submitting.
id="create-project"
>
<GitlabProjectCreate
+ almInstances={Array []}
canAdmin={false}
loadingBindings={true}
location={
"setRouteLeaveHook": [MockFunction],
}
}
- settings={Array []}
/>
</div>
</Fragment>
exports[`should render correctly 1`] = `
<GitlabProjectCreateRenderer
+ almInstances={
+ Array [
+ Object {
+ "alm": "gitlab",
+ "key": "gitlab-setting",
+ },
+ ]
+ }
canAdmin={false}
loading={false}
loadingMore={false}
+ onChangeConfig={[Function]}
onImport={[Function]}
onLoadMore={[Function]}
onPersonalAccessTokenCreated={[Function]}
resetPat={false}
searchQuery=""
searching={false}
- settings={
+ selectedAlmInstance={
Object {
"alm": "gitlab",
"key": "gitlab-setting",
</span>
}
/>
- <WrongBindingCountAlert
- alm="gitlab"
- canAdmin={false}
+ <PersonalAccessTokenForm
+ almSetting={
+ Object {
+ "alm": "gitlab",
+ "key": "key",
+ }
+ }
+ onPersonalAccessTokenCreated={[MockFunction]}
+ resetPat={false}
/>
</Fragment>
`;
</span>
}
/>
- <WrongBindingCountAlert
- alm="gitlab"
- canAdmin={true}
+ <PersonalAccessTokenForm
+ almSetting={
+ Object {
+ "alm": "gitlab",
+ "key": "key",
+ }
+ }
+ onPersonalAccessTokenCreated={[MockFunction]}
+ resetPat={false}
/>
</Fragment>
`;
</span>
}
/>
+ <div
+ className="display-flex-column huge-spacer-bottom"
+ >
+ <label
+ className="spacer-bottom"
+ htmlFor="alm-config-selector"
+ >
+ alm.configuration.selector.label
+ </label>
+ <AlmSettingsInstanceSelector
+ classNames="abs-width-400"
+ initialValue="key"
+ inputId="alm-config-selector"
+ instances={
+ Array [
+ Object {
+ "alm": "gitlab",
+ "key": "key",
+ },
+ Object {
+ "alm": "gitlab",
+ "key": "key",
+ },
+ Object {
+ "alm": "github",
+ "key": "key",
+ },
+ ]
+ }
+ onChange={[MockFunction]}
+ />
+ </div>
<i
className="spinner"
/>
</span>
}
/>
+ <div
+ className="display-flex-column huge-spacer-bottom"
+ >
+ <label
+ className="spacer-bottom"
+ htmlFor="alm-config-selector"
+ >
+ alm.configuration.selector.label
+ </label>
+ <AlmSettingsInstanceSelector
+ classNames="abs-width-400"
+ initialValue="key"
+ inputId="alm-config-selector"
+ instances={
+ Array [
+ Object {
+ "alm": "gitlab",
+ "key": "key",
+ },
+ Object {
+ "alm": "gitlab",
+ "key": "key",
+ },
+ Object {
+ "alm": "github",
+ "key": "key",
+ },
+ ]
+ }
+ onChange={[MockFunction]}
+ />
+ </div>
<PersonalAccessTokenForm
almSetting={
Object {
</span>
}
/>
+ <div
+ className="display-flex-column huge-spacer-bottom"
+ >
+ <label
+ className="spacer-bottom"
+ htmlFor="alm-config-selector"
+ >
+ alm.configuration.selector.label
+ </label>
+ <AlmSettingsInstanceSelector
+ classNames="abs-width-400"
+ initialValue="key"
+ inputId="alm-config-selector"
+ instances={
+ Array [
+ Object {
+ "alm": "gitlab",
+ "key": "key",
+ },
+ Object {
+ "alm": "gitlab",
+ "key": "key",
+ },
+ Object {
+ "alm": "github",
+ "key": "key",
+ },
+ ]
+ }
+ onChange={[MockFunction]}
+ />
+ </div>
<GitlabProjectSelectionForm
loadingMore={false}
onImport={[MockFunction]}
<input
autoFocus={true}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
onChange={[Function]}
type="text"
<input
autoFocus={false}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
onChange={[Function]}
type="text"
<input
autoFocus={true}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
onChange={[Function]}
type="text"
<input
autoFocus={true}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
onChange={[Function]}
type="text"
<input
autoFocus={false}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
onChange={[Function]}
type="text"
+import { AlmKeys } from '../../../types/alm-settings';
+
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
export const PROJECT_NAME_MAX_LEN = 255;
export const DEFAULT_BBS_PAGE_SIZE = 25;
+
+export const ALLOWED_MULTIPLE_CONFIGS = [AlmKeys.GitLab];
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import { Permissions } from '../../../types/permissions';
import { LoggedInUser } from '../../../types/users';
+import { ALLOWED_MULTIPLE_CONFIGS } from '../../create/project/constants';
import ProjectCreationMenuItem from './ProjectCreationMenuItem';
interface Props {
currentAlmSettings = almSettings.filter((s) => s.alm === key);
}
return (
- currentAlmSettings.length === 1 &&
+ this.configLengthChecker(key, currentAlmSettings.length) &&
key === currentAlmSettings[0].alm &&
this.almSettingIsValid(currentAlmSettings[0])
);
}
};
+ configLengthChecker = (key: AlmKeys, length: number) => {
+ return ALLOWED_MULTIPLE_CONFIGS.includes(key) ? length > 0 : length === 1;
+ };
+
render() {
const { className, currentUser } = this.props;
const { boundAlms } = this.state;
wrapper = shallowRender();
await waitAndUpdate(wrapper);
- expect(wrapper.state().boundAlms).toEqual([]);
+ expect(wrapper.state().boundAlms).toEqual([AlmKeys.GitLab]);
});
function shallowRender(overrides: Partial<ProjectCreationMenu['props']> = {}) {
AlmSettingsBindingStatusType,
} from '../../../../types/alm-settings';
import { EditionKey } from '../../../../types/editions';
+import { ALLOWED_MULTIPLE_CONFIGS } from '../../../create/project/constants';
export interface AlmBindingDefinitionBoxProps {
alm: AlmKeys;
multipleDefinitions: boolean,
type: AlmSettingsBindingStatusType.Success | AlmSettingsBindingStatusType.Failure
) {
- if (multipleDefinitions) {
+ if (multipleDefinitions && !ALLOWED_MULTIPLE_CONFIGS.includes(alm)) {
return (
<div className="display-inline-flex-center">
<strong className="spacer-left">
*/
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { components, OptionProps, SingleValueProps } from 'react-select';
import Link from '../../../../components/common/Link';
import { Button, SubmitButton } from '../../../../components/controls/buttons';
-import Select from '../../../../components/controls/Select';
+import AlmSettingsInstanceSelector from '../../../../components/devops-platform/AlmSettingsInstanceSelector';
import AlertSuccessIcon from '../../../../components/icons/AlertSuccessIcon';
import { Alert } from '../../../../components/ui/Alert';
import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
isSysAdmin: boolean;
}
-function optionRenderer(props: OptionProps<AlmSettingsInstance, false>) {
- return <components.Option {...props}>{customOptions(props.data)}</components.Option>;
-}
-
-function singleValueRenderer(props: SingleValueProps<AlmSettingsInstance>) {
- return <components.SingleValue {...props}>{customOptions(props.data)}</components.SingleValue>;
-}
-
-function customOptions(instance: AlmSettingsInstance) {
- return instance.url ? (
- <>
- <span>{instance.key} — </span>
- <span className="text-muted">{instance.url}</span>
- </>
- ) : (
- <span>{instance.key}</span>
- );
-}
-
export default function PRDecorationBindingRenderer(props: PRDecorationBindingRendererProps) {
const {
formData,
</div>
</div>
<div className="settings-definition-right">
- <Select
- inputId="name"
- className="abs-width-400 big-spacer-top it__configuration-name-select"
- isClearable={false}
- isSearchable={false}
- options={instances}
+ <AlmSettingsInstanceSelector
+ instances={instances}
onChange={(instance: AlmSettingsInstance) => props.onFieldChange('key', instance.key)}
- components={{
- Option: optionRenderer,
- SingleValue: singleValueRenderer,
- }}
- value={instances.filter((instance) => instance.key === formData.key)}
+ initialValue={formData.key}
+ classNames="abs-width-400 big-spacer-top it__configuration-name-select"
+ inputId="name"
/>
</div>
</div>
<div
className="settings-definition-right"
>
- <Select
- className="abs-width-400 big-spacer-top it__configuration-name-select"
- components={
- Object {
- "Option": [Function],
- "SingleValue": [Function],
- }
- }
+ <AlmSettingsInstanceSelector
+ classNames="abs-width-400 big-spacer-top it__configuration-name-select"
+ initialValue="i1"
inputId="name"
- isClearable={false}
- isSearchable={false}
- onChange={[Function]}
- options={
+ instances={
Array [
Object {
"alm": "github",
},
]
}
- value={
- Array [
- Object {
- "alm": "github",
- "key": "i1",
- "url": "http://github.enterprise.com",
- },
- ]
- }
+ onChange={[Function]}
/>
</div>
</div>
<div
className="settings-definition-right"
>
- <Select
- className="abs-width-400 big-spacer-top it__configuration-name-select"
- components={
- Object {
- "Option": [Function],
- "SingleValue": [Function],
- }
- }
+ <AlmSettingsInstanceSelector
+ classNames="abs-width-400 big-spacer-top it__configuration-name-select"
+ initialValue=""
inputId="name"
- isClearable={false}
- isSearchable={false}
- onChange={[Function]}
- options={
+ instances={
Array [
Object {
"alm": "github",
},
]
}
- value={Array []}
+ onChange={[Function]}
/>
</div>
</div>
<div
className="settings-definition-right"
>
- <Select
- className="abs-width-400 big-spacer-top it__configuration-name-select"
- components={
- Object {
- "Option": [Function],
- "SingleValue": [Function],
- }
- }
+ <AlmSettingsInstanceSelector
+ classNames="abs-width-400 big-spacer-top it__configuration-name-select"
+ initialValue=""
inputId="name"
- isClearable={false}
- isSearchable={false}
- onChange={[Function]}
- options={
+ instances={
Array [
Object {
"alm": "github",
},
]
}
- value={Array []}
+ onChange={[Function]}
/>
</div>
</div>
<div
className="settings-definition-right"
>
- <Select
- className="abs-width-400 big-spacer-top it__configuration-name-select"
- components={
- Object {
- "Option": [Function],
- "SingleValue": [Function],
- }
- }
+ <AlmSettingsInstanceSelector
+ classNames="abs-width-400 big-spacer-top it__configuration-name-select"
+ initialValue=""
inputId="name"
- isClearable={false}
- isSearchable={false}
- onChange={[Function]}
- options={
+ instances={
Array [
Object {
"alm": "github",
},
]
}
- value={Array []}
+ onChange={[Function]}
/>
</div>
</div>
<div
className="settings-definition-right"
>
- <Select
- className="abs-width-400 big-spacer-top it__configuration-name-select"
- components={
- Object {
- "Option": [Function],
- "SingleValue": [Function],
- }
- }
+ <AlmSettingsInstanceSelector
+ classNames="abs-width-400 big-spacer-top it__configuration-name-select"
+ initialValue="i1"
inputId="name"
- isClearable={false}
- isSearchable={false}
- onChange={[Function]}
- options={
+ instances={
Array [
Object {
"alm": "github",
},
]
}
- value={
- Array [
- Object {
- "alm": "github",
- "key": "i1",
- "url": "http://github.enterprise.com",
- },
- ]
- }
+ onChange={[Function]}
/>
</div>
</div>
<div
className="settings-definition-right"
>
- <Select
- className="abs-width-400 big-spacer-top it__configuration-name-select"
- components={
- Object {
- "Option": [Function],
- "SingleValue": [Function],
- }
- }
+ <AlmSettingsInstanceSelector
+ classNames="abs-width-400 big-spacer-top it__configuration-name-select"
+ initialValue=""
inputId="name"
- isClearable={false}
- isSearchable={false}
- onChange={[Function]}
- options={
+ instances={
Array [
Object {
"alm": "github",
},
]
}
- value={Array []}
+ onChange={[Function]}
/>
</div>
</div>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { components, OptionProps, SingleValueProps } from 'react-select';
+import { AlmSettingsInstance } from '../../types/alm-settings';
+import Select from '../controls/Select';
+
+function optionRenderer(props: OptionProps<AlmSettingsInstance, false>) {
+ return <components.Option {...props}>{customOptions(props.data)}</components.Option>;
+}
+
+function singleValueRenderer(props: SingleValueProps<AlmSettingsInstance>) {
+ return <components.SingleValue {...props}>{customOptions(props.data)}</components.SingleValue>;
+}
+
+function customOptions(instance: AlmSettingsInstance) {
+ return instance.url ? (
+ <>
+ <span>{instance.key} — </span>
+ <span className="text-muted">{instance.url}</span>
+ </>
+ ) : (
+ <span>{instance.key}</span>
+ );
+}
+
+interface Props {
+ instances: AlmSettingsInstance[];
+ initialValue?: string;
+ onChange: (instance: AlmSettingsInstance) => void;
+ classNames: string;
+ inputId: string;
+}
+
+export default function AlmSettingsInstanceSelector(props: Props) {
+ const { instances, initialValue, classNames, inputId } = props;
+
+ return (
+ <Select
+ inputId={inputId}
+ className={classNames}
+ isClearable={false}
+ isSearchable={false}
+ options={instances}
+ onChange={(inst) => {
+ if (inst) {
+ props.onChange(inst);
+ }
+ }}
+ components={{
+ Option: optionRenderer,
+ SingleValue: singleValueRenderer,
+ }}
+ getOptionValue={(opt) => opt.key}
+ value={instances.find((inst) => inst.key === initialValue)}
+ />
+ );
+}
alm.github.short=GitHub
alm.gitlab=GitLab
alm.gitlab.short=GitLab
+alm.configuration.selector.label=What DevOps platform do you want to import project from?
#------------------------------------------------------------------------------
#
onboarding.create_project.github.warning.message_admin.link=DevOps Platform integration settings
onboarding.create_project.github.no_orgs=We couldn't load any organizations with your key. Contact an administrator.
onboarding.create_project.github.no_orgs_admin=We couldn't load any organizations. Make sure the GitHub App is installed in at least one organization and check the GitHub instance configuration in the {link}.
-onboarding.create_project.gitlab.title=Which GitLab project do you want to set up?
+onboarding.create_project.gitlab.title=Gitlab project onboarding
onboarding.create_project.gitlab.no_projects=No projects could be fetched from Gitlab. Contact your system administrator, or {link}.
onboarding.create_project.gitlab.link=See on GitLab