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.

GitlabProjectCreate.tsx 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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 { orderBy } from 'lodash';
  22. import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
  23. import { getGitlabProjects } from '../../../../api/alm-integrations';
  24. import { useLocation, useRouter } from '../../../../components/hoc/withRouter';
  25. import { GitlabProject } from '../../../../types/alm-integration';
  26. import { AlmInstanceBase } from '../../../../types/alm-settings';
  27. import { DopSetting } from '../../../../types/dop-translation';
  28. import { Paging } from '../../../../types/types';
  29. import { ImportProjectParam } from '../CreateProjectPage';
  30. import MonorepoProjectCreate from '../monorepo/MonorepoProjectCreate';
  31. import { CreateProjectModes } from '../types';
  32. import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm';
  33. import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer';
  34. interface Props {
  35. canAdmin: boolean;
  36. isLoadingBindings: boolean;
  37. onProjectSetupDone: (importProjects: ImportProjectParam) => void;
  38. dopSettings: DopSetting[];
  39. }
  40. const REPOSITORY_PAGE_SIZE = 50;
  41. const REPOSITORY_SEARCH_DEBOUNCE_TIME = 250;
  42. export default function GitlabProjectCreate(props: Readonly<Props>) {
  43. const { canAdmin, dopSettings, isLoadingBindings, onProjectSetupDone } = props;
  44. const repositorySearchDebounceId = useRef<NodeJS.Timeout | undefined>();
  45. const [isLoadingRepositories, setIsLoadingRepositories] = useState(false);
  46. const [repositories, setRepositories] = useState<GitlabProject[]>([]);
  47. const [repositoryPaging, setRepositoryPaging] = useState<Paging>({
  48. pageSize: REPOSITORY_PAGE_SIZE,
  49. total: 0,
  50. pageIndex: 1,
  51. });
  52. const [searchQuery, setSearchQuery] = useState('');
  53. const [selectedDopSetting, setSelectedDopSetting] = useState<DopSetting>();
  54. const [selectedRepository, setSelectedRepository] = useState<GitlabProject>();
  55. const [resetPersonalAccessToken, setResetPersonalAccessToken] = useState<boolean>(false);
  56. const [showPersonalAccessTokenForm, setShowPersonalAccessTokenForm] = useState<boolean>(true);
  57. const location = useLocation();
  58. const router = useRouter();
  59. const isMonorepoSetup = location.query?.mono === 'true';
  60. const hasDopSettings = useMemo(() => {
  61. if (dopSettings === undefined) {
  62. return false;
  63. }
  64. return dopSettings.length > 0;
  65. }, [dopSettings]);
  66. const repositoryOptions = useMemo(() => {
  67. return repositories.map(transformToOption);
  68. }, [repositories]);
  69. const fetchProjects = useCallback(
  70. (pageIndex = 1, query?: string) => {
  71. if (!selectedDopSetting) {
  72. return Promise.resolve(undefined);
  73. }
  74. // eslint-disable-next-line local-rules/no-api-imports
  75. return getGitlabProjects({
  76. almSetting: selectedDopSetting.key,
  77. page: pageIndex,
  78. pageSize: REPOSITORY_PAGE_SIZE,
  79. query,
  80. });
  81. },
  82. [selectedDopSetting],
  83. );
  84. const fetchInitialData = useCallback(() => {
  85. if (!showPersonalAccessTokenForm) {
  86. setIsLoadingRepositories(true);
  87. fetchProjects()
  88. .then((result) => {
  89. if (result?.projects) {
  90. setIsLoadingRepositories(false);
  91. setRepositories(
  92. isMonorepoSetup
  93. ? orderBy(result.projects, [(res) => res.name.toLowerCase()], ['asc'])
  94. : result.projects,
  95. );
  96. setRepositoryPaging(result.projectsPaging);
  97. } else {
  98. setIsLoadingRepositories(false);
  99. }
  100. })
  101. .catch(() => {
  102. setResetPersonalAccessToken(true);
  103. setShowPersonalAccessTokenForm(true);
  104. });
  105. }
  106. }, [fetchProjects, isMonorepoSetup, showPersonalAccessTokenForm]);
  107. const cleanUrl = useCallback(() => {
  108. delete location.query.resetPat;
  109. router.replace(location);
  110. }, [location, router]);
  111. const handlePersonalAccessTokenCreated = useCallback(() => {
  112. cleanUrl();
  113. setShowPersonalAccessTokenForm(false);
  114. setResetPersonalAccessToken(false);
  115. fetchInitialData();
  116. }, [cleanUrl, fetchInitialData]);
  117. const handleImportRepository = useCallback(
  118. (repoKeys: string[]) => {
  119. if (selectedDopSetting && repoKeys.length > 0) {
  120. onProjectSetupDone({
  121. almSetting: selectedDopSetting.key,
  122. creationMode: CreateProjectModes.GitLab,
  123. monorepo: false,
  124. projects: repoKeys.map((repoKeys) => ({ gitlabProjectId: repoKeys })),
  125. });
  126. }
  127. },
  128. [onProjectSetupDone, selectedDopSetting],
  129. );
  130. const handleLoadMore = useCallback(async () => {
  131. const result = await fetchProjects(repositoryPaging.pageIndex + 1, searchQuery);
  132. if (result?.projects) {
  133. setRepositoryPaging(result ? result.projectsPaging : repositoryPaging);
  134. setRepositories(result ? [...repositories, ...result.projects] : repositories);
  135. }
  136. }, [fetchProjects, repositories, repositoryPaging, searchQuery]);
  137. const handleSelectRepository = useCallback(
  138. (repositoryKey: string) => {
  139. setSelectedRepository(repositories.find(({ id }) => id === repositoryKey));
  140. },
  141. [repositories],
  142. );
  143. const onSelectDopSetting = useCallback((setting: DopSetting | undefined) => {
  144. setSelectedDopSetting(setting);
  145. setShowPersonalAccessTokenForm(true);
  146. setRepositories([]);
  147. setSearchQuery('');
  148. }, []);
  149. const onSelectedAlmInstanceChange = useCallback(
  150. (instance: AlmInstanceBase) => {
  151. onSelectDopSetting(dopSettings.find((dopSetting) => dopSetting.key === instance.key));
  152. },
  153. [dopSettings, onSelectDopSetting],
  154. );
  155. useEffect(() => {
  156. if (dopSettings.length > 0) {
  157. setSelectedDopSetting(dopSettings[0]);
  158. return;
  159. }
  160. setSelectedDopSetting(undefined);
  161. // eslint-disable-next-line react-hooks/exhaustive-deps
  162. }, [hasDopSettings]);
  163. useEffect(() => {
  164. if (selectedDopSetting) {
  165. fetchInitialData();
  166. }
  167. }, [fetchInitialData, selectedDopSetting]);
  168. useEffect(() => {
  169. repositorySearchDebounceId.current = setTimeout(async () => {
  170. const result = await fetchProjects(1, searchQuery);
  171. if (result?.projects) {
  172. setRepositories(orderBy(result.projects, [(res) => res.name.toLowerCase()], ['asc']));
  173. setRepositoryPaging(result.projectsPaging);
  174. }
  175. }, REPOSITORY_SEARCH_DEBOUNCE_TIME);
  176. return () => {
  177. clearTimeout(repositorySearchDebounceId.current);
  178. };
  179. // eslint-disable-next-line react-hooks/exhaustive-deps
  180. }, [searchQuery]);
  181. return isMonorepoSetup ? (
  182. <MonorepoProjectCreate
  183. canAdmin={canAdmin}
  184. dopSettings={dopSettings}
  185. error={false}
  186. loadingBindings={isLoadingBindings}
  187. loadingOrganizations={false}
  188. loadingRepositories={isLoadingRepositories}
  189. onProjectSetupDone={onProjectSetupDone}
  190. onSearchRepositories={setSearchQuery}
  191. onSelectDopSetting={onSelectDopSetting}
  192. onSelectRepository={handleSelectRepository}
  193. personalAccessTokenComponent={
  194. !isLoadingRepositories &&
  195. selectedDopSetting && (
  196. <GitlabPersonalAccessTokenForm
  197. almSetting={selectedDopSetting}
  198. resetPat={resetPersonalAccessToken}
  199. onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated}
  200. />
  201. )
  202. }
  203. repositoryOptions={repositoryOptions}
  204. repositorySearchQuery={searchQuery}
  205. selectedDopSetting={selectedDopSetting}
  206. selectedRepository={selectedRepository ? transformToOption(selectedRepository) : undefined}
  207. showPersonalAccessToken={showPersonalAccessTokenForm || Boolean(location.query.resetPat)}
  208. />
  209. ) : (
  210. <GitlabProjectCreateRenderer
  211. almInstances={dopSettings.map((dopSetting) => ({
  212. alm: dopSetting.type,
  213. key: dopSetting.key,
  214. url: dopSetting.url,
  215. }))}
  216. canAdmin={canAdmin}
  217. loading={isLoadingRepositories || isLoadingBindings}
  218. onImport={handleImportRepository}
  219. onLoadMore={handleLoadMore}
  220. onPersonalAccessTokenCreated={handlePersonalAccessTokenCreated}
  221. onSearch={setSearchQuery}
  222. onSelectedAlmInstanceChange={onSelectedAlmInstanceChange}
  223. projects={repositories}
  224. projectsPaging={repositoryPaging}
  225. resetPat={resetPersonalAccessToken || Boolean(location.query.resetPat)}
  226. searchQuery={searchQuery}
  227. selectedAlmInstance={
  228. selectedDopSetting && {
  229. alm: selectedDopSetting.type,
  230. key: selectedDopSetting.key,
  231. url: selectedDopSetting.url,
  232. }
  233. }
  234. showPersonalAccessTokenForm={showPersonalAccessTokenForm || Boolean(location.query.resetPat)}
  235. />
  236. );
  237. }
  238. function transformToOption({ id, name }: GitlabProject): LabelValueSelectOption<string> {
  239. return { value: id, label: name };
  240. }