diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-09-21 14:04:25 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-09-25 20:21:00 +0200 |
commit | cd467657fc139c01074956aeca33bb57ac0b87bf (patch) | |
tree | ec9e14eba5f5831e038206126845dc5452404776 | |
parent | 748c41c723cef0f11a0b90378773dafb3b51b4a0 (diff) | |
download | sonarqube-cd467657fc139c01074956aeca33bb57ac0b87bf.tar.gz sonarqube-cd467657fc139c01074956aeca33bb57ac0b87bf.zip |
SONARCLOUD-120 update landing page of just created organization
15 files changed, 303 insertions, 28 deletions
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 78201f442d9..8e25b0e249b 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 @@ -88,6 +88,13 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr ); }; + finishCreation = (key: string) => { + this.props.router.push({ + pathname: getOrganizationUrl(key), + state: { justCreated: true } + }); + }; + handleOrganizationDetailsStepOpen = () => { this.setState({ step: Step.OrganizationDetails }); }; @@ -99,13 +106,13 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr handlePaidPlanChoose = () => { if (this.state.organization) { - this.props.router.push(getOrganizationUrl(this.state.organization.key)); + this.finishCreation(this.state.organization.key); } }; handleFreePlanChoose = () => { return this.createOrganization().then(key => { - this.props.router.push(getOrganizationUrl(key)); + this.finishCreation(key); }); }; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx index e466c12bb5c..6adb8f9d520 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx @@ -53,7 +53,10 @@ it('should render and create organization', async () => { wrapper.find('PlanStep').prop<Function>('onFreePlanChoose')(); await waitAndUpdate(wrapper); expect(createOrganization).toBeCalledWith(organization); - expect(router.push).toBeCalledWith('/organizations/foo'); + expect(router.push).toBeCalledWith({ + pathname: '/organizations/foo', + state: { justCreated: true } + }); }); it('should preselect paid plan', async () => { diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css new file mode 100644 index 00000000000..2405d972144 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +.organization-just-created { + margin: 120px auto 0; + width: 480px; +} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx new file mode 100644 index 00000000000..c77fabe8c26 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { withRouter, WithRouterProps } from 'react-router'; +import { Organization } from '../../../app/types'; +import { Button } from '../../../components/ui/buttons'; +import OnboardingProjectIcon from '../../../components/icons-components/OnboardingProjectIcon'; +import OnboardingAddMembersIcon from '../../../components/icons-components/OnboardingAddMembersIcon'; +import { translate } from '../../../helpers/l10n'; +import '../../tutorials/styles.css'; +import './OrganizationJustCreated.css'; + +interface Props { + organization: Organization; +} + +export class OrganizationJustCreated extends React.PureComponent<Props & WithRouterProps> { + static contextTypes = { + openProjectOnboarding: () => null + }; + + handleNewProjectClick = () => { + this.context.openProjectOnboarding(this.props.organization.key); + }; + + handleAddMembersClick = () => { + const { organization } = this.props; + this.props.router.push(`/organizations/${organization.key}/members`); + }; + + render() { + return ( + <div className="organization-just-created"> + <h3 className="text-center">{translate('onboarding.create_organization.ready')}</h3> + <div className="onboarding-choices"> + <Button className="onboarding-choice" onClick={this.handleNewProjectClick}> + <OnboardingProjectIcon className="big-spacer-bottom" /> + <h6 className="onboarding-choice-name"> + {translate('provisioning.create_new_project')} + </h6> + </Button> + <Button className="onboarding-choice" onClick={this.handleAddMembersClick}> + <OnboardingAddMembersIcon /> + <h6 className="onboarding-choice-name"> + {translate('organization.members.add.multiple')} + </h6> + </Button> + </div> + </div> + ); + } +} + +export default withRouter(OrganizationJustCreated); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx index a4581693d1b..5ae1a3503da 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx @@ -20,6 +20,8 @@ import * as React from 'react'; import Helmet from 'react-helmet'; import { connect } from 'react-redux'; +import { Location } from 'history'; +import OrganizationJustCreated from './OrganizationJustCreated'; import OrganizationNavigation from '../navigation/OrganizationNavigation'; import { fetchOrganization } from '../actions'; import NotFound from '../../../app/components/NotFound'; @@ -34,7 +36,7 @@ import { interface OwnProps { children?: React.ReactNode; - location: { pathname: string }; + location: Location; params: { organizationKey: string }; } @@ -84,6 +86,16 @@ export class OrganizationPage extends React.PureComponent<Props, State> { this.props.fetchOrganization(organizationKey).then(this.stopLoading, this.stopLoading); }; + renderChildren(organization: Organization) { + const { location } = this.props; + const justCreated = Boolean(location.state && location.state.justCreated); + return justCreated ? ( + <OrganizationJustCreated organization={organization} /> + ) : ( + this.props.children + ); + } + render() { const { organization } = this.props; @@ -105,7 +117,7 @@ export class OrganizationPage extends React.PureComponent<Props, State> { organization={organization} userOrganizations={this.props.userOrganizations} /> - {this.props.children} + {this.renderChildren(organization)} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationJustCreated-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationJustCreated-test.tsx new file mode 100644 index 00000000000..cc6298cd304 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationJustCreated-test.tsx @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { OrganizationJustCreated } from '../OrganizationJustCreated'; +import { Organization } from '../../../../app/types'; +import { click } from '../../../../helpers/testUtils'; + +const organization: Organization = { key: 'foo', name: 'Foo' }; + +it('should render', () => { + // @ts-ignore + expect(shallow(<OrganizationJustCreated organization={organization} />)).toMatchSnapshot(); +}); + +it('should create new project', () => { + const openProjectOnboarding = jest.fn(); + // @ts-ignore + const wrapper = shallow(<OrganizationJustCreated organization={organization} />, { + context: { openProjectOnboarding } + }); + click(wrapper.find('Button').first()); + expect(openProjectOnboarding).toBeCalledWith('foo'); +}); + +it('should add members', () => { + const router = { push: jest.fn() }; + // @ts-ignore + const wrapper = shallow(<OrganizationJustCreated organization={organization} router={router} />); + click(wrapper.find('Button').last()); + expect(router.push).toBeCalledWith('/organizations/foo/members'); +}); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx index 5bf896954b5..0f97587fa9d 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; +import { Location } from 'history'; import { OrganizationPage } from '../OrganizationPage'; const fetchOrganization = jest.fn().mockResolvedValue(undefined); @@ -54,7 +55,7 @@ function getWrapper(props = {}) { <OrganizationPage currentUser={{ isLoggedIn: false }} fetchOrganization={fetchOrganization} - location={{ pathname: 'foo' }} + location={{ pathname: 'foo' } as Location} params={{ organizationKey: 'foo' }} userOrganizations={[]} {...props}> diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationJustCreated-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationJustCreated-test.tsx.snap new file mode 100644 index 00000000000..4a96855b821 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationJustCreated-test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +<div + className="organization-just-created" +> + <h3 + className="text-center" + > + onboarding.create_organization.ready + </h3> + <div + className="onboarding-choices" + > + <Button + className="onboarding-choice" + onClick={[Function]} + > + <OnboardingProjectIcon + className="big-spacer-bottom" + /> + <h6 + className="onboarding-choice-name" + > + provisioning.create_new_project + </h6> + </Button> + <Button + className="onboarding-choice" + onClick={[Function]} + > + <OnboardingAddMembersIcon /> + <h6 + className="onboarding-choice-name" + > + organization.members.add.multiple + </h6> + </Button> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/organizations/routes.ts b/server/sonar-web/src/main/js/apps/organizations/routes.ts index d78634091ec..e3279292d9c 100644 --- a/server/sonar-web/src/main/js/apps/organizations/routes.ts +++ b/server/sonar-web/src/main/js/apps/organizations/routes.ts @@ -34,8 +34,11 @@ const routes = [ { indexRoute: { onEnter(nextState: RouterState, replace: RedirectFunction) { - const { params } = nextState; - replace(`/organizations/${params.organizationKey}/projects`); + const { location, params } = nextState; + const justCreated = Boolean(location.state && location.state.justCreated); + if (!justCreated) { + replace(`/organizations/${params.organizationKey}/projects`); + } } } }, diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx index 1ed6b84045e..b41edb8e99e 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx @@ -68,18 +68,24 @@ export class OnboardingModal extends React.PureComponent<Props> { </div> <div className="modal-simple-body text-center onboarding-choices"> <Button className="onboarding-choice" onClick={this.props.onOpenProjectOnboarding}> - <OnboardingProjectIcon /> - <span>{translate('onboarding.analyze_public_code')}</span> + <OnboardingProjectIcon className="big-spacer-bottom" /> + <h6 className="onboarding-choice-name"> + {translate('onboarding.analyze_public_code')} + </h6> <p className="note">{translate('onboarding.analyze_public_code.note')}</p> </Button> <Button className="onboarding-choice" onClick={this.props.onOpenOrganizationOnboarding}> - <OnboardingPrivateIcon /> - <span>{translate('onboarding.analyze_private_code')}</span> + <OnboardingPrivateIcon className="big-spacer-bottom" /> + <h6 className="onboarding-choice-name"> + {translate('onboarding.analyze_private_code')} + </h6> <p className="note">{translate('onboarding.analyze_private_code.note')}</p> </Button> <Button className="onboarding-choice" onClick={this.props.onOpenTeamOnboarding}> - <OnboardingTeamIcon /> - <span>{translate('onboarding.contribute_existing_project')}</span> + <OnboardingTeamIcon className="big-spacer-bottom" /> + <h6 className="onboarding-choice-name"> + {translate('onboarding.contribute_existing_project')} + </h6> <p className="note">{translate('onboarding.contribute_existing_project.note')}</p> </Button> </div> diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap index 18b2f16f846..1b46fdaca66 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap @@ -26,10 +26,14 @@ exports[`renders correctly 1`] = ` className="onboarding-choice" onClick={[MockFunction]} > - <OnboardingProjectIcon /> - <span> + <OnboardingProjectIcon + className="big-spacer-bottom" + /> + <h6 + className="onboarding-choice-name" + > onboarding.analyze_public_code - </span> + </h6> <p className="note" > @@ -40,10 +44,14 @@ exports[`renders correctly 1`] = ` className="onboarding-choice" onClick={[MockFunction]} > - <OnboardingPrivateIcon /> - <span> + <OnboardingPrivateIcon + className="big-spacer-bottom" + /> + <h6 + className="onboarding-choice-name" + > onboarding.analyze_private_code - </span> + </h6> <p className="note" > @@ -54,10 +62,14 @@ exports[`renders correctly 1`] = ` className="onboarding-choice" onClick={[MockFunction]} > - <OnboardingTeamIcon /> - <span> + <OnboardingTeamIcon + className="big-spacer-bottom" + /> + <h6 + className="onboarding-choice-name" + > onboarding.contribute_existing_project - </span> + </h6> <p className="note" > diff --git a/server/sonar-web/src/main/js/apps/tutorials/styles.css b/server/sonar-web/src/main/js/apps/tutorials/styles.css index da2571a369d..e7cc5a727f6 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/styles.css +++ b/server/sonar-web/src/main/js/apps/tutorials/styles.css @@ -65,7 +65,7 @@ .onboarding-choice { display: flex; flex-direction: column; - justify-content: flex-end; + justify-content: center; padding: calc(2 * var(--gridSize)); width: 190px; height: 190px; @@ -78,12 +78,13 @@ .onboarding-choice svg { color: var(--gray40); - margin-bottom: calc(3 * var(--gridSize)); } -.onboarding-choice span { +.onboarding-choice-name { + padding-top: var(--gridSize); + padding-bottom: calc(0.5 * var(--gridSize)); + color: inherit; font-size: var(--mediumFontSize); - margin-bottom: calc(var(--gridSize) / 2); } .onboarding-choice .note { diff --git a/server/sonar-web/src/main/js/components/icons-components/Icon.tsx b/server/sonar-web/src/main/js/components/icons-components/Icon.tsx index 25560e4eac8..5e2baa27f62 100644 --- a/server/sonar-web/src/main/js/components/icons-components/Icon.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/Icon.tsx @@ -26,7 +26,7 @@ export interface IconProps { } interface Props { - children: React.ReactElement<any>; + children: React.ReactNode; className?: string; size?: number; style?: React.CSSProperties; diff --git a/server/sonar-web/src/main/js/components/icons-components/OnboardingAddMembersIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OnboardingAddMembersIcon.tsx new file mode 100644 index 00000000000..5dcdff50549 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/OnboardingAddMembersIcon.tsx @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import Icon, { IconProps } from './Icon'; + +export default function OnboardingAddMembersIcon({ + className, + fill = 'currentColor', + size = 64 +}: IconProps) { + return ( + <Icon className={className} height={(size / 64) * 80} viewBox="0 0 64 80" width={size}> + <path + d="M49 34c0 9.389-7.611 17-17 17s-17-7.611-17-17 7.611-17 17-17 17 7.611 17 17z" + style={{ fill: 'none', stroke: fill, strokeWidth: 2 }} + /> + <path + d="M36 32c0 2.2-1.8 4-4 4s-4-1.8-4-4v-1c0-2.2 1.8-4 4-4s4 1.8 4 4v1zm4 39a8 8 0 1 1-16 0 8 8 0 0 1 16 0z" + style={{ fill: 'none', stroke: fill, strokeWidth: 2 }} + /> + <path + d="M33 70h2v2h-2v2h-2v-2h-2v-2h2v-2h2v2zm-5-14l-.072-.001c-1.521-.054-2.834-1.337-2.925-2.855L25 50h2c0 1.745-.532 3.91.952 3.999L28 54h8v.002l.072-.005c.506-.042.922-.489.928-1.003V50h2c0 1.024.011 2.048-.001 3.072-.054 1.518-1.337 2.834-2.855 2.925l-.072.002L36 56v8h-2v-7.982c-1.333.007-2.667.007-4 0V64h-2v-8zm-7 0H1V10 0h62v56H43v-2h18V10H3v44h18v2zm38-4H43v-2h14V14H7v36h14v2H5V12h54v40zm-19-9l1 .017c-.03 1.79-2.454 2.506-3.918 2.717-4.074.584-8.503.911-12.176-.477-.949-.358-1.887-1.119-1.906-2.24l.191-.017H23v-3.566l5.38-3.228.913-.913 1.414 1.414-1.087 1.087L25 40.566v2.438c.067 1.304 10.98 2.117 13.844.157.076-.052.152-.172.156-.178v-2.417l-4.62-2.772-1.087-1.087 1.414-1.414.913.913L41 39.434V43h-1zm14-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm42-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm42-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm20.198-10.999c3.529.062 6.837 1.669 9.386 4.169l-1.289 1.539c-4.178-4.152-11.167-5.254-16.359-.228l-.231.228-1.41-1.418c2.633-2.617 6.031-4.313 9.903-4.29zM3 2v6h58V2H3zm56 4H17V4h42v2zM11 6H9V4h2v2zM7 6H5V4h2v2zm8 0h-2V4h2v2z" + style={{ fill, fillRule: 'nonzero' }} + /> + </Icon> + ); +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index c63dff4130a..76f5cb99b84 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2613,6 +2613,7 @@ organization.url.description=Url of the homepage of the organization. organization.members.page=Members organization.members.page.description=Add users to the organization and grant them permissions to work on the projects. See {link} documentation. organization.members.add=Add a member +organization.members.add.multiple=Add members organization.members.x_groups={0} group(s) organization.members.members=member(s) organization.members.remove=Remove from organization's members @@ -2722,6 +2723,7 @@ onboarding.create_organization.choose_plan=Choose a plan onboarding.create_organization.choose_payment_method=Choose payment solution onboarding.create_organization.enter_your_coupon=Enter your coupon onboarding.create_organization.create_and_upgrade=Create Organization and Upgrade +onboarding.create_organization.ready=All set! Your organization is now ready to go onboarding.team.header=Join a team onboarding.team.first_step=Well congrats, the first step is done! |