/* * 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 React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { getGithubOrganizations, getGithubRepositories } from '../../../../api/alm-integrations'; import { useLocation, useRouter } from '../../../../components/hoc/withRouter'; import { GithubOrganization, GithubRepository } from '../../../../types/alm-integration'; import { AlmSettingsInstance } from '../../../../types/alm-settings'; import { DopSetting } from '../../../../types/dop-translation'; import { Paging } from '../../../../types/types'; import { ImportProjectParam } from '../CreateProjectPage'; import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate'; import { CreateProjectModes } from '../types'; import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer'; import { redirectToGithub } from './utils'; import { LabelValueSelectOption } from 'design-system'; interface Props { canAdmin: boolean; isLoadingBindings: boolean; onProjectSetupDone: (importProjects: ImportProjectParam) => void; dopSettings: DopSetting[]; } const REPOSITORY_PAGE_SIZE = 50; const REPOSITORY_SEARCH_DEBOUNCE_TIME = 250; export default function GitHubProjectCreate(props: Readonly) { const { canAdmin, dopSettings, isLoadingBindings, onProjectSetupDone } = props; const repositorySearchDebounceId = useRef(); const [isInError, setIsInError] = useState(false); const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(true); const [isLoadingRepositories, setIsLoadingRepositories] = useState(false); const [organizations, setOrganizations] = useState([]); const [repositories, setRepositories] = useState([]); const [repositoryPaging, setRepositoryPaging] = useState({ pageSize: REPOSITORY_PAGE_SIZE, total: 0, pageIndex: 1, }); const [searchQuery, setSearchQuery] = useState(''); const [selectedDopSetting, setSelectedDopSetting] = useState(); const [selectedOrganization, setSelectedOrganization] = useState(); const [selectedRepository, setSelectedRepository] = useState(); const location = useLocation(); const router = useRouter(); const isMonorepoSetup = location.query?.mono === 'true'; const hasDopSettings = Boolean(dopSettings?.length); const organizationOptions = useMemo(() => { return organizations.map(transformToOption); }, [organizations]); const repositoryOptions = useMemo(() => { return repositories.map(transformToOption); }, [repositories]); const fetchRepositories = useCallback( async (params: { organizationKey: string; page?: number; query?: string }) => { const { organizationKey, page = 1, query } = params; if (selectedDopSetting === undefined) { setIsInError(true); return; } setIsLoadingRepositories(true); try { const { paging, repositories } = await getGithubRepositories({ almSetting: selectedDopSetting.key, organization: organizationKey, pageSize: REPOSITORY_PAGE_SIZE, page, query, }); setRepositoryPaging(paging); setRepositories((prevRepositories) => page === 1 ? repositories : [...prevRepositories, ...repositories], ); } catch (_) { setRepositoryPaging({ pageIndex: 1, pageSize: REPOSITORY_PAGE_SIZE, total: 0 }); setRepositories([]); } finally { setIsLoadingRepositories(false); } }, [selectedDopSetting], ); const handleImportRepository = useCallback( (repoKeys: string[]) => { if (selectedDopSetting && selectedOrganization && repoKeys.length > 0) { onProjectSetupDone({ almSetting: selectedDopSetting.key, creationMode: CreateProjectModes.GitHub, monorepo: false, projects: repoKeys.map((repositoryKey) => ({ repositoryKey })), }); } }, [onProjectSetupDone, selectedDopSetting, selectedOrganization], ); const handleLoadMore = useCallback(() => { if (selectedOrganization) { fetchRepositories({ organizationKey: selectedOrganization.key, page: repositoryPaging.pageIndex + 1, query: searchQuery, }); } }, [fetchRepositories, repositoryPaging.pageIndex, searchQuery, selectedOrganization]); const handleSelectOrganization = useCallback( (organizationKey: string) => { setSearchQuery(''); setSelectedOrganization(organizations.find(({ key }) => key === organizationKey)); fetchRepositories({ organizationKey }); }, [fetchRepositories, organizations], ); const handleSelectRepository = useCallback( (repositoryIdentifier: string) => { setSelectedRepository(repositories.find(({ key }) => key === repositoryIdentifier)); }, [repositories], ); const authenticateToGithub = useCallback(async () => { try { await redirectToGithub({ isMonorepoSetup, selectedDopSetting }); } catch { setIsInError(true); } }, [isMonorepoSetup, selectedDopSetting]); const onSelectDopSetting = useCallback((setting: DopSetting | undefined) => { setSelectedDopSetting(setting); setOrganizations([]); setRepositories([]); setSearchQuery(''); }, []); const onSelectedAlmInstanceChange = useCallback( (instance: AlmSettingsInstance) => { onSelectDopSetting(dopSettings.find((dopSetting) => dopSetting.key === instance.key)); }, [dopSettings, onSelectDopSetting], ); useEffect(() => { const selectedDopSettingId = location.query?.dopSetting; if (selectedDopSettingId !== undefined) { const selectedDopSetting = dopSettings.find(({ id }) => id === selectedDopSettingId); if (selectedDopSetting) { setSelectedDopSetting(selectedDopSetting); } return; } if (dopSettings.length > 1) { setSelectedDopSetting(undefined); return; } setSelectedDopSetting(dopSettings[0]); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasDopSettings]); useEffect(() => { if (selectedDopSetting?.url === undefined) { setIsInError(true); return; } setIsInError(false); const code = location.query?.code; if (code === undefined) { authenticateToGithub().catch(() => { setIsInError(true); }); } else { delete location.query.code; router.replace(location); getGithubOrganizations(selectedDopSetting.key, code) .then(({ organizations }) => { setOrganizations(organizations); setIsLoadingOrganizations(false); }) .catch(() => { setIsInError(true); }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedDopSetting]); useEffect(() => { repositorySearchDebounceId.current = setTimeout(() => { if (selectedOrganization) { fetchRepositories({ organizationKey: selectedOrganization.key, query: searchQuery, }); } }, REPOSITORY_SEARCH_DEBOUNCE_TIME); return () => { clearTimeout(repositorySearchDebounceId.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQuery]); return isMonorepoSetup ? ( ) : ( ({ alm: type, key, url, }))} canAdmin={canAdmin} error={isInError} loadingBindings={isLoadingBindings} loadingOrganizations={isLoadingOrganizations} loadingRepositories={isLoadingRepositories} onImportRepository={handleImportRepository} onLoadMore={handleLoadMore} onSearch={setSearchQuery} onSelectedAlmInstanceChange={onSelectedAlmInstanceChange} onSelectOrganization={handleSelectOrganization} organizations={organizations} repositories={repositories} repositoryPaging={repositoryPaging} searchQuery={searchQuery} selectedAlmInstance={ selectedDopSetting && { alm: selectedDopSetting.type, key: selectedDopSetting.key, url: selectedDopSetting.url, } } selectedOrganization={selectedOrganization} /> ); } function transformToOption({ key, name, }: GithubOrganization | GithubRepository): LabelValueSelectOption { return { value: key, label: name }; }