From 8ff9a3138ec0c7a0056b1dca7e47c71e9dad8494 Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Fri, 13 Oct 2023 18:06:50 +0200 Subject: [PATCH] SONAR-20708 Add multi project import API call --- .../project/Azure/AzureProjectCreate.tsx | 23 ++--- .../BitbucketCloudProjectCreate.tsx | 25 +++--- .../BitbucketProjectCreate.tsx | 23 ++--- .../apps/create/project/CreateProjectPage.tsx | 74 +++++++++++++---- .../project/Github/GitHubProjectCreate.tsx | 20 ++--- .../project/Gitlab/GitlabProjectCreate.tsx | 16 ++-- .../create/project/__tests__/Manual-it.tsx | 4 +- .../components/NewCodeDefinitionSelection.tsx | 83 +++++++++++++------ .../project/manual/ManualProjectCreate.tsx | 23 ++--- .../src/main/js/apps/create/project/types.ts | 7 -- .../src/main/js/queries/import-projects.ts | 69 +++++++++++++++ .../resources/org/sonar/l10n/core.properties | 3 +- 12 files changed, 257 insertions(+), 113 deletions(-) create mode 100644 server/sonar-web/src/main/js/queries/import-projects.ts diff --git a/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx index bb511fa7335..bf418da89e3 100644 --- a/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx @@ -22,13 +22,13 @@ import { getAzureProjects, getAzureRepositories, searchAzureRepositories, - setupAzureProjectCreation, } from '../../../../api/alm-integrations'; import { Location, Router } from '../../../../components/hoc/withRouter'; import { AzureProject, AzureRepository } from '../../../../types/alm-integration'; import { AlmSettingsInstance } from '../../../../types/alm-settings'; import { Dict } from '../../../../types/types'; -import { CreateProjectApiCallback } from '../types'; +import { ImportProjectParam } from '../CreateProjectPage'; +import { CreateProjectModes } from '../types'; import AzureCreateProjectRenderer from './AzureProjectCreateRenderer'; interface Props { @@ -37,7 +37,7 @@ interface Props { almInstances: AlmSettingsInstance[]; location: Location; router: Router; - onProjectSetupDone: (createProject: CreateProjectApiCallback) => void; + onProjectSetupDone: (importProjects: ImportProjectParam) => void; } interface State { @@ -210,13 +210,16 @@ export default class AzureProjectCreate extends React.PureComponent void; + onProjectSetupDone: (importProjects: ImportProjectParam) => void; } interface State { @@ -193,12 +191,15 @@ export default class BitbucketCloudProjectCreate extends React.PureComponent void; + onProjectSetupDone: (importProjects: ImportProjectParam) => void; } interface State { @@ -184,13 +184,16 @@ export default class BitbucketProjectCreate extends React.PureComponent { mounted = false; - createProjectFnRef: CreateProjectApiCallback | null = null; state: State = { azureSettings: [], @@ -95,7 +142,7 @@ export class CreateProjectPage extends React.PureComponent { location.query.setncd = undefined; @@ -138,11 +185,10 @@ export class CreateProjectPage extends React.PureComponent { + handleProjectSetupDone = (importProjects: ImportProjectParam) => { const { location, router } = this.props; - this.createProjectFnRef = createProject; - this.setState({ nbrOfProjects }); + this.setState({ importProjects }); location.query.setncd = 'true'; router.push(location); @@ -275,8 +321,8 @@ export class CreateProjectPage extends React.PureComponent {this.renderProjectCreation(mode)} -
- -
+ {importProjects !== undefined && isProjectSetupDone && ( + + )} {creatingAlmDefinition && ( void; + onProjectSetupDone: (importProjects: ImportProjectParam) => void; almInstances: AlmSettingsInstance[]; location: Location; router: Router; @@ -263,14 +263,14 @@ export default class GitHubProjectCreate extends React.Component { const { selectedOrganization, selectedAlmInstance } = this.state; if (selectedAlmInstance && selectedOrganization && repoKeys.length > 0) { - this.props.onProjectSetupDone( - setupGithubProjectCreation({ - almSetting: selectedAlmInstance.key, + this.props.onProjectSetupDone({ + almSetting: selectedAlmInstance.key, + creationMode: CreateProjectModes.GitHub, + projects: repoKeys.map((repositoryKey) => ({ + repositoryKey, organization: selectedOrganization.key, - repositoryKey: repoKeys.join(','), // TBD - }), - repoKeys.length, - ); + })), + }); } }; diff --git a/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx index 228f67171b5..e2032788c0f 100644 --- a/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx @@ -18,12 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { getGitlabProjects, setupGitlabProjectCreation } from '../../../../api/alm-integrations'; +import { getGitlabProjects } from '../../../../api/alm-integrations'; import { Location, Router } from '../../../../components/hoc/withRouter'; import { GitlabProject } from '../../../../types/alm-integration'; import { AlmSettingsInstance } from '../../../../types/alm-settings'; import { Paging } from '../../../../types/types'; -import { CreateProjectApiCallback } from '../types'; +import { ImportProjectParam } from '../CreateProjectPage'; +import { CreateProjectModes } from '../types'; import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer'; interface Props { @@ -32,7 +33,7 @@ interface Props { almInstances: AlmSettingsInstance[]; location: Location; router: Router; - onProjectSetupDone: (createProject: CreateProjectApiCallback) => void; + onProjectSetupDone: (importProjects: ImportProjectParam) => void; } interface State { @@ -139,10 +140,11 @@ export default class GitlabProjectCreate extends React.PureComponent ({ - setupManualProjectCreation: jest - .fn() - .mockReturnValue(() => Promise.resolve({ project: mockProject() })), + createProject: jest.fn().mockReturnValue(Promise.resolve({ project: mockProject() })), })); jest.mock('../../../../api/components', () => ({ ...jest.requireActual('../../../../api/components'), diff --git a/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx b/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx index c9c56f65fa3..e2d22150707 100644 --- a/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx @@ -18,50 +18,81 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { ButtonPrimary, ButtonSecondary, FlagMessage, Link, Spinner, Title } from 'design-system'; +import { omit } from 'lodash'; import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; +import { useEffect } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { Router } from '../../../../components/hoc/withRouter'; import NewCodeDefinitionSelector from '../../../../components/new-code-definition/NewCodeDefinitionSelector'; import { useDocUrl } from '../../../../helpers/docs'; import { addGlobalSuccessMessage } from '../../../../helpers/globalMessages'; import { translate } from '../../../../helpers/l10n'; -import { getProjectUrl } from '../../../../helpers/urls'; +import { getProjectUrl, queryToSearch } from '../../../../helpers/urls'; +import { + MutationArg, + useImportProjectMutation, + useImportProjectProgress, +} from '../../../../queries/import-projects'; import { NewCodeDefinitiondWithCompliance } from '../../../../types/new-code-definition'; -import { CreateProjectApiCallback } from '../types'; +import { ImportProjectParam } from '../CreateProjectPage'; interface Props { - createProjectFnRef: CreateProjectApiCallback | null; - router: Router; - numberOfProjects?: number; + importProjects: ImportProjectParam; } export default function NewCodeDefinitionSelection(props: Props) { - const { createProjectFnRef, router, numberOfProjects } = props; + const { importProjects } = props; - const [submitting, setSubmitting] = React.useState(false); const [selectedDefinition, selectDefinition] = React.useState(); - + const { mutate, isLoading, data, reset } = useImportProjectMutation(); + const mutateCount = useImportProjectProgress(); + const intl = useIntl(); const navigate = useNavigate(); - const getDocUrl = useDocUrl(); - const isMultipleProjects = numberOfProjects !== undefined && numberOfProjects !== 1; - const projectCount = isMultipleProjects ? numberOfProjects : 1; + const projectCount = importProjects.projects.length; + const isMultipleProjects = projectCount > 1; - const handleProjectCreation = React.useCallback(async () => { - if (createProjectFnRef && selectedDefinition) { - setSubmitting(true); - const { project } = await createProjectFnRef( - selectedDefinition.type, - selectedDefinition.value, - ); - setSubmitting(false); - router.push(getProjectUrl(project.key)); + useEffect(() => { + if (mutateCount > 0 || !data) { + return; + } + reset(); + addGlobalSuccessMessage( + intl.formatMessage( + { id: 'onboarding.create_project.success' }, + { + count: projectCount, + }, + ), + ); + + if (projectCount === 1) { + navigate(getProjectUrl(data.project.key)); + } else { + navigate({ + pathname: '/projects', + search: queryToSearch({ recent: true }), + }); + } + }, [data, projectCount, mutateCount, reset, intl, navigate]); - addGlobalSuccessMessage(translate('onboarding.create_project.success')); + const handleProjectCreation = () => { + if (selectedDefinition) { + importProjects.projects.forEach((p) => { + const arg = { + // eslint-disable-next-line local-rules/use-metrickey-enum + ...omit(importProjects, 'projects'), + ...p, + } as MutationArg; + mutate({ + newCodeDefinitionType: selectedDefinition.type, + newCodeDefinitionValue: selectedDefinition.value, + ...arg, + }); + }); } - }, [createProjectFnRef, router, selectedDefinition]); + }; return (
@@ -106,7 +137,7 @@ export default function NewCodeDefinitionSelection(props: Props) { - +
diff --git a/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx index 5cf0729d81d..f567f7f2f06 100644 --- a/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/manual/ManualProjectCreate.tsx @@ -33,19 +33,19 @@ import { debounce, isEmpty } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { doesComponentExists } from '../../../../api/components'; -import { setupManualProjectCreation } from '../../../../api/project-management'; import { getValue } from '../../../../api/settings'; import { useDocUrl } from '../../../../helpers/docs'; import { translate } from '../../../../helpers/l10n'; import { PROJECT_KEY_INVALID_CHARACTERS, validateProjectKey } from '../../../../helpers/projects'; import { ProjectKeyValidationResult } from '../../../../types/component'; import { GlobalSettingKeys } from '../../../../types/settings'; +import { ImportProjectParam } from '../CreateProjectPage'; import { PROJECT_NAME_MAX_LEN } from '../constants'; -import { CreateProjectApiCallback } from '../types'; +import { CreateProjectModes } from '../types'; interface Props { branchesEnabled: boolean; - onProjectSetupDone: (createProject: CreateProjectApiCallback) => void; + onProjectSetupDone: (importProjects: ImportProjectParam) => void; } interface State { @@ -133,13 +133,16 @@ export default class ManualProjectCreate extends React.PureComponent Promise<{ project: ProjectBase }>; diff --git a/server/sonar-web/src/main/js/queries/import-projects.ts b/server/sonar-web/src/main/js/queries/import-projects.ts new file mode 100644 index 00000000000..2510b371c1a --- /dev/null +++ b/server/sonar-web/src/main/js/queries/import-projects.ts @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { useIsMutating, useMutation } from '@tanstack/react-query'; +import { + importAzureRepository, + importBitbucketCloudRepository, + importBitbucketServerProject, + importGithubRepository, + importGitlabProject, +} from '../api/alm-integrations'; +import { createProject } from '../api/project-management'; +import { ImportProjectParam } from '../apps/create/project/CreateProjectPage'; +import { CreateProjectModes } from '../apps/create/project/types'; + +export type MutationArg = + AlmImport extends { + creationMode: infer A; + almSetting: string; + projects: (infer R)[]; + } + ? { creationMode: A; almSetting: string } & R + : never; + +export function useImportProjectMutation() { + return useMutation({ + mutationFn: ( + data: { + newCodeDefinitionType?: string; + newCodeDefinitionValue?: string; + } & MutationArg, + ) => { + if (data.creationMode === CreateProjectModes.GitHub) { + return importGithubRepository(data); + } else if (data.creationMode === CreateProjectModes.AzureDevOps) { + return importAzureRepository(data); + } else if (data.creationMode === CreateProjectModes.BitbucketCloud) { + return importBitbucketCloudRepository(data); + } else if (data.creationMode === CreateProjectModes.BitbucketServer) { + return importBitbucketServerProject(data); + } else if (data.creationMode === CreateProjectModes.GitLab) { + return importGitlabProject(data); + } + + return createProject(data); + }, + mutationKey: ['import'], + }); +} + +export function useImportProjectProgress() { + return useIsMutating({ mutationKey: ['import'] }); +} 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 fb19ad89c0f..09b12b70209 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -4200,8 +4200,7 @@ onboarding.create_project.new_code_definition.description.link=Defining New Code onboarding.create_project.new_code_definition.create_project=Create project onboarding.create_project.new_code_definition.create_x_projects=Create {count, plural, one {project} other {# projects}} onboarding.create_projects.new_code_definition.change_info=You can change this for each project individually at any time in the project administration. - -onboarding.create_project.success=Congratulations! Your project has been created. +onboarding.create_project.success=Your {count, plural, one {project has} other {# projects have}} been created. onboarding.token.header=Provide a token onboarding.token.text=The token is used to identify you when an analysis is performed. If it has been compromised, you can revoke it at any point in time in your {link}. -- 2.39.5