]> source.dussan.org Git - sonarqube.git/commitdiff
SONARCLOUD-120 update landing page of just created organization
authorStas Vilchik <stas.vilchik@sonarsource.com>
Fri, 21 Sep 2018 12:04:25 +0000 (14:04 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 25 Sep 2018 18:21:00 +0000 (20:21 +0200)
15 files changed:
server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationJustCreated-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationJustCreated-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/routes.ts
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/styles.css
server/sonar-web/src/main/js/components/icons-components/Icon.tsx
server/sonar-web/src/main/js/components/icons-components/OnboardingAddMembersIcon.tsx [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 78201f442d9e3b498fc1e41ae231a2761885219b..8e25b0e249b2c50b5e2627b5fa74b83c8336e5cd 100644 (file)
@@ -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);
     });
   };
 
index e466c12bb5cb5d1c48152bfa7828211ea54a032b..6adb8f9d5206acff21b96131a877b6838da1e132 100644 (file)
@@ -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 (file)
index 0000000..2405d97
--- /dev/null
@@ -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 (file)
index 0000000..c77fabe
--- /dev/null
@@ -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);
index a4581693d1b0668a725c0a532f055ed1f7483718..5ae1a3503da126176626f45c1541be1740cf1619 100644 (file)
@@ -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 (file)
index 0000000..cc6298c
--- /dev/null
@@ -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');
+});
index 5bf896954b58d3381cbbc85c456abbbec5952670..0f97587fa9d785048c236ca07a1332054b1ff3e8 100644 (file)
@@ -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 (file)
index 0000000..4a96855
--- /dev/null
@@ -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>
+`;
index d78634091ec50bdf242e99a0f3f3c7093ac5337b..e3279292d9c6bb17f1a6d16c83a366c53630f851 100644 (file)
@@ -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`);
+            }
           }
         }
       },
index 1ed6b84045e3d3511bec70025530fc7545fd9212..b41edb8e99e22ae5b1b260f54a7d0c81821605be 100644 (file)
@@ -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>
index 18b2f16f846eb7979a8dbe96aef96deb60f1f773..1b46fdaca6671ba4e1ae6f9085af7970e548e344 100644 (file)
@@ -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"
       >
index da2571a369d4a4c9e0db5df69352f3b568ce880f..e7cc5a727f6ac902de83f712f67741a99d9de583 100644 (file)
@@ -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;
 
 .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 {
index 25560e4eac8ca268b2581b2ff523801b472d9367..5e2baa27f62ef0b5ab20ba04c2eb8f44f236fd94 100644 (file)
@@ -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 (file)
index 0000000..5dcdff5
--- /dev/null
@@ -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>
+  );
+}
index c63dff4130a96d45a195365e92af40103c4600b6..76f5cb99b84da8b6ea37aaf6d0a8c2e562f2742d 100644 (file)
@@ -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!