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 {
almInstances: AlmSettingsInstance[];
location: Location;
router: Router;
- onProjectSetupDone: (createProject: CreateProjectApiCallback) => void;
+ onProjectSetupDone: (importProjects: ImportProjectParam) => void;
}
interface State {
const { selectedAlmInstance } = this.state;
if (selectedAlmInstance && selectedRepository) {
- this.props.onProjectSetupDone(
- setupAzureProjectCreation({
- almSetting: selectedAlmInstance.key,
- projectName: selectedRepository.projectName,
- repositoryName: selectedRepository.name,
- }),
- );
+ this.props.onProjectSetupDone({
+ creationMode: CreateProjectModes.AzureDevOps,
+ almSetting: selectedAlmInstance.key,
+ projects: [
+ {
+ projectName: selectedRepository.projectName,
+ repositoryName: selectedRepository.name,
+ },
+ ],
+ });
}
};
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import {
- searchForBitbucketCloudRepositories,
- setupBitbucketCloudProjectCreation,
-} from '../../../../api/alm-integrations';
+import { searchForBitbucketCloudRepositories } from '../../../../api/alm-integrations';
import { Location, Router } from '../../../../components/hoc/withRouter';
import { BitbucketCloudRepository } from '../../../../types/alm-integration';
import { AlmSettingsInstance } from '../../../../types/alm-settings';
import { Paging } from '../../../../types/types';
+import { ImportProjectParam } from '../CreateProjectPage';
import { BITBUCKET_CLOUD_PROJECTS_PAGESIZE } from '../constants';
-import { CreateProjectApiCallback } from '../types';
+import { CreateProjectModes } from '../types';
import BitbucketCloudProjectCreateRenderer from './BitbucketCloudProjectCreateRender';
interface Props {
loadingBindings: boolean;
location: Location;
router: Router;
- onProjectSetupDone: (createProject: CreateProjectApiCallback) => void;
+ onProjectSetupDone: (importProjects: ImportProjectParam) => void;
}
interface State {
const { selectedAlmInstance } = this.state;
if (selectedAlmInstance) {
- this.props.onProjectSetupDone(
- setupBitbucketCloudProjectCreation({
- almSetting: selectedAlmInstance.key,
- repositorySlug,
- }),
- );
+ this.props.onProjectSetupDone({
+ creationMode: CreateProjectModes.BitbucketCloud,
+ almSetting: selectedAlmInstance.key,
+ projects: [
+ {
+ repositorySlug,
+ },
+ ],
+ });
}
};
getBitbucketServerProjects,
getBitbucketServerRepositories,
searchForBitbucketServerRepositories,
- setupBitbucketServerProjectCreation,
} from '../../../../api/alm-integrations';
import { Location, Router } from '../../../../components/hoc/withRouter';
import {
BitbucketRepository,
} from '../../../../types/alm-integration';
import { AlmSettingsInstance } from '../../../../types/alm-settings';
+import { ImportProjectParam } from '../CreateProjectPage';
import { DEFAULT_BBS_PAGE_SIZE } from '../constants';
-import { CreateProjectApiCallback } from '../types';
+import { CreateProjectModes } from '../types';
import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer';
interface Props {
loadingBindings: boolean;
location: Location;
router: Router;
- onProjectSetupDone: (createProject: CreateProjectApiCallback) => void;
+ onProjectSetupDone: (importProjects: ImportProjectParam) => void;
}
interface State {
const { selectedAlmInstance } = this.state;
if (selectedAlmInstance) {
- this.props.onProjectSetupDone(
- setupBitbucketServerProjectCreation({
- almSetting: selectedAlmInstance.key,
- projectKey: selectedRepository.projectKey,
- repositorySlug: selectedRepository.slug,
- }),
- );
+ this.props.onProjectSetupDone({
+ creationMode: CreateProjectModes.BitbucketServer,
+ almSetting: selectedAlmInstance.key,
+ projects: [
+ {
+ projectKey: selectedRepository.projectKey,
+ repositorySlug: selectedRepository.slug,
+ },
+ ],
+ });
}
};
import GitlabProjectCreate from './Gitlab/GitlabProjectCreate';
import NewCodeDefinitionSelection from './components/NewCodeDefinitionSelection';
import ManualProjectCreate from './manual/ManualProjectCreate';
-import { CreateProjectApiCallback, CreateProjectModes } from './types';
+import { CreateProjectModes } from './types';
export interface CreateProjectPageProps extends WithAvailableFeaturesProps {
appState: AppState;
gitlabSettings: AlmSettingsInstance[];
loading: boolean;
creatingAlmDefinition?: AlmKeys;
- nbrOfProjects?: number;
+ importProjects?: ImportProjectParam;
}
const PROJECT_MODE_FOR_ALM_KEY = {
[AlmKeys.GitLab]: CreateProjectModes.GitLab,
};
+export type ImportProjectParam =
+ | {
+ creationMode: CreateProjectModes.AzureDevOps;
+ almSetting: string;
+ projects: {
+ projectName: string;
+ repositoryName: string;
+ }[];
+ }
+ | {
+ creationMode: CreateProjectModes.BitbucketCloud;
+ almSetting: string;
+ projects: {
+ repositorySlug: string;
+ }[];
+ }
+ | {
+ creationMode: CreateProjectModes.BitbucketServer;
+ almSetting: string;
+ projects: {
+ repositorySlug: string;
+ projectKey: string;
+ }[];
+ }
+ | {
+ creationMode: CreateProjectModes.GitHub;
+ almSetting: string;
+ projects: {
+ organization: string;
+ repositoryKey: string;
+ }[];
+ }
+ | {
+ creationMode: CreateProjectModes.GitLab;
+ almSetting: string;
+ projects: {
+ gitlabProjectId: string;
+ }[];
+ }
+ | {
+ creationMode: CreateProjectModes.Manual;
+ projects: {
+ project: string;
+ name: string;
+ mainBranch: string;
+ }[];
+ };
+
export class CreateProjectPage extends React.PureComponent<CreateProjectPageProps, State> {
mounted = false;
- createProjectFnRef: CreateProjectApiCallback | null = null;
state: State = {
azureSettings: [],
cleanQueryParameters() {
const { location, router } = this.props;
- if (location.query?.setncd === 'true' && this.createProjectFnRef === null) {
+ if (location.query?.setncd === 'true') {
// Timeout is required to force the refresh of the URL
setTimeout(() => {
location.query.setncd = undefined;
this.setState({ creatingAlmDefinition: alm });
};
- handleProjectSetupDone = (createProject: CreateProjectApiCallback, nbrOfProjects?: number) => {
+ handleProjectSetupDone = (importProjects: ImportProjectParam) => {
const { location, router } = this.props;
- this.createProjectFnRef = createProject;
- this.setState({ nbrOfProjects });
+ this.setState({ importProjects });
location.query.setncd = 'true';
router.push(location);
}
render() {
- const { location, router } = this.props;
- const { creatingAlmDefinition, nbrOfProjects } = this.state;
+ const { location } = this.props;
+ const { creatingAlmDefinition, importProjects } = this.state;
const mode: CreateProjectModes | undefined = location.query?.mode;
const isProjectSetupDone = location.query?.setncd === 'true';
const gridLayoutStyle = mode ? 'sw-col-start-2 sw-col-span-10' : 'sw-col-span-12';
<div className={classNames({ 'sw-hidden': isProjectSetupDone })}>
{this.renderProjectCreation(mode)}
</div>
- <div className={classNames({ 'sw-hidden': !isProjectSetupDone })}>
- <NewCodeDefinitionSelection
- router={router}
- createProjectFnRef={this.createProjectFnRef}
- numberOfProjects={nbrOfProjects}
- />
- </div>
+ {importProjects !== undefined && isProjectSetupDone && (
+ <NewCodeDefinitionSelection importProjects={importProjects} />
+ )}
{creatingAlmDefinition && (
<AlmBindingDefinitionForm
getGithubClientId,
getGithubOrganizations,
getGithubRepositories,
- setupGithubProjectCreation,
} from '../../../../api/alm-integrations';
import { Location, Router } from '../../../../components/hoc/withRouter';
import { getHostUrl } from '../../../../helpers/urls';
import { GithubOrganization, GithubRepository } from '../../../../types/alm-integration';
import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
import { Paging } from '../../../../types/types';
-import { CreateProjectApiCallback } from '../types';
+import { ImportProjectParam } from '../CreateProjectPage';
+import { CreateProjectModes } from '../types';
import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';
interface Props {
canAdmin: boolean;
loadingBindings: boolean;
- onProjectSetupDone: (createProject: CreateProjectApiCallback, nbrOfProjects: number) => void;
+ onProjectSetupDone: (importProjects: ImportProjectParam) => void;
almInstances: AlmSettingsInstance[];
location: Location;
router: Router;
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,
- );
+ })),
+ });
}
};
* 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 {
almInstances: AlmSettingsInstance[];
location: Location;
router: Router;
- onProjectSetupDone: (createProject: CreateProjectApiCallback) => void;
+ onProjectSetupDone: (importProjects: ImportProjectParam) => void;
}
interface State {
const { selectedAlmInstance } = this.state;
if (selectedAlmInstance) {
- this.props.onProjectSetupDone(
- // eslint-disable-next-line local-rules/no-api-imports
- setupGitlabProjectCreation({ almSetting: selectedAlmInstance.key, gitlabProjectId }),
- );
+ this.props.onProjectSetupDone({
+ creationMode: CreateProjectModes.GitLab,
+ almSetting: selectedAlmInstance.key,
+ projects: [{ gitlabProjectId }],
+ });
}
};
jest.mock('../../../../api/alm-settings');
jest.mock('../../../../api/newCodeDefinition');
jest.mock('../../../../api/project-management', () => ({
- setupManualProjectCreation: jest
- .fn()
- .mockReturnValue(() => Promise.resolve({ project: mockProject() })),
+ createProject: jest.fn().mockReturnValue(Promise.resolve({ project: mockProject() })),
}));
jest.mock('../../../../api/components', () => ({
...jest.requireActual('../../../../api/components'),
* 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<NewCodeDefinitiondWithCompliance>();
-
+ 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 (
<div id="project-ncd-selection" className="sw-body-sm">
</ButtonSecondary>
<ButtonPrimary
onClick={handleProjectCreation}
- disabled={!selectedDefinition?.isCompliant || submitting}
+ disabled={!selectedDefinition?.isCompliant || isLoading}
type="submit"
>
<FormattedMessage
count: projectCount,
}}
/>
- <Spinner className="sw-ml-2" loading={submitting} />
+ <Spinner className="sw-ml-2" loading={isLoading} />
</ButtonPrimary>
</div>
</div>
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 {
event.preventDefault();
const { projectKey, projectName, mainBranchName } = this.state;
if (this.canSubmit(this.state)) {
- this.props.onProjectSetupDone(
- setupManualProjectCreation({
- project: projectKey,
- name: (projectName || projectKey).trim(),
- mainBranch: mainBranchName,
- }),
- );
+ this.props.onProjectSetupDone({
+ creationMode: CreateProjectModes.Manual,
+ projects: [
+ {
+ project: projectKey,
+ name: (projectName || projectKey).trim(),
+ mainBranch: mainBranchName,
+ },
+ ],
+ });
}
};
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ProjectBase } from '../../../api/components';
-import { NewCodeDefinitionType } from '../../../types/new-code-definition';
export enum CreateProjectModes {
Manual = 'manual',
GitHub = 'github',
GitLab = 'gitlab',
}
-
-export type CreateProjectApiCallback = (
- newCodeDefinitionType?: NewCodeDefinitionType,
- newCodeDefinitionValue?: string,
-) => Promise<{ project: ProjectBase }>;
--- /dev/null
+/*
+ * 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 ImportProjectParam = ImportProjectParam> =
+ 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'] });
+}
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}.