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.

RemoteRepositories.tsx 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 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 * as classNames from 'classnames';
  22. import { keyBy } from 'lodash';
  23. import AlmRepositoryItem from './AlmRepositoryItem';
  24. import SetupProjectBox from './SetupProjectBox';
  25. import DeferredSpinner from '../../../components/common/DeferredSpinner';
  26. import Checkbox from '../../../components/controls/Checkbox';
  27. import SearchBox from '../../../components/controls/SearchBox';
  28. import UpgradeOrganizationBox from '../components/UpgradeOrganizationBox';
  29. import { Alert } from '../../../components/ui/Alert';
  30. import { getRepositories } from '../../../api/alm-integration';
  31. import { translateWithParameters, translate } from '../../../helpers/l10n';
  32. import { isPaidOrganization } from '../../../helpers/organizations';
  33. import { isDefined } from '../../../helpers/types';
  34. interface Props {
  35. almApplication: T.AlmApplication;
  36. onOrganizationUpgrade: () => void;
  37. onProjectCreate: (projectKeys: string[], organization: string) => void;
  38. organization: T.Organization;
  39. }
  40. type SelectedRepositories = T.Dict<T.AlmRepository | undefined>;
  41. interface State {
  42. checkAllRepositories: boolean;
  43. highlight: boolean;
  44. loading: boolean;
  45. repositories: T.AlmRepository[];
  46. search: string;
  47. selectedRepositories: SelectedRepositories;
  48. successfullyUpgraded: boolean;
  49. }
  50. export default class RemoteRepositories extends React.PureComponent<Props, State> {
  51. mounted = false;
  52. state: State = {
  53. checkAllRepositories: false,
  54. highlight: false,
  55. loading: true,
  56. repositories: [],
  57. search: '',
  58. selectedRepositories: {},
  59. successfullyUpgraded: false
  60. };
  61. componentDidMount() {
  62. this.mounted = true;
  63. this.fetchRepositories();
  64. }
  65. componentDidUpdate(prevProps: Props) {
  66. if (prevProps.organization.key !== this.props.organization.key) {
  67. this.setState({ loading: true, selectedRepositories: {} });
  68. this.fetchRepositories();
  69. }
  70. }
  71. componentWillUnmount() {
  72. this.mounted = false;
  73. }
  74. fetchRepositories = () => {
  75. const { organization } = this.props;
  76. return getRepositories({ organization: organization.key }).then(
  77. ({ repositories }) => {
  78. if (this.mounted) {
  79. this.setState({ loading: false, repositories });
  80. }
  81. },
  82. () => {
  83. if (this.mounted) {
  84. this.setState({ loading: false });
  85. }
  86. }
  87. );
  88. };
  89. filterBySearch = (search: String) => (repo: T.AlmRepository) => {
  90. return repo.label.toLowerCase().includes(search.toLowerCase());
  91. };
  92. handleHighlightUpgradeBox = (highlight: boolean) => {
  93. this.setState({ highlight });
  94. };
  95. handleOrganizationUpgrade = () => {
  96. this.props.onOrganizationUpgrade();
  97. if (this.mounted) {
  98. this.setState({ successfullyUpgraded: true });
  99. }
  100. };
  101. handleProvisionFail = () => {
  102. return this.fetchRepositories().then(() => {
  103. if (this.mounted) {
  104. this.setState(({ repositories, selectedRepositories }) => {
  105. const updateSelectedRepositories: SelectedRepositories = {};
  106. Object.keys(selectedRepositories).forEach(installationKey => {
  107. const newRepository = repositories.find(r => r.installationKey === installationKey);
  108. if (newRepository && !newRepository.linkedProjectKey) {
  109. updateSelectedRepositories[newRepository.installationKey] = newRepository;
  110. }
  111. });
  112. return { selectedRepositories: updateSelectedRepositories };
  113. });
  114. }
  115. });
  116. };
  117. handleSearch = (search: string) => {
  118. this.setState({ search, checkAllRepositories: false, selectedRepositories: {} });
  119. };
  120. onCheckAllRepositories = () => {
  121. this.setState(({ checkAllRepositories, repositories, search }) => {
  122. const { organization } = this.props;
  123. const isPaidOrg = isPaidOrganization(organization);
  124. const filterByPlan = (repo: T.AlmRepository) => (isPaidOrg ? true : !repo.private);
  125. const filterByImportable = (repo: T.AlmRepository) => !repo.linkedProjectKey;
  126. const nextState = {
  127. selectedRepositories: {},
  128. checkAllRepositories: !checkAllRepositories
  129. };
  130. if (nextState.checkAllRepositories) {
  131. const validRepositories = repositories.filter(
  132. repo =>
  133. this.filterBySearch(search)(repo) && filterByPlan(repo) && filterByImportable(repo)
  134. );
  135. nextState.selectedRepositories = keyBy(validRepositories, 'installationKey');
  136. }
  137. return nextState;
  138. });
  139. };
  140. toggleRepository = (repository: T.AlmRepository) => {
  141. this.setState(({ selectedRepositories }) => ({
  142. selectedRepositories: {
  143. ...selectedRepositories,
  144. [repository.installationKey]: selectedRepositories[repository.installationKey]
  145. ? undefined
  146. : repository
  147. }
  148. }));
  149. };
  150. render() {
  151. const { highlight, loading, repositories, search, selectedRepositories } = this.state;
  152. const { almApplication, organization } = this.props;
  153. const isPaidOrg = isPaidOrganization(organization);
  154. const hasPrivateRepositories = repositories.some(repository => Boolean(repository.private));
  155. const showSearchBox = repositories.length > 5;
  156. const showCheckAll = repositories.length > 1;
  157. const showUpgradebox =
  158. !isPaidOrg && hasPrivateRepositories && organization.actions && organization.actions.admin;
  159. const filteredRepositories = repositories.filter(this.filterBySearch(search));
  160. return (
  161. <div className="create-project">
  162. <div className="flex-1 huge-spacer-right">
  163. <div className="spacer-bottom create-project-actions">
  164. <div>
  165. {showCheckAll && (
  166. <Checkbox
  167. checked={this.state.checkAllRepositories}
  168. disabled={filteredRepositories.length === 0}
  169. onCheck={this.onCheckAllRepositories}>
  170. {translate('onboarding.create_project.select_all_repositories')}
  171. </Checkbox>
  172. )}
  173. </div>
  174. {showSearchBox && (
  175. <SearchBox
  176. minLength={1}
  177. onChange={this.handleSearch}
  178. placeholder={translate('search.search_for_repositories')}
  179. value={this.state.search}
  180. />
  181. )}
  182. </div>
  183. {this.state.successfullyUpgraded && (
  184. <Alert variant="success">
  185. {translateWithParameters(
  186. 'onboarding.create_project.subscribtion_success_x',
  187. organization.name
  188. )}
  189. </Alert>
  190. )}
  191. <DeferredSpinner loading={loading}>
  192. <ul>
  193. {filteredRepositories.length === 0 && (
  194. <li className="big-spacer-top note">
  195. {showUpgradebox
  196. ? translateWithParameters('no_results_for_x', search)
  197. : translate('onboarding.create_project.no_repositories')}
  198. </li>
  199. )}
  200. {filteredRepositories.map(repo => (
  201. <AlmRepositoryItem
  202. disabled={Boolean(repo.private && !isPaidOrg)}
  203. highlightUpgradeBox={this.handleHighlightUpgradeBox}
  204. identityProvider={almApplication}
  205. key={repo.installationKey}
  206. repository={repo}
  207. selected={Boolean(selectedRepositories[repo.installationKey])}
  208. toggleRepository={this.toggleRepository}
  209. />
  210. ))}
  211. </ul>
  212. </DeferredSpinner>
  213. </div>
  214. {organization && (
  215. <div className={classNames({ 'create-project-side-with-search': showSearchBox })}>
  216. <div className="create-project-side-sticky">
  217. <SetupProjectBox
  218. onProjectCreate={this.props.onProjectCreate}
  219. onProvisionFail={this.handleProvisionFail}
  220. organization={organization}
  221. selectedRepositories={Object.keys(selectedRepositories)
  222. .map(r => selectedRepositories[r])
  223. .filter(isDefined)}
  224. />
  225. {showUpgradebox && (
  226. <UpgradeOrganizationBox
  227. className={classNames({ highlight })}
  228. onOrganizationUpgrade={this.handleOrganizationUpgrade}
  229. organization={organization}
  230. />
  231. )}
  232. </div>
  233. </div>
  234. )}
  235. </div>
  236. );
  237. }
  238. }