You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GitHubProjectCreate.tsx 10.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
  21. import { getGithubOrganizations, getGithubRepositories } from '../../../../api/alm-integrations';
  22. import { useLocation, useRouter } from '../../../../components/hoc/withRouter';
  23. import { LabelValueSelectOption } from '../../../../helpers/search';
  24. import { GithubOrganization, GithubRepository } from '../../../../types/alm-integration';
  25. import { AlmSettingsInstance } from '../../../../types/alm-settings';
  26. import { DopSetting } from '../../../../types/dop-translation';
  27. import { Paging } from '../../../../types/types';
  28. import { ImportProjectParam } from '../CreateProjectPage';
  29. import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate';
  30. import { CreateProjectModes } from '../types';
  31. import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';
  32. import { redirectToGithub } from './utils';
  33. interface Props {
  34. canAdmin: boolean;
  35. isLoadingBindings: boolean;
  36. onProjectSetupDone: (importProjects: ImportProjectParam) => void;
  37. dopSettings: DopSetting[];
  38. }
  39. const REPOSITORY_PAGE_SIZE = 50;
  40. const REPOSITORY_SEARCH_DEBOUNCE_TIME = 250;
  41. export default function GitHubProjectCreate(props: Readonly<Props>) {
  42. const { canAdmin, dopSettings, isLoadingBindings, onProjectSetupDone } = props;
  43. const repositorySearchDebounceId = useRef<NodeJS.Timeout | undefined>();
  44. const [isInError, setIsInError] = useState(false);
  45. const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(true);
  46. const [isLoadingRepositories, setIsLoadingRepositories] = useState(false);
  47. const [organizations, setOrganizations] = useState<GithubOrganization[]>([]);
  48. const [repositories, setRepositories] = useState<GithubRepository[]>([]);
  49. const [repositoryPaging, setRepositoryPaging] = useState<Paging>({
  50. pageSize: REPOSITORY_PAGE_SIZE,
  51. total: 0,
  52. pageIndex: 1,
  53. });
  54. const [searchQuery, setSearchQuery] = useState('');
  55. const [selectedDopSetting, setSelectedDopSetting] = useState<DopSetting>();
  56. const [selectedOrganization, setSelectedOrganization] = useState<GithubOrganization>();
  57. const [selectedRepository, setSelectedRepository] = useState<GithubRepository>();
  58. const location = useLocation();
  59. const router = useRouter();
  60. const isMonorepoSetup = location.query?.mono === 'true';
  61. const hasDopSettings = Boolean(dopSettings?.length);
  62. const organizationOptions = useMemo(() => {
  63. return organizations.map(transformToOption);
  64. }, [organizations]);
  65. const repositoryOptions = useMemo(() => {
  66. return repositories.map(transformToOption);
  67. }, [repositories]);
  68. const fetchRepositories = useCallback(
  69. async (params: { organizationKey: string; page?: number; query?: string }) => {
  70. const { organizationKey, page = 1, query } = params;
  71. if (selectedDopSetting === undefined) {
  72. setIsInError(true);
  73. return;
  74. }
  75. setIsLoadingRepositories(true);
  76. try {
  77. const { paging, repositories } = await getGithubRepositories({
  78. almSetting: selectedDopSetting.key,
  79. organization: organizationKey,
  80. pageSize: REPOSITORY_PAGE_SIZE,
  81. page,
  82. query,
  83. });
  84. setRepositoryPaging(paging);
  85. setRepositories((prevRepositories) =>
  86. page === 1 ? repositories : [...prevRepositories, ...repositories],
  87. );
  88. } catch (_) {
  89. setRepositoryPaging({ pageIndex: 1, pageSize: REPOSITORY_PAGE_SIZE, total: 0 });
  90. setRepositories([]);
  91. } finally {
  92. setIsLoadingRepositories(false);
  93. }
  94. },
  95. [selectedDopSetting],
  96. );
  97. const handleImportRepository = useCallback(
  98. (repoKeys: string[]) => {
  99. if (selectedDopSetting && selectedOrganization && repoKeys.length > 0) {
  100. onProjectSetupDone({
  101. almSetting: selectedDopSetting.key,
  102. creationMode: CreateProjectModes.GitHub,
  103. monorepo: false,
  104. projects: repoKeys.map((repositoryKey) => ({ repositoryKey })),
  105. });
  106. }
  107. },
  108. [onProjectSetupDone, selectedDopSetting, selectedOrganization],
  109. );
  110. const handleLoadMore = useCallback(() => {
  111. if (selectedOrganization) {
  112. fetchRepositories({
  113. organizationKey: selectedOrganization.key,
  114. page: repositoryPaging.pageIndex + 1,
  115. query: searchQuery,
  116. });
  117. }
  118. }, [fetchRepositories, repositoryPaging.pageIndex, searchQuery, selectedOrganization]);
  119. const handleSelectOrganization = useCallback(
  120. (organizationKey: string) => {
  121. setSearchQuery('');
  122. setSelectedOrganization(organizations.find(({ key }) => key === organizationKey));
  123. fetchRepositories({ organizationKey });
  124. },
  125. [fetchRepositories, organizations],
  126. );
  127. const handleSelectRepository = useCallback(
  128. (repositoryIdentifier: string) => {
  129. setSelectedRepository(repositories.find(({ key }) => key === repositoryIdentifier));
  130. },
  131. [repositories],
  132. );
  133. const authenticateToGithub = useCallback(async () => {
  134. try {
  135. await redirectToGithub({ isMonorepoSetup, selectedDopSetting });
  136. } catch {
  137. setIsInError(true);
  138. }
  139. }, [isMonorepoSetup, selectedDopSetting]);
  140. const onSelectDopSetting = useCallback((setting: DopSetting | undefined) => {
  141. setSelectedDopSetting(setting);
  142. setOrganizations([]);
  143. setRepositories([]);
  144. setSearchQuery('');
  145. }, []);
  146. const onSelectedAlmInstanceChange = useCallback(
  147. (instance: AlmSettingsInstance) => {
  148. onSelectDopSetting(dopSettings.find((dopSetting) => dopSetting.key === instance.key));
  149. },
  150. [dopSettings, onSelectDopSetting],
  151. );
  152. useEffect(() => {
  153. const selectedDopSettingId = location.query?.dopSetting;
  154. if (selectedDopSettingId !== undefined) {
  155. const selectedDopSetting = dopSettings.find(({ id }) => id === selectedDopSettingId);
  156. if (selectedDopSetting) {
  157. setSelectedDopSetting(selectedDopSetting);
  158. }
  159. return;
  160. }
  161. if (dopSettings.length > 1) {
  162. setSelectedDopSetting(undefined);
  163. return;
  164. }
  165. setSelectedDopSetting(dopSettings[0]);
  166. // eslint-disable-next-line react-hooks/exhaustive-deps
  167. }, [hasDopSettings]);
  168. useEffect(() => {
  169. if (selectedDopSetting?.url === undefined) {
  170. setIsInError(true);
  171. return;
  172. }
  173. setIsInError(false);
  174. const code = location.query?.code;
  175. if (code === undefined) {
  176. authenticateToGithub().catch(() => {
  177. setIsInError(true);
  178. });
  179. } else {
  180. delete location.query.code;
  181. router.replace(location);
  182. getGithubOrganizations(selectedDopSetting.key, code)
  183. .then(({ organizations }) => {
  184. setOrganizations(organizations);
  185. setIsLoadingOrganizations(false);
  186. })
  187. .catch(() => {
  188. setIsInError(true);
  189. });
  190. }
  191. // eslint-disable-next-line react-hooks/exhaustive-deps
  192. }, [selectedDopSetting]);
  193. useEffect(() => {
  194. repositorySearchDebounceId.current = setTimeout(() => {
  195. if (selectedOrganization) {
  196. fetchRepositories({
  197. organizationKey: selectedOrganization.key,
  198. query: searchQuery,
  199. });
  200. }
  201. }, REPOSITORY_SEARCH_DEBOUNCE_TIME);
  202. return () => {
  203. clearTimeout(repositorySearchDebounceId.current);
  204. };
  205. // eslint-disable-next-line react-hooks/exhaustive-deps
  206. }, [searchQuery]);
  207. return isMonorepoSetup ? (
  208. <MonorepoProjectCreate
  209. dopSettings={dopSettings}
  210. canAdmin={canAdmin}
  211. error={isInError}
  212. loadingBindings={isLoadingBindings}
  213. loadingOrganizations={isLoadingOrganizations}
  214. loadingRepositories={isLoadingRepositories}
  215. onProjectSetupDone={onProjectSetupDone}
  216. onSearchRepositories={setSearchQuery}
  217. onSelectDopSetting={onSelectDopSetting}
  218. onSelectOrganization={handleSelectOrganization}
  219. onSelectRepository={handleSelectRepository}
  220. organizationOptions={organizationOptions}
  221. repositoryOptions={repositoryOptions}
  222. repositorySearchQuery={searchQuery}
  223. selectedDopSetting={selectedDopSetting}
  224. selectedOrganization={selectedOrganization && transformToOption(selectedOrganization)}
  225. selectedRepository={selectedRepository && transformToOption(selectedRepository)}
  226. />
  227. ) : (
  228. <GitHubProjectCreateRenderer
  229. almInstances={dopSettings.map(({ key, type, url }) => ({
  230. alm: type,
  231. key,
  232. url,
  233. }))}
  234. canAdmin={canAdmin}
  235. error={isInError}
  236. loadingBindings={isLoadingBindings}
  237. loadingOrganizations={isLoadingOrganizations}
  238. loadingRepositories={isLoadingRepositories}
  239. onImportRepository={handleImportRepository}
  240. onLoadMore={handleLoadMore}
  241. onSearch={setSearchQuery}
  242. onSelectedAlmInstanceChange={onSelectedAlmInstanceChange}
  243. onSelectOrganization={handleSelectOrganization}
  244. organizations={organizations}
  245. repositories={repositories}
  246. repositoryPaging={repositoryPaging}
  247. searchQuery={searchQuery}
  248. selectedAlmInstance={
  249. selectedDopSetting && {
  250. alm: selectedDopSetting.type,
  251. key: selectedDopSetting.key,
  252. url: selectedDopSetting.url,
  253. }
  254. }
  255. selectedOrganization={selectedOrganization}
  256. />
  257. );
  258. }
  259. function transformToOption({
  260. key,
  261. name,
  262. }: GithubOrganization | GithubRepository): LabelValueSelectOption {
  263. return { value: key, label: name };
  264. }