aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>2022-11-11 16:12:19 +0100
committersonartech <sonartech@sonarsource.com>2022-11-17 20:03:07 +0000
commit317ada58ae514beeb7865d67dae54f271f6d26ae (patch)
tree30f20c3f681ea773403f9d74d1ff0d7af748afd1
parent8d0f159bb4994704c4395c99762d794441eece8f (diff)
downloadsonarqube-317ada58ae514beeb7865d67dae54f271f6d26ae.tar.gz
sonarqube-317ada58ae514beeb7865d67dae54f271f6d26ae.zip
SONAR-17586 Allow project onboarding when multiple Gitlab integrations are configured
-rw-r--r--server/sonar-web/src/main/js/api/mocks/AlmIntegrationsServiceMock.ts86
-rw-r--r--server/sonar-web/src/main/js/api/mocks/AlmSettingsServiceMock.ts44
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx46
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx29
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProject-it.tsx105
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap11
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap120
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap10
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/constants.ts4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap124
-rw-r--r--server/sonar-web/src/main/js/components/devops-platform/AlmSettingsInstanceSelector.tsx75
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties3
23 files changed, 603 insertions, 199 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/AlmIntegrationsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/AlmIntegrationsServiceMock.ts
new file mode 100644
index 00000000000..d63bbf29a17
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/mocks/AlmIntegrationsServiceMock.ts
@@ -0,0 +1,86 @@
+/*
+ * 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);
+ };
+}
diff --git a/server/sonar-web/src/main/js/api/mocks/AlmSettingsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/AlmSettingsServiceMock.ts
new file mode 100644
index 00000000000..9fb8b1bc6b2
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/mocks/AlmSettingsServiceMock.ts
@@ -0,0 +1,44 @@
+/*
+ * 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);
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
index 7e4c9c0c007..22f27994a94 100644
--- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
@@ -28,6 +28,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
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 {
@@ -42,6 +43,32 @@ 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,
@@ -61,7 +88,7 @@ function renderAlmOption(
? 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 = () => {
@@ -73,23 +100,10 @@ function renderAlmOption(
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">
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
index abbd5af9d05..926f2fe4f89 100644
--- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
@@ -219,7 +219,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
location={location}
onProjectCreate={this.handleProjectCreate}
router={router}
- settings={gitlabSettings}
+ almInstances={gitlabSettings}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx
index b51ade4a0e9..2c2c80ec3a8 100644
--- a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx
@@ -29,7 +29,7 @@ interface Props {
canAdmin: boolean;
loadingBindings: boolean;
onProjectCreate: (projectKey: string) => void;
- settings: AlmSettingsInstance[];
+ almInstances: AlmSettingsInstance[];
location: Location;
router: Router;
}
@@ -43,7 +43,7 @@ interface State {
resetPat: boolean;
searching: boolean;
searchQuery: string;
- settings?: AlmSettingsInstance;
+ selectedAlmInstance: AlmSettingsInstance;
showPersonalAccessTokenForm: boolean;
}
@@ -63,7 +63,7 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
showPersonalAccessTokenForm: true,
searching: false,
searchQuery: '',
- settings: props.settings.length === 1 ? props.settings[0] : undefined,
+ selectedAlmInstance: props.almInstances[0],
};
}
@@ -72,11 +72,9 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
}
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());
}
}
@@ -115,14 +113,14 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
};
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,
@@ -133,15 +131,15 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
};
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 (_) {
@@ -172,7 +170,6 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
} = this.state;
const result = await this.fetchProjects(pageIndex + 1, searchQuery);
-
if (this.mounted) {
this.setState(({ projects = [], projectsPaging }) => ({
loadingMore: false,
@@ -186,7 +183,6 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
this.setState({ searching: true, searchQuery });
const result = await this.fetchProjects(1, searchQuery);
-
if (this.mounted) {
this.setState(({ projects, projectsPaging }) => ({
searching: false,
@@ -208,8 +204,17 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
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,
@@ -219,14 +224,15 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
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}
@@ -242,6 +248,7 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
showPersonalAccessTokenForm={
showPersonalAccessTokenForm || Boolean(location.query.resetPat)
}
+ onChangeConfig={this.onChangeConfig}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx
index 8dc36218a5a..668420dffe2 100644
--- a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx
@@ -18,6 +18,7 @@
* 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';
@@ -42,8 +43,10 @@ export interface GitlabProjectCreateRendererProps {
resetPat: boolean;
searching: boolean;
searchQuery: string;
- settings?: AlmSettingsInstance;
+ almInstances?: AlmSettingsInstance[];
+ selectedAlmInstance?: AlmSettingsInstance;
showPersonalAccessTokenForm?: boolean;
+ onChangeConfig: (instance: AlmSettingsInstance) => void;
}
export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) {
@@ -57,7 +60,8 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe
resetPat,
searching,
searchQuery,
- settings,
+ selectedAlmInstance,
+ almInstances,
showPersonalAccessTokenForm,
} = props;
@@ -77,17 +81,32 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe
}
/>
+ {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}
/>
diff --git a/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx
index 3462224c37d..d3b501776c0 100644
--- a/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx
@@ -75,12 +75,26 @@ export default class PersonalAccessTokenForm extends React.PureComponent<Props,
};
}
- 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) {
@@ -106,11 +120,7 @@ export default class PersonalAccessTokenForm extends React.PureComponent<Props,
}
}
}
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
+ };
handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
@@ -379,7 +389,7 @@ export default class PersonalAccessTokenForm extends React.PureComponent<Props,
className={classNames('input-super-large', {
'is-invalid': isInvalid,
})}
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
value={password}
onChange={this.handlePasswordChange}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProject-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProject-it.tsx
new file mode 100644
index 00000000000..ca3c8f2e6b0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProject-it.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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 />);
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx
index 4504a6e0912..4293a00e936 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx
@@ -154,7 +154,7 @@ it('should import', async () => {
});
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');
@@ -204,7 +204,7 @@ function shallowRender(props: Partial<GitlabProjectCreate['props']> = {}) {
location={mockLocation()}
onProjectCreate={jest.fn()}
router={mockRouter()}
- settings={[mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: almSettingKey })]}
+ almInstances={[mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: almSettingKey })]}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx
index dd0773a03bf..9cdb7d6aa0e 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx
@@ -27,8 +27,8 @@ import GitlabProjectCreateRenderer, {
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');
@@ -47,13 +47,19 @@ function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) {
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}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx
index 51f82e784c5..5ab0fec34d5 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx
@@ -97,7 +97,7 @@ it('should correctly handle form for bitbucket interactions', async () => {
// 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.
@@ -120,7 +120,7 @@ it('should show error when issue', async () => {
(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.
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
index 1d7755f6e7a..7d5f76c3837 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
@@ -296,6 +296,7 @@ exports[`should render correctly for gitlab mode 1`] = `
id="create-project"
>
<GitlabProjectCreate
+ almInstances={Array []}
canAdmin={false}
loadingBindings={true}
location={
@@ -324,7 +325,6 @@ exports[`should render correctly for gitlab mode 1`] = `
"setRouteLeaveHook": [MockFunction],
}
}
- settings={Array []}
/>
</div>
</Fragment>
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap
index 577f859ef13..e6a39694a60 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap
@@ -2,9 +2,18 @@
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]}
@@ -19,7 +28,7 @@ exports[`should render correctly 1`] = `
resetPat={false}
searchQuery=""
searching={false}
- settings={
+ selectedAlmInstance={
Object {
"alm": "gitlab",
"key": "gitlab-setting",
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap
index 68e71143079..0fcab57ab4d 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap
@@ -17,9 +17,15 @@ exports[`should render correctly: invalid settings 1`] = `
</span>
}
/>
- <WrongBindingCountAlert
- alm="gitlab"
- canAdmin={false}
+ <PersonalAccessTokenForm
+ almSetting={
+ Object {
+ "alm": "gitlab",
+ "key": "key",
+ }
+ }
+ onPersonalAccessTokenCreated={[MockFunction]}
+ resetPat={false}
/>
</Fragment>
`;
@@ -41,9 +47,15 @@ exports[`should render correctly: invalid settings, admin user 1`] = `
</span>
}
/>
- <WrongBindingCountAlert
- alm="gitlab"
- canAdmin={true}
+ <PersonalAccessTokenForm
+ almSetting={
+ Object {
+ "alm": "gitlab",
+ "key": "key",
+ }
+ }
+ onPersonalAccessTokenCreated={[MockFunction]}
+ resetPat={false}
/>
</Fragment>
`;
@@ -65,6 +77,38 @@ exports[`should render correctly: loading 1`] = `
</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"
/>
@@ -88,6 +132,38 @@ exports[`should render correctly: pat form 1`] = `
</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 {
@@ -118,6 +194,38 @@ exports[`should render correctly: project selection form 1`] = `
</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]}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap
index 2072f4e8815..d569b0fb97b 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap
@@ -29,7 +29,7 @@ exports[`should render correctly: bitbucket 1`] = `
<input
autoFocus={true}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
onChange={[Function]}
type="text"
@@ -180,7 +180,7 @@ exports[`should render correctly: bitbucket cloud 1`] = `
<input
autoFocus={false}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
onChange={[Function]}
type="text"
@@ -321,7 +321,7 @@ exports[`should render correctly: gitlab 1`] = `
<input
autoFocus={true}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
onChange={[Function]}
type="text"
@@ -431,7 +431,7 @@ exports[`should render correctly: gitlab with non-standard api path 1`] = `
<input
autoFocus={true}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
onChange={[Function]}
type="text"
@@ -566,7 +566,7 @@ exports[`should show error when issue: issue submitting token 1`] = `
<input
autoFocus={false}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="personal_access_token_validation"
minLength={1}
onChange={[Function]}
type="text"
diff --git a/server/sonar-web/src/main/js/apps/create/project/constants.ts b/server/sonar-web/src/main/js/apps/create/project/constants.ts
index d47426c1831..3c342c2804e 100644
--- a/server/sonar-web/src/main/js/apps/create/project/constants.ts
+++ b/server/sonar-web/src/main/js/apps/create/project/constants.ts
@@ -1,3 +1,5 @@
+import { AlmKeys } from '../../../types/alm-settings';
+
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
@@ -20,3 +22,5 @@
export const PROJECT_NAME_MAX_LEN = 255;
export const DEFAULT_BBS_PAGE_SIZE = 25;
+
+export const ALLOWED_MULTIPLE_CONFIGS = [AlmKeys.GitLab];
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx
index 1204ca8ae50..97abfd16f81 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx
@@ -31,6 +31,7 @@ import { hasGlobalPermission } from '../../../helpers/users';
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 {
@@ -90,7 +91,7 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> {
currentAlmSettings = almSettings.filter((s) => s.alm === key);
}
return (
- currentAlmSettings.length === 1 &&
+ this.configLengthChecker(key, currentAlmSettings.length) &&
key === currentAlmSettings[0].alm &&
this.almSettingIsValid(currentAlmSettings[0])
);
@@ -103,6 +104,10 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> {
}
};
+ 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;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx
index e3f2908bf9b..fabc60c1c97 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx
@@ -121,7 +121,7 @@ it('should filter alm bindings appropriately', async () => {
wrapper = shallowRender();
await waitAndUpdate(wrapper);
- expect(wrapper.state().boundAlms).toEqual([]);
+ expect(wrapper.state().boundAlms).toEqual([AlmKeys.GitLab]);
});
function shallowRender(overrides: Partial<ProjectCreationMenu['props']> = {}) {
diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx
index 7a5b9780acf..ee27acc7694 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx
@@ -38,6 +38,7 @@ import {
AlmSettingsBindingStatusType,
} from '../../../../types/alm-settings';
import { EditionKey } from '../../../../types/editions';
+import { ALLOWED_MULTIPLE_CONFIGS } from '../../../create/project/constants';
export interface AlmBindingDefinitionBoxProps {
alm: AlmKeys;
@@ -110,7 +111,7 @@ function getImportFeatureStatus(
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">
diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx
index 5589c1e9527..c0737d418eb 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx
@@ -19,10 +19,9 @@
*/
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';
@@ -57,25 +56,6 @@ export interface PRDecorationBindingRendererProps {
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,
@@ -151,18 +131,12 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe
</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>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap
index 8e6dce16295..ba29cae6f90 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap
@@ -129,19 +129,11 @@ exports[`should render correctly: when there are configuration errors (admin use
<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",
@@ -164,15 +156,7 @@ exports[`should render correctly: when there are configuration errors (admin use
},
]
}
- value={
- Array [
- Object {
- "alm": "github",
- "key": "i1",
- "url": "http://github.enterprise.com",
- },
- ]
- }
+ onChange={[Function]}
/>
</div>
</div>
@@ -329,19 +313,11 @@ exports[`should render correctly: when there are configuration errors (admin use
<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",
@@ -364,7 +340,7 @@ exports[`should render correctly: when there are configuration errors (admin use
},
]
}
- value={Array []}
+ onChange={[Function]}
/>
</div>
</div>
@@ -467,19 +443,11 @@ exports[`should render correctly: when there are configuration errors (non-admin
<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",
@@ -502,7 +470,7 @@ exports[`should render correctly: when there are configuration errors (non-admin
},
]
}
- value={Array []}
+ onChange={[Function]}
/>
</div>
</div>
@@ -608,19 +576,11 @@ exports[`should render correctly: with a single ALM instance 1`] = `
<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",
@@ -629,7 +589,7 @@ exports[`should render correctly: with a single ALM instance 1`] = `
},
]
}
- value={Array []}
+ onChange={[Function]}
/>
</div>
</div>
@@ -686,19 +646,11 @@ exports[`should render correctly: with a valid and saved form 1`] = `
<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",
@@ -721,15 +673,7 @@ exports[`should render correctly: with a valid and saved form 1`] = `
},
]
}
- value={
- Array [
- Object {
- "alm": "github",
- "key": "i1",
- "url": "http://github.enterprise.com",
- },
- ]
- }
+ onChange={[Function]}
/>
</div>
</div>
@@ -848,19 +792,11 @@ exports[`should render correctly: with an empty form 1`] = `
<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",
@@ -883,7 +819,7 @@ exports[`should render correctly: with an empty form 1`] = `
},
]
}
- value={Array []}
+ onChange={[Function]}
/>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/components/devops-platform/AlmSettingsInstanceSelector.tsx b/server/sonar-web/src/main/js/components/devops-platform/AlmSettingsInstanceSelector.tsx
new file mode 100644
index 00000000000..dee6fae90a8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/devops-platform/AlmSettingsInstanceSelector.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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)}
+ />
+ );
+}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index ae40c64092d..aa861ca230e 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -389,6 +389,7 @@ alm.github=GitHub
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?
#------------------------------------------------------------------------------
#
@@ -3598,7 +3599,7 @@ onboarding.create_project.github.warning.message_admin=Please make sure the GitH
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