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.

RemoteOrganizationChoose.tsx 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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 { WithRouterProps, withRouter } from 'react-router';
  23. import { FormattedMessage } from 'react-intl';
  24. import { sortBy } from 'lodash';
  25. import IdentityProviderLink from 'sonar-ui-common/components/controls/IdentityProviderLink';
  26. import { save } from 'sonar-ui-common/helpers/storage';
  27. import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
  28. import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
  29. import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
  30. import { Alert } from 'sonar-ui-common/components/ui/Alert';
  31. import Select from 'sonar-ui-common/components/controls/Select';
  32. import { serializeQuery, ORGANIZATION_IMPORT_BINDING_IN_PROGRESS_TIMESTAMP } from './utils';
  33. import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
  34. import { sanitizeAlmId } from '../../../helpers/almIntegrations';
  35. interface Props {
  36. almApplication: T.AlmApplication;
  37. almInstallId?: string;
  38. almOrganization?: T.AlmOrganization;
  39. almUnboundApplications: T.AlmUnboundApplication[];
  40. boundOrganization?: T.OrganizationBase;
  41. className?: string;
  42. }
  43. interface State {
  44. unboundInstallationId: string;
  45. }
  46. export class RemoteOrganizationChoose extends React.PureComponent<Props & WithRouterProps, State> {
  47. state: State = { unboundInstallationId: '' };
  48. handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  49. event.preventDefault();
  50. const { unboundInstallationId } = this.state;
  51. if (unboundInstallationId) {
  52. this.props.router.push({
  53. pathname: '/create-organization',
  54. query: serializeQuery({
  55. almInstallId: unboundInstallationId,
  56. almKey: this.props.almApplication.key
  57. })
  58. });
  59. }
  60. };
  61. handleInstallAppClick = () => {
  62. save(ORGANIZATION_IMPORT_BINDING_IN_PROGRESS_TIMESTAMP, Date.now().toString(10));
  63. };
  64. handleInstallationChange = ({ installationId }: T.AlmUnboundApplication) => {
  65. this.setState({ unboundInstallationId: installationId });
  66. };
  67. renderOption = (organization: T.AlmUnboundApplication) => {
  68. const { almApplication } = this.props;
  69. return (
  70. <span>
  71. <img
  72. alt={almApplication.name}
  73. className="spacer-right"
  74. height={14}
  75. src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(almApplication.key)}.svg`}
  76. />
  77. {organization.name}
  78. </span>
  79. );
  80. };
  81. render() {
  82. const {
  83. almApplication,
  84. almInstallId,
  85. almOrganization,
  86. almUnboundApplications,
  87. boundOrganization,
  88. className
  89. } = this.props;
  90. const { unboundInstallationId } = this.state;
  91. return (
  92. <div className={classNames('boxed-group', className)}>
  93. <div className="boxed-group-header">
  94. <h2>{translate('onboarding.import_organization.import_org_details')}</h2>
  95. </div>
  96. <div className="boxed-group-inner">
  97. {almInstallId && !almOrganization && (
  98. <Alert className="big-spacer-bottom width-60" variant="error">
  99. <div className="markdown">
  100. {translate('onboarding.import_organization.org_not_found')}
  101. <ul>
  102. <li>{translate('onboarding.import_organization.org_not_found.tips_1')}</li>
  103. <li>{translate('onboarding.import_organization.org_not_found.tips_2')}</li>
  104. </ul>
  105. </div>
  106. </Alert>
  107. )}
  108. {almOrganization && boundOrganization && (
  109. <Alert className="big-spacer-bottom width-60" variant="error">
  110. <FormattedMessage
  111. defaultMessage={translate('onboarding.import_organization.already_bound_x')}
  112. id="onboarding.import_organization.already_bound_x"
  113. values={{
  114. avatar: (
  115. <img
  116. alt={almApplication.name}
  117. className="little-spacer-left"
  118. src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(
  119. almApplication.key
  120. )}.svg`}
  121. width={16}
  122. />
  123. ),
  124. name: <strong>{almOrganization.name}</strong>,
  125. boundAvatar: (
  126. <OrganizationAvatar
  127. className="little-spacer-left"
  128. organization={boundOrganization}
  129. small={true}
  130. />
  131. ),
  132. boundName: <strong>{boundOrganization.name}</strong>
  133. }}
  134. />
  135. </Alert>
  136. )}
  137. <div className="display-flex-center">
  138. <div className="display-inline-block">
  139. <IdentityProviderLink
  140. backgroundColor={almApplication.backgroundColor}
  141. className="display-inline-block"
  142. iconPath={almApplication.iconPath}
  143. name={almApplication.name}
  144. onClick={this.handleInstallAppClick}
  145. small={true}
  146. url={almApplication.installationUrl}>
  147. {translate(
  148. 'onboarding.import_organization.choose_organization_button',
  149. almApplication.key
  150. )}
  151. </IdentityProviderLink>
  152. </div>
  153. {almUnboundApplications.length > 0 && (
  154. <div className="display-flex-stretch">
  155. <div className="vertical-pipe-separator">
  156. <div className="vertical-separator " />
  157. <span className="note">{translate('or')}</span>
  158. <div className="vertical-separator" />
  159. </div>
  160. <form className="big-spacer-top big-spacer-bottom" onSubmit={this.handleSubmit}>
  161. <div className="form-field abs-width-400">
  162. <label className="text-normal" htmlFor="select-unbound-installation">
  163. {translateWithParameters(
  164. 'onboarding.import_organization.choose_unbound_installation_x',
  165. translate(sanitizeAlmId(almApplication.key))
  166. )}
  167. </label>
  168. <Select
  169. className="input-super-large"
  170. clearable={false}
  171. id="select-unbound-installation"
  172. labelKey="name"
  173. onChange={this.handleInstallationChange}
  174. optionRenderer={this.renderOption}
  175. options={sortBy(almUnboundApplications, o => o.name.toLowerCase())}
  176. placeholder={translate('onboarding.import_organization.choose_organization')}
  177. value={unboundInstallationId}
  178. valueKey="installationId"
  179. valueRenderer={this.renderOption}
  180. />
  181. </div>
  182. <SubmitButton disabled={!unboundInstallationId}>
  183. {translate('continue')}
  184. </SubmitButton>
  185. </form>
  186. </div>
  187. )}
  188. </div>
  189. </div>
  190. </div>
  191. );
  192. }
  193. }
  194. export default withRouter(RemoteOrganizationChoose);