選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

BitbucketProjectCreate.tsx 8.4KB

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