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 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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 { LabelValueSelectOption } from 'design-system';
  21. import React, { useCallback, useEffect, useMemo, useState } from 'react';
  22. import { getGithubOrganizations, getGithubRepositories } from '../../../../api/alm-integrations';
  23. import { useLocation, useRouter } from '../../../../components/hoc/withRouter';
  24. import { GithubOrganization, GithubRepository } from '../../../../types/alm-integration';
  25. import { AlmInstanceBase, AlmKeys } from '../../../../types/alm-settings';
  26. import { DopSetting } from '../../../../types/dop-translation';
  27. import { ImportProjectParam } from '../CreateProjectPage';
  28. import { REPOSITORY_PAGE_SIZE } from '../constants';
  29. import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate';
  30. import { CreateProjectModes } from '../types';
  31. import { useProjectCreate } from '../useProjectCreate';
  32. import { useProjectRepositorySearch } from '../useProjectRepositorySearch';
  33. import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';
  34. import { redirectToGithub } from './utils';
  35. interface Props {
  36. isLoadingBindings: boolean;
  37. onProjectSetupDone: (importProjects: ImportProjectParam) => void;
  38. dopSettings: DopSetting[];
  39. }
  40. export default function GitHubProjectCreate(props: Readonly<Props>) {
  41. const { dopSettings, isLoadingBindings, onProjectSetupDone } = props;
  42. const {
  43. handleSelectRepository,
  44. isInitialized,
  45. isLoadingOrganizations,
  46. isLoadingRepositories,
  47. isMonorepoSetup,
  48. onSelectedAlmInstanceChange,
  49. onSelectDopSetting,
  50. projectsPaging,
  51. organizations,
  52. repositories,
  53. searchQuery,
  54. selectedDopSetting,
  55. selectedRepository,
  56. setIsInitialized,
  57. setIsLoadingRepositories,
  58. setProjectsPaging,
  59. setOrganizations,
  60. setRepositories,
  61. setSearchQuery,
  62. setSelectedOrganization,
  63. selectedOrganization,
  64. setIsLoadingOrganizations,
  65. } = useProjectCreate<GithubRepository, GithubOrganization>(
  66. AlmKeys.GitHub,
  67. dopSettings,
  68. ({ key }) => key,
  69. REPOSITORY_PAGE_SIZE,
  70. );
  71. const [isInError, setIsInError] = useState(false);
  72. const [isAuthenticated, setIsAuthenticated] = useState(false);
  73. const location = useLocation();
  74. const router = useRouter();
  75. const organizationOptions = useMemo(() => {
  76. return organizations.map(transformToOption);
  77. }, [organizations]);
  78. const repositoryOptions = useMemo(() => {
  79. return repositories.map(transformToOption);
  80. }, [repositories]);
  81. const fetchRepositories = useCallback(
  82. (orgKey: string, query?: string, pageIndex = 1) => {
  83. if (selectedDopSetting === undefined) {
  84. setIsInError(true);
  85. return Promise.resolve();
  86. }
  87. setIsLoadingRepositories(true);
  88. return getGithubRepositories({
  89. almSetting: selectedDopSetting.key,
  90. organization: orgKey,
  91. pageSize: REPOSITORY_PAGE_SIZE,
  92. page: pageIndex,
  93. query,
  94. })
  95. .then(({ paging, repositories }) => {
  96. setProjectsPaging(paging);
  97. setRepositories((prevRepositories) =>
  98. pageIndex === 1 ? repositories : [...prevRepositories, ...repositories],
  99. );
  100. setIsInitialized(true);
  101. })
  102. .finally(() => {
  103. setIsLoadingRepositories(false);
  104. })
  105. .catch(() => {
  106. setProjectsPaging({ pageIndex: 1, pageSize: REPOSITORY_PAGE_SIZE, total: 0 });
  107. setRepositories([]);
  108. });
  109. },
  110. [
  111. selectedDopSetting,
  112. setIsInitialized,
  113. setIsLoadingRepositories,
  114. setProjectsPaging,
  115. setRepositories,
  116. ],
  117. );
  118. const onSelectDopSettingReauthenticate = useCallback(
  119. (setting?: DopSetting) => {
  120. onSelectDopSetting(setting);
  121. setIsAuthenticated(false);
  122. },
  123. [onSelectDopSetting],
  124. );
  125. const onSelectAlmSettingReauthenticate = useCallback(
  126. (setting?: AlmInstanceBase) => {
  127. onSelectedAlmInstanceChange(setting);
  128. setIsAuthenticated(false);
  129. },
  130. [onSelectedAlmInstanceChange],
  131. );
  132. const handleImportRepository = useCallback(
  133. (repoKeys: string[]) => {
  134. if (selectedDopSetting && selectedOrganization && repoKeys.length > 0) {
  135. onProjectSetupDone({
  136. almSetting: selectedDopSetting.key,
  137. creationMode: CreateProjectModes.GitHub,
  138. monorepo: false,
  139. projects: repoKeys.map((repositoryKey) => ({ repositoryKey })),
  140. });
  141. }
  142. },
  143. [onProjectSetupDone, selectedDopSetting, selectedOrganization],
  144. );
  145. const handleLoadMore = useCallback(() => {
  146. if (selectedOrganization) {
  147. fetchRepositories(selectedOrganization.key, searchQuery, projectsPaging.pageIndex + 1);
  148. }
  149. }, [fetchRepositories, projectsPaging.pageIndex, searchQuery, selectedOrganization]);
  150. const handleSelectOrganization = useCallback(
  151. (organizationKey: string) => {
  152. setSearchQuery('');
  153. setSelectedOrganization(organizations.find(({ key }) => key === organizationKey));
  154. },
  155. [organizations, setSearchQuery, setSelectedOrganization],
  156. );
  157. useEffect(() => {
  158. if (selectedDopSetting?.url === undefined) {
  159. setIsInError(true);
  160. return;
  161. }
  162. setIsInError(false);
  163. const code = location.query?.code;
  164. if (!isAuthenticated) {
  165. if (code === undefined) {
  166. redirectToGithub({ isMonorepoSetup, selectedDopSetting }).catch(() => {
  167. setIsInError(true);
  168. });
  169. } else {
  170. setIsAuthenticated(true);
  171. delete location.query.code;
  172. router.replace(location);
  173. getGithubOrganizations(selectedDopSetting.key, code)
  174. .then(({ organizations }) => {
  175. setOrganizations(organizations);
  176. setIsLoadingOrganizations(false);
  177. })
  178. .catch(() => {
  179. setIsInError(true);
  180. });
  181. }
  182. }
  183. // Disabling rule as it causes an infinite loop and should only be called for dopSetting changes.
  184. // eslint-disable-next-line react-hooks/exhaustive-deps
  185. }, [selectedDopSetting]);
  186. const { onSearch } = useProjectRepositorySearch(
  187. AlmKeys.GitHub,
  188. fetchRepositories,
  189. isInitialized,
  190. selectedDopSetting,
  191. selectedOrganization?.key,
  192. setSearchQuery,
  193. );
  194. return isMonorepoSetup ? (
  195. <MonorepoProjectCreate
  196. dopSettings={dopSettings}
  197. error={isInError}
  198. loadingBindings={isLoadingBindings}
  199. loadingOrganizations={isLoadingOrganizations}
  200. loadingRepositories={isLoadingRepositories}
  201. onProjectSetupDone={onProjectSetupDone}
  202. onSearchRepositories={onSearch}
  203. onSelectDopSetting={onSelectDopSettingReauthenticate}
  204. onSelectOrganization={handleSelectOrganization}
  205. onSelectRepository={handleSelectRepository}
  206. organizationOptions={organizationOptions}
  207. repositoryOptions={repositoryOptions}
  208. repositorySearchQuery={searchQuery}
  209. selectedDopSetting={selectedDopSetting}
  210. selectedOrganization={selectedOrganization && transformToOption(selectedOrganization)}
  211. selectedRepository={selectedRepository && transformToOption(selectedRepository)}
  212. showOrganizations
  213. />
  214. ) : (
  215. <GitHubProjectCreateRenderer
  216. almInstances={dopSettings.map(({ key, type, url }) => ({
  217. alm: type,
  218. key,
  219. url,
  220. }))}
  221. error={isInError}
  222. loadingBindings={isLoadingBindings}
  223. loadingOrganizations={isLoadingOrganizations}
  224. loadingRepositories={isLoadingRepositories}
  225. onImportRepository={handleImportRepository}
  226. onLoadMore={handleLoadMore}
  227. onSearch={onSearch}
  228. onSelectedAlmInstanceChange={onSelectAlmSettingReauthenticate}
  229. onSelectOrganization={handleSelectOrganization}
  230. organizations={organizations}
  231. repositories={repositories}
  232. repositoryPaging={projectsPaging}
  233. searchQuery={searchQuery}
  234. selectedAlmInstance={
  235. selectedDopSetting && {
  236. alm: selectedDopSetting.type,
  237. key: selectedDopSetting.key,
  238. url: selectedDopSetting.url,
  239. }
  240. }
  241. selectedOrganization={selectedOrganization}
  242. />
  243. );
  244. }
  245. function transformToOption({
  246. key,
  247. name,
  248. }: GithubOrganization | GithubRepository): LabelValueSelectOption<string> {
  249. return { value: key, label: name };
  250. }