diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2022-11-11 16:12:19 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-11-17 20:03:07 +0000 |
commit | 317ada58ae514beeb7865d67dae54f271f6d26ae (patch) | |
tree | 30f20c3f681ea773403f9d74d1ff0d7af748afd1 | |
parent | 8d0f159bb4994704c4395c99762d794441eece8f (diff) | |
download | sonarqube-317ada58ae514beeb7865d67dae54f271f6d26ae.tar.gz sonarqube-317ada58ae514beeb7865d67dae54f271f6d26ae.zip |
SONAR-17586 Allow project onboarding when multiple Gitlab integrations are configured
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 |