From f61d654f7d74036f50f2a1ca6b380a437ec911a2 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Mon, 15 Oct 2018 11:55:35 +0200 Subject: SONAR-11321 Create organization from GitHub organization or BitBucket team * Create api/alm_integration/show_organization and handle only GitHub installation * Add import from ALM tab in Create Org page * Do not show error while validating detail input * Add step to create organization from ALM * Display a warning if the installation id was not found * Add Alm link to remote organization in org context * Create GET api/alm_integration/show_app_info --- .../create/organization/CreateOrganization.tsx | 222 ++++++++++++--------- 1 file changed, 130 insertions(+), 92 deletions(-) (limited to 'server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx') diff --git a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx index b6c0fa25b32..2767cdfa399 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx @@ -18,17 +18,29 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; import { Helmet } from 'react-helmet'; import { FormattedMessage } from 'react-intl'; import { Link, withRouter, WithRouterProps } from 'react-router'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import OrganizationDetailsStep from './OrganizationDetailsStep'; -import PlanStep from './PlanStep'; -import { formatPrice } from './utils'; +import { formatPrice, parseQuery } from './utils'; import { whenLoggedIn } from './whenLoggedIn'; +import AutoOrganizationCreate from './AutoOrganizationCreate'; +import ManualOrganizationCreate from './ManualOrganizationCreate'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import Tabs from '../../../components/controls/Tabs'; +import { getAlmAppInfo, getAlmOrganization } from '../../../api/alm-integration'; import { getSubscriptionPlans } from '../../../api/billing'; -import { OrganizationBase, Organization, SubscriptionPlan } from '../../../app/types'; +import { + LoggedInUser, + Organization, + SubscriptionPlan, + AlmApplication, + AlmOrganization, + OrganizationBase +} from '../../../app/types'; +import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations'; import { translate } from '../../../helpers/l10n'; import { getOrganizationUrl } from '../../../helpers/urls'; import * as api from '../../../api/organizations'; @@ -38,27 +50,26 @@ import '../../tutorials/styles.css'; // TODO remove me interface Props { createOrganization: (organization: OrganizationBase) => Promise; + currentUser: LoggedInUser; deleteOrganization: (key: string) => Promise; } -enum Step { - OrganizationDetails, - Plan -} - interface State { + almApplication?: AlmApplication; + almOrganization?: AlmOrganization; loading: boolean; organization?: Organization; - step: Step; subscriptionPlans?: SubscriptionPlan[]; } +interface LocationState { + paid?: boolean; + tab?: 'auto' | 'manual'; +} + export class CreateOrganization extends React.PureComponent { mounted = false; - state: State = { - loading: true, - step: Step.OrganizationDetails - }; + state: State = { loading: true }; componentDidMount() { this.mounted = true; @@ -66,7 +77,16 @@ export class CreateOrganization extends React.PureComponent { - getSubscriptionPlans().then( - subscriptionPlans => { - if (this.mounted) { - this.setState({ loading: false, subscriptionPlans }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } + fetchAlmApplication = () => { + return getAlmAppInfo().then(({ application }) => { + if (this.mounted) { + this.setState({ almApplication: application }); } - ); - }; - - finishCreation = (key: string) => { - this.props.router.push({ - pathname: getOrganizationUrl(key), - state: { justCreated: true } }); }; - handleOrganizationDetailsStepOpen = () => { - this.setState({ step: Step.OrganizationDetails }); + fetchAlmOrganization = (installationId: string) => { + return getAlmOrganization({ installationId }).then(almOrganization => { + if (this.mounted) { + this.setState({ almOrganization }); + } + }); }; - handleOrganizationDetailsFinish = (organization: Required) => { - this.setState({ organization, step: Step.Plan }); - return Promise.resolve(); + fetchSubscriptionPlans = () => { + return getSubscriptionPlans().then(subscriptionPlans => { + if (this.mounted) { + this.setState({ subscriptionPlans }); + } + }); }; - handlePaidPlanChoose = () => { - if (this.state.organization) { - this.finishCreation(this.state.organization.key); - } + handleOrgCreated = (organization: string) => { + this.props.router.push({ + pathname: getOrganizationUrl(organization), + state: { justCreated: true } + }); }; - handleFreePlanChoose = () => { - return this.createOrganization().then(key => { - this.finishCreation(key); - }); + onTabChange = (tab: 'auto' | 'manual') => { + this.updateUrl({ tab }); }; - createOrganization = () => { - const { organization } = this.state; - if (organization) { - return this.props - .createOrganization({ - avatar: organization.avatar, - description: organization.description, - key: organization.key, - name: organization.name || organization.key, - url: organization.url - }) - .then(({ key }) => key); - } else { - return Promise.reject(); + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); } }; - deleteOrganization = () => { - const { organization } = this.state; - if (organization) { - this.props.deleteOrganization(organization.key).catch(() => {}); - } + updateUrl = (state: Partial = {}) => { + this.props.router.replace({ + pathname: this.props.location.pathname, + state: { ...(this.props.location.state || {}), ...state } + }); }; render() { const { location } = this.props; - const { loading, subscriptionPlans } = this.state; + const { almApplication, loading, subscriptionPlans } = this.state; + const state = (location.state || {}) as LocationState; + const query = parseQuery(location.query); const header = translate('onboarding.create_organization.page.header'); const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price; const formattedPrice = formatPrice(startedPrice); + const showManualTab = state.tab === 'manual' && !query.almInstallId; return ( <> @@ -174,27 +178,59 @@ export class CreateOrganization extends React.PureComponent {loading ? ( - + ) : ( <> - - - {subscriptionPlans !== undefined && ( - + {translate( + 'onboarding.create_organization.import_organization', + almApplication.key + )} + + {translate('beta')} + + + ) + }, + { + disabled: Boolean(query.almInstallId), + key: 'manual', + node: translate('onboarding.create_organization.create_manually') + } + ]} + /> + )} + + {showManualTab || !almApplication ? ( + + ) : ( + )} @@ -205,7 +241,7 @@ export class CreateOrganization extends React.PureComponent { return api.createOrganization(organization).then((organization: Organization) => { dispatch(actions.createOrganization(organization)); @@ -228,8 +264,10 @@ const mapDispatchToProps = { }; export default whenLoggedIn( - connect( - null, - mapDispatchToProps - )(withRouter(CreateOrganization)) + withRouter( + connect( + null, + mapDispatchToProps + )(CreateOrganization) + ) ); -- cgit v1.2.3