/* * SonarQube * Copyright (C) 2009-2024 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 { LabelValueSelectOption } from 'design-system'; import React, { useCallback, useMemo, useState } from 'react'; import { GroupBase } from 'react-select'; import { useLocation } from '~sonar-aligned/components/hoc/withRouter'; import { getAzureProjects, getAzureRepositories, searchAzureRepositories, } from '../../../../api/alm-integrations'; import { AzureProject, AzureRepository } from '../../../../types/alm-integration'; import { AlmKeys } from '../../../../types/alm-settings'; import { DopSetting } from '../../../../types/dop-translation'; import { Dict } from '../../../../types/types'; import { ImportProjectParam } from '../CreateProjectPage'; import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate'; import { CreateProjectModes } from '../types'; import { useProjectCreate } from '../useProjectCreate'; import { useProjectRepositorySearch } from '../useProjectRepositorySearch'; import AzurePersonalAccessTokenForm from './AzurePersonalAccessTokenForm'; import AzureCreateProjectRenderer from './AzureProjectCreateRenderer'; interface Props { dopSettings: DopSetting[]; isLoadingBindings: boolean; onProjectSetupDone: (importProjects: ImportProjectParam) => void; } export default function AzureProjectCreate({ dopSettings, isLoadingBindings, onProjectSetupDone, }: Readonly) { const { almInstances, handlePersonalAccessTokenCreated, handleSelectRepository: defaultRepositorySelect, isLoadingRepositories, isMonorepoSetup, onSelectedAlmInstanceChange, organizations: projects, repositories, searchQuery, selectedAlmInstance, selectedDopSetting, selectedRepository, setSearchQuery, setIsLoadingRepositories, setOrganizations: setProjects, setRepositories, setSelectedDopSetting, setSelectedRepository, setShowPersonalAccessTokenForm, showPersonalAccessTokenForm, } = useProjectCreate, AzureProject>( AlmKeys.Azure, dopSettings, ({ name }) => name, ); const [loadingRepositories, setLoadingRepositories] = useState>({}); const location = useLocation(); const fetchAzureProjects = useCallback(async (): Promise => { if (selectedDopSetting === undefined) { return undefined; } const azureProjects = await getAzureProjects(selectedDopSetting.key); return azureProjects.projects; }, [selectedDopSetting]); const fetchAzureRepositories = useCallback( async (projectName: string): Promise => { if (!selectedDopSetting) { return []; } try { const azureRepositories = await getAzureRepositories(selectedDopSetting.key, projectName); return azureRepositories.repositories; } catch { return []; } }, [selectedDopSetting], ); const fetchData = useCallback(async () => { if (showPersonalAccessTokenForm) { return; } setIsLoadingRepositories(true); let projects: AzureProject[] | undefined; try { projects = await fetchAzureProjects(); } catch (_) { setShowPersonalAccessTokenForm(true); setIsLoadingRepositories(false); return; } if (projects && projects.length > 0) { if (isMonorepoSetup) { // Load every projects repos if we're in monorepo setup projects.forEach(async (project) => { setLoadingRepositories((loadingRepositories) => ({ ...loadingRepositories, [project.name]: true, })); try { const repos = await fetchAzureRepositories(project.name); setRepositories((repositories) => ({ ...repositories, [project.name]: repos, })); } finally { setLoadingRepositories((loadingRepositories) => { loadingRepositories[project.name] = false; return { ...loadingRepositories }; }); } }); } else { const firstProjectName = projects[0].name; setLoadingRepositories((loadingRepositories) => ({ ...loadingRepositories, [firstProjectName]: true, })); const repos = await fetchAzureRepositories(firstProjectName); setLoadingRepositories((loadingRepositories) => { loadingRepositories[firstProjectName] = false; return { ...loadingRepositories }; }); setRepositories((repositories) => ({ ...repositories, [firstProjectName]: repos })); } } setProjects(projects); setIsLoadingRepositories(false); }, [ fetchAzureProjects, fetchAzureRepositories, isMonorepoSetup, setIsLoadingRepositories, setProjects, setRepositories, setShowPersonalAccessTokenForm, showPersonalAccessTokenForm, ]); const handleImportRepository = useCallback( (selectedRepository: AzureRepository) => { if (selectedDopSetting !== undefined && selectedRepository !== undefined) { onProjectSetupDone({ creationMode: CreateProjectModes.AzureDevOps, almSetting: selectedDopSetting.key, monorepo: false, projects: [ { projectName: selectedRepository.projectName, repositoryName: selectedRepository.name, }, ], }); } }, [onProjectSetupDone, selectedDopSetting], ); const handleMonorepoSetupDone = useCallback( (monorepoSetup: ImportProjectParam) => { const azureMonorepoSetup = { ...monorepoSetup, projectIdentifier: selectedRepository?.projectName, }; onProjectSetupDone(azureMonorepoSetup); }, [onProjectSetupDone, selectedRepository?.projectName], ); const { isSearching, onSearch, onSelectRepository, searchResults } = useProjectRepositorySearch({ defaultRepositorySelect, fetchData, fetchSearchResults: (query: string, dopKey: string) => searchAzureRepositories(dopKey, query), getRepositoryKey: ({ name }) => name, isMonorepoSetup, selectedDopSetting, setSearchQuery, setSelectedRepository, setShowPersonalAccessTokenForm, }); const handleOpenProject = useCallback( async (projectName: string) => { if (searchResults !== undefined) { return; } setLoadingRepositories((loadingRepositories) => ({ ...loadingRepositories, [projectName]: true, })); const projectRepos = await fetchAzureRepositories(projectName); setLoadingRepositories((loadingRepositories) => ({ ...loadingRepositories, [projectName]: false, })); setRepositories((repositories) => ({ ...repositories, [projectName]: projectRepos })); }, [fetchAzureRepositories, searchResults, setRepositories], ); const repositoryOptions = useMemo(() => { if (searchResults) { const dict = projects?.reduce((acc: Dict, { name }) => { return { ...acc, [name]: searchResults?.filter((o) => o.projectName === name) }; }, {}); return transformToOptions(projects ?? [], dict); } return transformToOptions(projects ?? [], repositories); }, [projects, repositories, searchResults]); return isMonorepoSetup ? ( ) } repositoryOptions={repositoryOptions} repositorySearchQuery={searchQuery} selectedDopSetting={selectedDopSetting} selectedRepository={selectedRepository ? transformToOption(selectedRepository) : undefined} showPersonalAccessToken={showPersonalAccessTokenForm || Boolean(location.query.resetPat)} /> ) : ( ); } function transformToOptions( projects: AzureProject[], repositories?: Dict, ): Array> { return projects.map(({ name: projectName }) => ({ label: projectName, options: repositories?.[projectName] !== undefined ? repositories[projectName].map(transformToOption) : [], })); } function transformToOption({ name }: AzureRepository): LabelValueSelectOption { return { value: name, label: name }; }