From a292eb339f4ec2b413b855dfa1cf1cf8ac1a1667 Mon Sep 17 00:00:00 2001 From: Kevin Silva Date: Thu, 17 Nov 2022 12:01:13 +0100 Subject: [PATCH] SONAR-17589 Allow project onboarding when multiple Azure integrations are configured --- .../project/AlmSettingsInstanceDropdown.tsx | 51 +++++ .../create/project/AzureProjectCreate.tsx | 67 ++++--- .../project/AzureProjectCreateRenderer.tsx | 28 ++- .../apps/create/project/CreateProjectPage.tsx | 2 +- .../project/GitlabProjectCreateRenderer.tsx | 21 +- .../__tests__/AzureProjectCreate-test.tsx | 6 +- .../AzureProjectCreateRenderer-test.tsx | 29 ++- .../CreateProjectModeSelection-test.tsx | 2 +- .../AzureProjectCreate-test.tsx.snap | 11 +- .../AzureProjectCreateRenderer-test.tsx.snap | 124 ++++++------ .../CreateProjectPage-test.tsx.snap | 2 +- .../GitlabProjectCreateRenderer-test.tsx.snap | 183 +++++++++--------- .../main/js/apps/create/project/constants.ts | 2 +- .../__tests__/ProjectCreationMenu-test.tsx | 2 +- .../resources/org/sonar/l10n/core.properties | 2 +- 15 files changed, 312 insertions(+), 220 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/create/project/AlmSettingsInstanceDropdown.tsx diff --git a/server/sonar-web/src/main/js/apps/create/project/AlmSettingsInstanceDropdown.tsx b/server/sonar-web/src/main/js/apps/create/project/AlmSettingsInstanceDropdown.tsx new file mode 100644 index 00000000000..1bd3d726f48 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/AlmSettingsInstanceDropdown.tsx @@ -0,0 +1,51 @@ +/* + * 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 AlmSettingsInstanceSelector from '../../../components/devops-platform/AlmSettingsInstanceSelector'; +import { translate } from '../../../helpers/l10n'; +import { AlmSettingsInstance } from '../../../types/alm-settings'; + +export interface AlmSettingsInstanceDropdownProps { + almInstances?: AlmSettingsInstance[]; + selectedAlmInstance?: AlmSettingsInstance; + onChangeConfig: (instance: AlmSettingsInstance) => void; +} + +export default function AlmSettingsInstanceDropdown(props: AlmSettingsInstanceDropdownProps) { + const { almInstances, selectedAlmInstance } = props; + return ( + <> + {almInstances && almInstances.length > 1 ? ( +
+ + +
+ ) : null} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx index fb2ffd7920d..1c9864fd9e4 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx @@ -36,7 +36,7 @@ interface Props { canAdmin: boolean; loadingBindings: boolean; onProjectCreate: (projectKey: string) => void; - settings: AlmSettingsInstance[]; + almInstances: AlmSettingsInstance[]; location: Location; router: Router; } @@ -52,7 +52,7 @@ interface State { searchResults?: AzureRepository[]; searchQuery?: string; selectedRepository?: AzureRepository; - settings?: AlmSettingsInstance; + selectedAlmInstance?: AlmSettingsInstance; submittingToken?: boolean; tokenValidationFailed: boolean; } @@ -65,7 +65,7 @@ export default class AzureProjectCreate extends React.PureComponent 0) { - this.setState( - { settings: this.props.settings.length === 1 ? this.props.settings[0] : undefined }, - () => this.fetchInitialData() - ); + if (prevProps.almInstances.length === 0 && this.props.almInstances.length > 0) { + this.setState({ selectedAlmInstance: this.props.almInstances[0] }, () => this.fetchData()); } } @@ -92,7 +89,7 @@ export default class AzureProjectCreate extends React.PureComponent { + fetchData = async () => { this.setState({ loading: true }); const patIsValid = await this.checkPersonalAccessToken().catch(() => false); @@ -135,23 +132,23 @@ export default class AzureProjectCreate extends React.PureComponent => { - const { settings } = this.state; + const { selectedAlmInstance } = this.state; - if (!settings) { + if (!selectedAlmInstance) { return Promise.resolve(undefined); } - return getAzureProjects(settings.key).then(({ projects }) => projects); + return getAzureProjects(selectedAlmInstance.key).then(({ projects }) => projects); }; fetchAzureRepositories = (projectName: string): Promise => { - const { settings } = this.state; + const { selectedAlmInstance } = this.state; - if (!settings) { + if (!selectedAlmInstance) { return Promise.resolve([]); } - return getAzureRepositories(settings.key, projectName) + return getAzureRepositories(selectedAlmInstance.key, projectName) .then(({ repositories }) => repositories) .catch(() => []); }; @@ -180,9 +177,9 @@ export default class AzureProjectCreate extends React.PureComponent { - const { settings } = this.state; + const { selectedAlmInstance } = this.state; - if (!settings) { + if (!selectedAlmInstance) { return; } @@ -194,7 +191,7 @@ export default class AzureProjectCreate extends React.PureComponent repositories) @@ -210,16 +207,16 @@ export default class AzureProjectCreate extends React.PureComponent { - const { selectedRepository, settings } = this.state; + const { selectedRepository, selectedAlmInstance } = this.state; - if (!settings || !selectedRepository) { + if (!selectedAlmInstance || !selectedRepository) { return; } this.setState({ importing: true }); const createdProject = await importAzureRepository( - settings.key, + selectedAlmInstance.key, selectedRepository.projectName, selectedRepository.name ) @@ -239,26 +236,26 @@ export default class AzureProjectCreate extends React.PureComponent { - const { settings } = this.state; + const { selectedAlmInstance } = this.state; - if (!settings) { + if (!selectedAlmInstance) { return Promise.resolve(false); } - return checkPersonalAccessTokenIsValid(settings.key).then(({ status }) => status); + return checkPersonalAccessTokenIsValid(selectedAlmInstance.key).then(({ status }) => status); }; handlePersonalAccessTokenCreate = async (token: string) => { - const { settings } = this.state; + const { selectedAlmInstance } = this.state; - if (!settings || token.length < 1) { + if (!selectedAlmInstance || token.length < 1) { return; } this.setState({ submittingToken: true, tokenValidationFailed: false }); try { - await setAlmPersonalAccessToken(settings.key, token); + await setAlmPersonalAccessToken(selectedAlmInstance.key, token); const patIsValid = await this.checkPersonalAccessToken(); if (this.mounted) { @@ -266,7 +263,7 @@ export default class AzureProjectCreate extends React.PureComponent { + this.setState({ selectedAlmInstance: instance }, () => this.fetchData()); + }; + render() { - const { canAdmin, loadingBindings, location } = this.props; + const { canAdmin, loadingBindings, location, almInstances } = this.props; const { importing, loading, @@ -289,7 +290,7 @@ export default class AzureProjectCreate extends React.PureComponent ); } diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx index fc5b4823c9d..22fa07d7101 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx @@ -31,6 +31,7 @@ import { AzureProject, AzureRepository } from '../../../types/alm-integration'; import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; import { Dict } from '../../../types/types'; import { ALM_INTEGRATION_CATEGORY } from '../../settings/constants'; +import AlmSettingsInstanceDropdown from './AlmSettingsInstanceDropdown'; import AzurePersonalAccessTokenForm from './AzurePersonalAccessTokenForm'; import AzureProjectsList from './AzureProjectsList'; import CreateProjectPageHeader from './CreateProjectPageHeader'; @@ -52,10 +53,12 @@ export interface AzureProjectCreateRendererProps { searchResults?: AzureRepository[]; searchQuery?: string; selectedRepository?: AzureRepository; - settings?: AlmSettingsInstance; + almInstances?: AlmSettingsInstance[]; + selectedAlmInstance?: AlmSettingsInstance; showPersonalAccessTokenForm?: boolean; submittingToken?: boolean; tokenValidationFailed: boolean; + onChangeConfig: (instance: AlmSettingsInstance) => void; } export default function AzureProjectCreateRenderer(props: AzureProjectCreateRendererProps) { @@ -70,15 +73,16 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend searchResults, searchQuery, selectedRepository, - settings, + almInstances, showPersonalAccessTokenForm, submittingToken, tokenValidationFailed, + selectedAlmInstance, } = props; - const settingIsValid = settings && settings.url; - const showCountError = !loading && !settings; - const showUrlError = !loading && settings && !settings.url; + const showCountError = !loading && (!almInstances || almInstances?.length === 0); + const settingIsValid = selectedAlmInstance && selectedAlmInstance.url; + const showUrlError = !loading && selectedAlmInstance && !selectedAlmInstance.url; return ( <> @@ -111,6 +115,12 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend } /> + + {loading && } {showUrlError && ( @@ -137,12 +147,12 @@ export default function AzureProjectCreateRenderer(props: AzureProjectCreateRend {showCountError && } {!loading && - settings && - settings.url && + selectedAlmInstance && + selectedAlmInstance.url && (showPersonalAccessTokenForm ? ( -
+
{ location={location} onProjectCreate={this.handleProjectCreate} router={router} - settings={azureSettings} + almInstances={azureSettings} /> ); } 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 668420dffe2..a90b987b31a 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,12 +18,12 @@ * 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'; import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; import { Paging } from '../../../types/types'; +import AlmSettingsInstanceDropdown from './AlmSettingsInstanceDropdown'; import CreateProjectPageHeader from './CreateProjectPageHeader'; import GitlabProjectSelectionForm from './GitlabProjectSelectionForm'; import PersonalAccessTokenForm from './PersonalAccessTokenForm'; @@ -81,20 +81,11 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe } /> - {almInstances && almInstances.length > 1 && ( -
- - -
- )} + {loading && } diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx index f2a80b0cf13..c033380802a 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx @@ -195,8 +195,8 @@ it('should select and import a repository', async () => { expect(wrapper.state().importing).toBe(false); }); -it('should handle no settings', () => { - const wrapper = shallowRender({ settings: [] }); +it('should handle no almInstances', () => { + const wrapper = shallowRender({ almInstances: [] }); wrapper.instance().fetchAzureProjects(); wrapper.instance().fetchAzureRepositories('whatever'); @@ -221,7 +221,7 @@ function shallowRender(overrides: Partial = {}) { location={mockLocation()} onProjectCreate={jest.fn()} router={mockRouter()} - settings={[mockAlmSettingsInstance({ alm: AlmKeys.Azure, key: 'foo' })]} + almInstances={[mockAlmSettingsInstance({ alm: AlmKeys.Azure, key: 'foo' })]} {...overrides} /> ); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx index 265a1890e03..c4549235f11 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx @@ -28,19 +28,35 @@ import AzureProjectCreateRenderer, { it('should render correctly', () => { expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); - expect(shallowRender({ settings: undefined })).toMatchSnapshot('no settings'); + expect(shallowRender({ almInstances: undefined })).toMatchSnapshot('no settings'); expect(shallowRender({ showPersonalAccessTokenForm: true })).toMatchSnapshot('token form'); - expect(shallowRender()).toMatchSnapshot('project list'); expect( shallowRender({ - settings: mockAlmSettingsInstance({ alm: AlmKeys.Azure }), + almInstances: [ + mockAlmSettingsInstance({ alm: AlmKeys.Azure, url: 'https://azure.company.com' }), + mockAlmSettingsInstance({ + alm: AlmKeys.Azure, + url: 'https://azure.company.com', + key: 'key2', + }), + ], + selectedAlmInstance: mockAlmSettingsInstance({ + alm: AlmKeys.Azure, + url: 'https://azure.company.com', + }), + }) + ).toMatchSnapshot('project list'); + + expect( + shallowRender({ + almInstances: [mockAlmSettingsInstance({ alm: AlmKeys.Azure })], showPersonalAccessTokenForm: true, }) ).toMatchSnapshot('setting missing url, admin'); expect( shallowRender({ canAdmin: false, - settings: mockAlmSettingsInstance({ alm: AlmKeys.Azure }), + almInstances: [mockAlmSettingsInstance({ alm: AlmKeys.Azure })], }) ).toMatchSnapshot('setting missing url, not admin'); }); @@ -62,9 +78,12 @@ function shallowRender(overrides: Partial = {}) projects={[project]} repositories={{ [project.name]: [mockAzureRepository()] }} tokenValidationFailed={false} - settings={mockAlmSettingsInstance({ alm: AlmKeys.Azure, url: 'https://azure.company.com' })} + almInstances={[ + mockAlmSettingsInstance({ alm: AlmKeys.Azure, url: 'https://azure.company.com' }), + ]} showPersonalAccessTokenForm={false} submittingToken={false} + onChangeConfig={jest.fn()} {...overrides} /> ); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx index 47ddaad401e..0301387ae44 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx @@ -102,7 +102,7 @@ it('should call the proper click handler', () => { const onSelectMode = jest.fn(); const onConfigMode = jest.fn(); - let wrapper = shallowRender({ onSelectMode, onConfigMode }, { [AlmKeys.Azure]: 2 }); + let wrapper = shallowRender({ onSelectMode, onConfigMode }, { [AlmKeys.Azure]: 0 }); click(wrapper.find(almButton).at(0)); expect(onConfigMode).not.toHaveBeenCalled(); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap index a3c1a9a74d7..4af7d5f4433 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap @@ -2,17 +2,26 @@ exports[`should render correctly 1`] = ` - - -
- } title={ } /> + @@ -57,6 +52,9 @@ exports[`should render correctly: no settings 1`] = ` } /> + } /> +
@@ -155,29 +177,17 @@ exports[`should render correctly: setting missing url, admin 1`] = ` } /> - - - settings.page - , - } - } - /> - + "alm": "azure", + "key": "key", + }, + ] + } + onChangeConfig={[MockFunction]} + /> `; @@ -198,11 +208,17 @@ exports[`should render correctly: setting missing url, not admin 1`] = ` } /> - - onboarding.create_project.azure.no_url - + `; @@ -224,21 +240,17 @@ exports[`should render correctly: token form 1`] = ` } /> -
- -
+ }, + ] + } + onChangeConfig={[MockFunction]} + /> `; 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 7d5f76c3837..a49fc0433bb 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 @@ -92,6 +92,7 @@ exports[`should render correctly for azure mode 1`] = ` id="create-project" >
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 0fcab57ab4d..a84db1f9b28 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,6 +17,15 @@ exports[`should render correctly: invalid settings 1`] = ` } /> + } /> + } /> -
- - -
+ } + /> @@ -132,38 +143,31 @@ exports[`should render correctly: pat form 1`] = ` } /> -
- - -
+ } + /> } /> -
- - -
+ } + /> { wrapper = shallowRender(); await waitAndUpdate(wrapper); - expect(wrapper.state().boundAlms).toEqual([AlmKeys.GitLab]); + expect(wrapper.state().boundAlms).toEqual([AlmKeys.Azure, AlmKeys.GitLab]); }); function shallowRender(overrides: Partial = {}) { 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 aa861ca230e..870ff352a71 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3583,7 +3583,7 @@ onboarding.create_project.see_on_github=See project on GitHub onboarding.create_project.search_prompt=Search for projects onboarding.create_project.set_up=Set up -onboarding.create_project.azure.title=Which Azure DevOps repository do you want to set up? +onboarding.create_project.azure.title=Azure project onboarding onboarding.create_project.azure.no_projects=No projects could be fetched from Azure DevOps. Contact your system administrator, or {link}. onboarding.create_project.azure.search_results_for_project_X=Search results for "{0}" onboarding.create_project.azure.no_repositories=Could not fetch repositories for this project. Contact your system administrator, or {link}. -- 2.39.5