Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

BitbucketProjectCreate.tsx 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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 * as React from 'react';
  21. import {
  22. getBitbucketServerProjects,
  23. getBitbucketServerRepositories,
  24. searchForBitbucketServerRepositories,
  25. } from '../../../../api/alm-integrations';
  26. import { Location, Router } from '../../../../components/hoc/withRouter';
  27. import {
  28. BitbucketProject,
  29. BitbucketProjectRepositories,
  30. BitbucketRepository,
  31. } from '../../../../types/alm-integration';
  32. import { AlmSettingsInstance } from '../../../../types/alm-settings';
  33. import { ImportProjectParam } from '../CreateProjectPage';
  34. import { DEFAULT_BBS_PAGE_SIZE } from '../constants';
  35. import { CreateProjectModes } from '../types';
  36. import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer';
  37. interface Props {
  38. almInstances: AlmSettingsInstance[];
  39. loadingBindings: boolean;
  40. location: Location;
  41. router: Router;
  42. onProjectSetupDone: (importProjects: ImportProjectParam) => void;
  43. }
  44. interface State {
  45. selectedAlmInstance?: AlmSettingsInstance;
  46. loading: boolean;
  47. projects?: BitbucketProject[];
  48. projectRepositories?: BitbucketProjectRepositories;
  49. searching: boolean;
  50. searchResults?: BitbucketRepository[];
  51. showPersonalAccessTokenForm: boolean;
  52. }
  53. export default class BitbucketProjectCreate extends React.PureComponent<Props, State> {
  54. mounted = false;
  55. constructor(props: Props) {
  56. super(props);
  57. this.state = {
  58. selectedAlmInstance: props.almInstances[0],
  59. loading: false,
  60. searching: false,
  61. showPersonalAccessTokenForm: true,
  62. };
  63. }
  64. componentDidMount() {
  65. this.mounted = true;
  66. }
  67. componentDidUpdate(prevProps: Props) {
  68. if (prevProps.almInstances.length === 0 && this.props.almInstances.length > 0) {
  69. this.setState({ selectedAlmInstance: this.props.almInstances[0] }, () => {
  70. this.fetchInitialData().catch(() => {
  71. /* noop */
  72. });
  73. });
  74. }
  75. }
  76. componentWillUnmount() {
  77. this.mounted = false;
  78. }
  79. fetchInitialData = async () => {
  80. const { showPersonalAccessTokenForm } = this.state;
  81. if (!showPersonalAccessTokenForm) {
  82. this.setState({ loading: true });
  83. const projects = await this.fetchBitbucketProjects().catch(() => undefined);
  84. let projectRepositories;
  85. if (projects && projects.length > 0) {
  86. projectRepositories = await this.fetchBitbucketRepositories(projects).catch(
  87. () => undefined,
  88. );
  89. }
  90. if (this.mounted) {
  91. this.setState({
  92. projects,
  93. projectRepositories,
  94. loading: false,
  95. });
  96. }
  97. }
  98. };
  99. fetchBitbucketProjects = (): Promise<BitbucketProject[] | undefined> => {
  100. const { selectedAlmInstance } = this.state;
  101. if (!selectedAlmInstance) {
  102. return Promise.resolve(undefined);
  103. }
  104. return getBitbucketServerProjects(selectedAlmInstance.key).then(({ projects }) => projects);
  105. };
  106. fetchBitbucketRepositories = (
  107. projects: BitbucketProject[],
  108. ): Promise<BitbucketProjectRepositories | undefined> => {
  109. const { selectedAlmInstance } = this.state;
  110. if (!selectedAlmInstance) {
  111. return Promise.resolve(undefined);
  112. }
  113. return Promise.all(
  114. projects.map((p) => {
  115. return getBitbucketServerRepositories(selectedAlmInstance.key, p.name).then(
  116. ({ isLastPage, repositories }) => {
  117. // Because the WS uses the project name rather than its key to find
  118. // repositories, we can match more repositories than we expect. For
  119. // example, p.name = "A1" would find repositories for projects "A1",
  120. // "A10", "A11", etc. This is a limitation of BBS. To make sure we
  121. // don't display incorrect information, filter on the project key.
  122. const filteredRepositories = repositories.filter((r) => r.projectKey === p.key);
  123. // And because of the above, the "isLastPage" cannot be relied upon
  124. // either. This one is impossible to get 100% for now. We can only
  125. // make some assumptions: by default, the page size for BBS is 25
  126. // (this is not part of the payload, so we don't know the actual
  127. // number; but changing this implies changing some advanced config,
  128. // so it's not likely). If the filtered repos is larger than this
  129. // number AND isLastPage is false, we'll keep it at false.
  130. // Otherwise, we assume it's true.
  131. const realIsLastPage =
  132. isLastPage || filteredRepositories.length < DEFAULT_BBS_PAGE_SIZE;
  133. return {
  134. repositories: filteredRepositories,
  135. isLastPage: realIsLastPage,
  136. projectKey: p.key,
  137. };
  138. },
  139. );
  140. }),
  141. ).then((results) => {
  142. return results.reduce(
  143. (acc: BitbucketProjectRepositories, { isLastPage, projectKey, repositories }) => {
  144. return { ...acc, [projectKey]: { allShown: isLastPage, repositories } };
  145. },
  146. {},
  147. );
  148. });
  149. };
  150. cleanUrl = () => {
  151. const { location, router } = this.props;
  152. delete location.query.resetPat;
  153. router.replace(location);
  154. };
  155. handlePersonalAccessTokenCreated = () => {
  156. this.cleanUrl();
  157. this.setState({ showPersonalAccessTokenForm: false }, () => {
  158. this.fetchInitialData();
  159. });
  160. };
  161. handleImportRepository = (selectedRepository: BitbucketRepository) => {
  162. const { selectedAlmInstance } = this.state;
  163. if (selectedAlmInstance) {
  164. this.props.onProjectSetupDone({
  165. creationMode: CreateProjectModes.BitbucketServer,
  166. almSetting: selectedAlmInstance.key,
  167. monorepo: false,
  168. projects: [
  169. {
  170. projectKey: selectedRepository.projectKey,
  171. repositorySlug: selectedRepository.slug,
  172. },
  173. ],
  174. });
  175. }
  176. };
  177. handleSearch = (query: string) => {
  178. const { selectedAlmInstance } = this.state;
  179. if (!selectedAlmInstance) {
  180. return;
  181. }
  182. if (!query) {
  183. this.setState({ searching: false, searchResults: undefined });
  184. return;
  185. }
  186. this.setState({ searching: true });
  187. searchForBitbucketServerRepositories(selectedAlmInstance.key, query)
  188. .then(({ repositories }) => {
  189. if (this.mounted) {
  190. this.setState({ searching: false, searchResults: repositories });
  191. }
  192. })
  193. .catch(() => {
  194. if (this.mounted) {
  195. this.setState({ searching: false });
  196. }
  197. });
  198. };
  199. onSelectedAlmInstanceChange = (instance: AlmSettingsInstance) => {
  200. this.setState({
  201. selectedAlmInstance: instance,
  202. showPersonalAccessTokenForm: true,
  203. searching: false,
  204. searchResults: undefined,
  205. });
  206. };
  207. render() {
  208. const { loadingBindings, location, almInstances } = this.props;
  209. const {
  210. selectedAlmInstance,
  211. loading,
  212. projectRepositories,
  213. projects,
  214. searching,
  215. searchResults,
  216. showPersonalAccessTokenForm,
  217. } = this.state;
  218. return (
  219. <BitbucketCreateProjectRenderer
  220. selectedAlmInstance={selectedAlmInstance}
  221. almInstances={almInstances}
  222. loading={loading || loadingBindings}
  223. onImportRepository={this.handleImportRepository}
  224. onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated}
  225. onSearch={this.handleSearch}
  226. onSelectedAlmInstanceChange={this.onSelectedAlmInstanceChange}
  227. projectRepositories={projectRepositories}
  228. projects={projects}
  229. resetPat={Boolean(location.query.resetPat)}
  230. searchResults={searchResults}
  231. searching={searching}
  232. showPersonalAccessTokenForm={
  233. showPersonalAccessTokenForm || Boolean(location.query.resetPat)
  234. }
  235. />
  236. );
  237. }
  238. }