]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9424 Show onboarding tutorial on first login
authorStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 12 Jun 2017 12:31:31 +0000 (14:31 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 20 Jun 2017 11:10:53 +0000 (04:10 -0700)
16 files changed:
server/sonar-web/src/main/js/api/users.js
server/sonar-web/src/main/js/app/components/help/GlobalHelp.js
server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js
server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js
server/sonar-web/src/main/js/app/components/help/__tests__/__snapshots__/GlobalHelp-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Onboarding-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/routes.js [deleted file]
server/sonar-web/src/main/js/components/icons-components/HelpIcon.js [new file with mode: 0644]
server/sonar-web/src/main/less/components/modals.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index c8a5e993449025931ae29a760700049d1f4a2af4..e603c77ef7bce28e8121a7f6a759b6c0516d7248 100644 (file)
@@ -56,3 +56,7 @@ export function searchUsers(query: string, pageSize?: number) {
   }
   return getJSON(url, data);
 }
+
+export function skipOnboarding(): Promise<void> {
+  return post('/api/users/skip_onboarding_tutorial');
+}
index 553f5e3cc71d9030d00434be8cc8bcb557673f34..6f941b93f4b8e34b7d4662747e58d80672a13b06 100644 (file)
@@ -28,7 +28,9 @@ import TutorialsHelp from './TutorialsHelp';
 import { translate } from '../../../helpers/l10n';
 
 type Props = {
+  currentUser: { isLoggedIn: boolean },
   onClose: () => void,
+  onTutorialSelect: () => void,
   sonarCloud?: boolean
 };
 
@@ -60,7 +62,7 @@ export default class GlobalHelp extends React.PureComponent {
           ? <LinksHelpSonarCloud onClose={this.props.onClose} />
           : <LinksHelp onClose={this.props.onClose} />;
       case 'tutorials':
-        return <TutorialsHelp onClose={this.props.onClose} />;
+        return <TutorialsHelp onTutorialSelect={this.props.onTutorialSelect} />;
       default:
         return null;
     }
@@ -80,7 +82,9 @@ export default class GlobalHelp extends React.PureComponent {
 
   renderMenu = () => (
     <ul className="side-tabs-menu">
-      {['shortcuts', 'tutorials', 'links'].map(this.renderMenuItem)}
+      {(this.props.currentUser.isLoggedIn
+        ? ['shortcuts', 'tutorials', 'links']
+        : ['shortcuts', 'links']).map(this.renderMenuItem)}
     </ul>
   );
 
index 112b63933618fecece73cc7ccb403fb8ef6e822b..5202060d98404647c3dcd2ffeaa5b8a45a59f435 100644 (file)
  */
 // @flow
 import React from 'react';
-import { Link } from 'react-router';
 import { translate } from '../../../helpers/l10n';
 
-type Props = { onClose: () => void };
+type Props = { onTutorialSelect: () => void };
+
+export default function TutorialsHelp({ onTutorialSelect }: Props) {
+  const handleClick = (event: Event) => {
+    event.preventDefault();
+    onTutorialSelect();
+  };
 
-export default function TutorialsHelp({ onClose }: Props) {
   return (
     <div>
       <h2 className="spacer-top spacer-bottom">{translate('help.section.tutorials')}</h2>
-      <Link to="/tutorials/onboarding" onClick={onClose}>Onboarding Tutorial</Link>
+      <a href="#" onClick={handleClick}>{translate('tutorials.onboarding')}</a>
     </div>
   );
 }
index ff0a3dd6befac023b8788abeb367315cbf71ed4d..96bbd9a27fee17407805f77f03d68a49ade17775 100644 (file)
@@ -24,7 +24,13 @@ import GlobalHelp from '../GlobalHelp';
 import { click } from '../../../../helpers/testUtils';
 
 it('switches between tabs', () => {
-  const wrapper = shallow(<GlobalHelp onClose={jest.fn()} />);
+  const wrapper = shallow(
+    <GlobalHelp
+      currentUser={{ isLoggedIn: true }}
+      onClose={jest.fn()}
+      onTutorialSelect={jest.fn()}
+    />
+  );
   expect(wrapper.find('ShortcutsHelp')).toHaveLength(1);
   clickOnSection(wrapper, 'links');
   expect(wrapper.find('LinksHelp')).toHaveLength(1);
@@ -34,6 +40,18 @@ it('switches between tabs', () => {
   expect(wrapper.find('ShortcutsHelp')).toHaveLength(1);
 });
 
+it('does not show tutorials for anonymous', () => {
+  expect(
+    shallow(
+      <GlobalHelp
+        currentUser={{ isLoggedIn: false }}
+        onClose={jest.fn()}
+        onTutorialSelect={jest.fn()}
+      />
+    )
+  ).toMatchSnapshot();
+});
+
 function clickOnSection(wrapper: Object, section: string) {
   click(wrapper.find(`[data-section="${section}"]`), { currentTarget: { dataset: { section } } });
 }
diff --git a/server/sonar-web/src/main/js/app/components/help/__tests__/__snapshots__/GlobalHelp-test.js.snap b/server/sonar-web/src/main/js/app/components/help/__tests__/__snapshots__/GlobalHelp-test.js.snap
new file mode 100644 (file)
index 0000000..3758713
--- /dev/null
@@ -0,0 +1,72 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`does not show tutorials for anonymous 1`] = `
+<Modal
+  ariaHideApp={true}
+  className="modal modal-medium"
+  closeTimeoutMS={0}
+  contentLabel="help"
+  isOpen={true}
+  onRequestClose={[Function]}
+  overlayClassName="modal-overlay"
+  parentSelector={[Function]}
+  portalClassName="ReactModalPortal"
+  shouldCloseOnOverlayClick={true}
+>
+  <div
+    className="modal-head"
+  >
+    <h2>
+      help
+    </h2>
+  </div>
+  <div
+    className="side-tabs-layout"
+  >
+    <div
+      className="side-tabs-side"
+    >
+      <ul
+        className="side-tabs-menu"
+      >
+        <li>
+          <a
+            className="active"
+            data-section="shortcuts"
+            href="#"
+            onClick={[Function]}
+          >
+            help.section.shortcuts
+          </a>
+        </li>
+        <li>
+          <a
+            className=""
+            data-section="links"
+            href="#"
+            onClick={[Function]}
+          >
+            help.section.links
+          </a>
+        </li>
+      </ul>
+    </div>
+    <div
+      className="side-tabs-main"
+    >
+      <ShortcutsHelp />
+    </div>
+  </div>
+  <div
+    className="modal-foot"
+  >
+    <a
+      className="js-modal-close"
+      href="#"
+      onClick={[Function]}
+    >
+      close
+    </a>
+  </div>
+</Modal>
+`;
index 8d432d498a04fdf823ba0c7ac723d4d72db8ad40..dc2829912945845c95e8f7597c9ae9a43456479f 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+// @flow
 import React from 'react';
 import { connect } from 'react-redux';
 import GlobalNavBranding from './GlobalNavBranding';
@@ -24,13 +25,30 @@ import GlobalNavMenu from './GlobalNavMenu';
 import GlobalNavUserContainer from './GlobalNavUserContainer';
 import Search from '../../search/Search';
 import GlobalHelp from '../../help/GlobalHelp';
+import HelpIcon from '../../../../components/icons-components/HelpIcon';
+import OnboardingModal from '../../../../apps/tutorials/onboarding/OnboardingModal';
 import { getCurrentUser, getAppState, getSettingValue } from '../../../../store/rootReducer';
 
+type Props = {
+  appState: { organizationsEnabled: boolean },
+  currentUser: { isLoggedIn: boolean, showOnboardingTutorial: true },
+  sonarCloud: boolean
+};
+
+type State = {
+  helpOpen: boolean,
+  onboardingTutorialOpen: boolean
+};
+
 class GlobalNav extends React.PureComponent {
-  state = { helpOpen: false };
+  props: Props;
+  state: State = { helpOpen: false, onboardingTutorialOpen: false };
 
   componentDidMount() {
     window.addEventListener('keypress', this.onKeyPress);
+    if (this.props.currentUser.showOnboardingTutorial) {
+      this.openOnboardingTutorial();
+    }
   }
 
   componentWillUnmount() {
@@ -42,8 +60,7 @@ class GlobalNav extends React.PureComponent {
     const code = e.keyCode || e.which;
     const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
     const isTriggerKey = code === 63;
-    const isModalOpen = document.querySelector('html').classList.contains('modal-open');
-    if (!isInput && !isModalOpen && isTriggerKey) {
+    if (!isInput && isTriggerKey) {
       this.openHelp();
     }
   };
@@ -57,8 +74,11 @@ class GlobalNav extends React.PureComponent {
 
   closeHelp = () => this.setState({ helpOpen: false });
 
+  openOnboardingTutorial = () => this.setState({ helpOpen: false, onboardingTutorialOpen: true });
+
+  closeOnboardingTutorial = () => this.setState({ onboardingTutorialOpen: false });
+
   render() {
-    /* eslint-disable max-len */
     return (
       <nav className="navbar navbar-global page-container" id="global-navigation">
         <div className="container">
@@ -67,17 +87,10 @@ class GlobalNav extends React.PureComponent {
           <GlobalNavMenu {...this.props} />
 
           <ul className="nav navbar-nav navbar-right">
-            <Search {...this.props} />
+            <Search appState={this.props.appState} currentUser={this.props.currentUser} />
             <li>
               <a className="navbar-help" onClick={this.handleHelpClick} href="#">
-                <svg width="16" height="16">
-                  <g transform="matrix(0.0364583,0,0,0.0364583,1,-0.166667)">
-                    <path
-                      fill="#fff"
-                      d="M224,344L224,296C224,293.667 223.25,291.75 221.75,290.25C220.25,288.75 218.333,288 216,288L168,288C165.667,288 163.75,288.75 162.25,290.25C160.75,291.75 160,293.667 160,296L160,344C160,346.333 160.75,348.25 162.25,349.75C163.75,351.25 165.667,352 168,352L216,352C218.333,352 220.25,351.25 221.75,349.75C223.25,348.25 224,346.333 224,344ZM288,176C288,161.333 283.375,147.75 274.125,135.25C264.875,122.75 253.333,113.083 239.5,106.25C225.667,99.417 211.5,96 197,96C156.5,96 125.583,113.75 104.25,149.25C101.75,153.25 102.417,156.75 106.25,159.75L139.25,184.75C140.417,185.75 142,186.25 144,186.25C146.667,186.25 148.75,185.25 150.25,183.25C159.083,171.917 166.25,164.25 171.75,160.25C177.417,156.25 184.583,154.25 193.25,154.25C201.25,154.25 208.375,156.417 214.625,160.75C220.875,165.083 224,170 224,175.5C224,181.833 222.333,186.917 219,190.75C215.667,194.583 210,198.333 202,202C191.5,206.667 181.875,213.875 173.125,223.625C164.375,233.375 160,243.833 160,255L160,264C160,266.333 160.75,268.25 162.25,269.75C163.75,271.25 165.667,272 168,272L216,272C218.333,272 220.25,271.25 221.75,269.75C223.25,268.25 224,266.333 224,264C224,260.833 225.792,256.708 229.375,251.625C232.958,246.542 237.5,242.417 243,239.25C248.333,236.25 252.417,233.875 255.25,232.125C258.083,230.375 261.917,227.458 266.75,223.375C271.583,219.292 275.292,215.292 277.875,211.375C280.458,207.458 282.792,202.417 284.875,196.25C286.958,190.083 288,183.333 288,176ZM384,224C384,258.833 375.417,290.958 358.25,320.375C341.083,349.792 317.792,373.083 288.375,390.25C258.958,407.417 226.833,416 192,416C157.167,416 125.042,407.417 95.625,390.25C66.208,373.083 42.917,349.792 25.75,320.375C8.583,290.958 0,258.833 0,224C0,189.167 8.583,157.042 25.75,127.625C42.917,98.208 66.208,74.917 95.625,57.75C125.042,40.583 157.167,32 192,32C226.833,32 258.958,40.583 288.375,57.75C317.792,74.917 341.083,98.208 358.25,127.625C375.417,157.042 384,189.167 384,224Z"
-                    />
-                  </g>
-                </svg>
+                <HelpIcon />
               </a>
             </li>
             <GlobalNavUserContainer {...this.props} />
@@ -85,7 +98,15 @@ class GlobalNav extends React.PureComponent {
         </div>
 
         {this.state.helpOpen &&
-          <GlobalHelp onClose={this.closeHelp} sonarCloud={this.props.sonarCloud} />}
+          <GlobalHelp
+            currentUser={this.props.currentUser}
+            onClose={this.closeHelp}
+            onTutorialSelect={this.openOnboardingTutorial}
+            sonarCloud={this.props.sonarCloud}
+          />}
+
+        {this.state.onboardingTutorialOpen &&
+          <OnboardingModal onClose={this.closeOnboardingTutorial} />}
       </nav>
     );
   }
index 1ffac0eb63666b0a2bd0ebeb3f525aefd137f087..861f20199a9bfe5020a31da49253cb629fb15ccc 100644 (file)
@@ -63,7 +63,6 @@ import qualityProfilesRoutes from '../../apps/quality-profiles/routes';
 import sessionsRoutes from '../../apps/sessions/routes';
 import settingsRoutes from '../../apps/settings/routes';
 import systemRoutes from '../../apps/system/routes';
-import tutorialRoutes from '../../apps/tutorials/routes';
 import updateCenterRoutes from '../../apps/update-center/routes';
 import usersRoutes from '../../apps/users/routes';
 import webAPIRoutes from '../../apps/web-api/routes';
@@ -161,7 +160,6 @@ const startReactApp = () => {
                   <Route path="quality_gates" childRoutes={qualityGatesRoutes} />
                   <Route path="portfolios" component={PortfoliosPage} />
                   <Route path="profiles" childRoutes={qualityProfilesRoutes} />
-                  <Route path="tutorials" childRoutes={tutorialRoutes} />
                   <Route path="web_api" childRoutes={webAPIRoutes} />
 
                   <Route component={ProjectContainer}>
index 78db84e6ef97273700d301b816a4d8c441fc1d0e..573551790a41b7be50b93a4cf832a3ae1a830a2c 100644 (file)
@@ -31,6 +31,8 @@ import Other from './commands/Other';
 import { translate } from '../../../helpers/l10n';
 
 type Props = {|
+  onFinish: () => void,
+  onReset: () => void,
   open: boolean,
   organization?: string,
   sonarCloud: boolean,
@@ -48,10 +50,12 @@ export default class AnalysisStep extends React.PureComponent {
 
   handleLanguageSelect = (result?: Result) => {
     this.setState({ result });
+    this.props.onFinish();
   };
 
   handleLanguageReset = () => {
     this.setState({ result: undefined });
+    this.props.onReset();
   };
 
   getHost = () => window.location.origin + window.baseUrl;
index 0382a33e0d6cf5b50fd852316271d3023030121a..e2617224cc09dd1d6de6bf62ff7c82a3af9fca3b 100644 (file)
@@ -22,37 +22,51 @@ import React from 'react';
 import TokenStep from './TokenStep';
 import OrganizationStep from './OrganizationStep';
 import AnalysisStep from './AnalysisStep';
+import { skipOnboarding } from '../../../api/users';
 import { translate } from '../../../helpers/l10n';
 import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
 import './styles.css';
 
 type Props = {
   currentUser: { login: string, isLoggedIn: boolean },
+  onSkip: () => void,
   organizationsEnabled: boolean,
   sonarCloud: boolean
 };
 
 type State = {
+  finished: boolean,
   organization?: string,
+  skipping: boolean,
   step: string,
   token?: string
 };
 
 export default class Onboarding extends React.PureComponent {
+  mounted: boolean;
   props: Props;
   state: State;
 
   constructor(props: Props) {
     super(props);
-    this.state = { step: props.organizationsEnabled ? 'organization' : 'token' };
+    this.state = {
+      finished: false,
+      skipping: false,
+      step: props.organizationsEnabled ? 'organization' : 'token'
+    };
   }
 
   componentDidMount() {
+    this.mounted = true;
     if (!this.props.currentUser.isLoggedIn) {
       handleRequiredAuthentication();
     }
   }
 
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
   handleTokenDone = (token: string) => {
     this.setState({ step: 'analysis', token });
   };
@@ -61,6 +75,27 @@ export default class Onboarding extends React.PureComponent {
     this.setState({ organization, step: 'token' });
   };
 
+  handleSkipClick = (event: Event) => {
+    event.preventDefault();
+    this.setState({ skipping: true });
+    skipOnboarding().then(
+      () => {
+        if (this.mounted) {
+          this.props.onSkip();
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ skipping: false });
+        }
+      }
+    );
+  };
+
+  handleFinish = () => this.setState({ finished: true });
+
+  handleReset = () => this.setState({ finished: false });
+
   render() {
     if (!this.props.currentUser.isLoggedIn) {
       return null;
@@ -77,6 +112,13 @@ export default class Onboarding extends React.PureComponent {
           <h1 className="page-title">
             {translate(sonarCloud ? 'onboarding.header.sonarcloud' : 'onboarding.header')}
           </h1>
+          <div className="page-actions">
+            {this.state.skipping
+              ? <i className="spinner" />
+              : <a className="js-skip text-muted" href="#" onClick={this.handleSkipClick}>
+                  {translate('tutorials.skip')}
+                </a>}
+          </div>
           <div className="page-description">
             {translate('onboarding.header.description')}
           </div>
@@ -97,12 +139,22 @@ export default class Onboarding extends React.PureComponent {
         />
 
         <AnalysisStep
+          onFinish={this.handleFinish}
+          onReset={this.handleReset}
           organization={this.state.organization}
           open={step === 'analysis'}
           sonarCloud={sonarCloud}
           stepNumber={stepNumber}
           token={token}
         />
+
+        {this.state.finished &&
+          !this.state.skipping &&
+          <footer className="text-right">
+            <a className="button" href="#" onClick={this.handleSkipClick}>
+              {translate('tutorials.finish')}
+            </a>
+          </footer>}
       </div>
     );
   }
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.js
new file mode 100644 (file)
index 0000000..13e1af8
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+  onClose: () => void
+};
+
+type State = {
+  OnboardingContainer?: Object
+};
+
+export default class OnboardingModal extends React.PureComponent {
+  mounted: boolean;
+  props: Props;
+  state: State = {};
+
+  componentDidMount() {
+    this.mounted = true;
+    // $FlowFixMe
+    require.ensure([], require => {
+      this.receiveComponent(require('./OnboardingContainer').default);
+    });
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  receiveComponent = (OnboardingContainer: Object) => {
+    if (this.mounted) {
+      this.setState({ OnboardingContainer });
+    }
+  };
+
+  render() {
+    const { OnboardingContainer } = this.state;
+
+    return (
+      <Modal
+        isOpen={true}
+        contentLabel={translate('tutorials.onboarding')}
+        className="modal modal-full-screen"
+        overlayClassName="modal-overlay">
+        {OnboardingContainer != null && <OnboardingContainer onSkip={this.props.onClose} />}
+      </Modal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Onboarding-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Onboarding-test.js
new file mode 100644 (file)
index 0000000..e489889
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+// @flow
+import React from 'react';
+import { shallow, mount } from 'enzyme';
+import Onboarding from '../Onboarding';
+import { click, doAsync } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/users', () => ({
+  skipOnboarding: () => Promise.resolve()
+}));
+
+const currentUser = { login: 'admin', isLoggedIn: true };
+
+it('guides for on-premise', () => {
+  const wrapper = shallow(
+    <Onboarding
+      currentUser={currentUser}
+      onSkip={jest.fn()}
+      organizationsEnabled={false}
+      sonarCloud={false}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+
+  // $FlowFixMe
+  wrapper.instance().handleTokenDone('abcd1234');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('guides for sonarcloud', () => {
+  const wrapper = shallow(
+    <Onboarding
+      currentUser={currentUser}
+      onSkip={jest.fn()}
+      organizationsEnabled={true}
+      sonarCloud={true}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+
+  // $FlowFixMe
+  wrapper.instance().handleOrganizationDone('my-org');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  // $FlowFixMe
+  wrapper.instance().handleTokenDone('abcd1234');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('skips', () => {
+  const onSkip = jest.fn();
+  const wrapper = mount(
+    <Onboarding
+      currentUser={currentUser}
+      onSkip={onSkip}
+      organizationsEnabled={false}
+      sonarCloud={false}
+    />
+  );
+  click(wrapper.find('.js-skip'));
+  return doAsync(() => {
+    expect(onSkip).toBeCalled();
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap
new file mode 100644 (file)
index 0000000..df16111
--- /dev/null
@@ -0,0 +1,258 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`guides for on-premise 1`] = `
+<div
+  className="page page-limited"
+>
+  <header
+    className="page-header"
+  >
+    <h1
+      className="page-title"
+    >
+      onboarding.header
+    </h1>
+    <div
+      className="page-actions"
+    >
+      <a
+        className="js-skip text-muted"
+        href="#"
+        onClick={[Function]}
+      >
+        tutorials.skip
+      </a>
+    </div>
+    <div
+      className="page-description"
+    >
+      onboarding.header.description
+    </div>
+  </header>
+  <TokenStep
+    onContinue={[Function]}
+    open={true}
+    stepNumber={1}
+  />
+  <AnalysisStep
+    onFinish={[Function]}
+    onReset={[Function]}
+    open={false}
+    sonarCloud={false}
+    stepNumber={2}
+  />
+</div>
+`;
+
+exports[`guides for on-premise 2`] = `
+<div
+  className="page page-limited"
+>
+  <header
+    className="page-header"
+  >
+    <h1
+      className="page-title"
+    >
+      onboarding.header
+    </h1>
+    <div
+      className="page-actions"
+    >
+      <a
+        className="js-skip text-muted"
+        href="#"
+        onClick={[Function]}
+      >
+        tutorials.skip
+      </a>
+    </div>
+    <div
+      className="page-description"
+    >
+      onboarding.header.description
+    </div>
+  </header>
+  <TokenStep
+    onContinue={[Function]}
+    open={false}
+    stepNumber={1}
+  />
+  <AnalysisStep
+    onFinish={[Function]}
+    onReset={[Function]}
+    open={true}
+    sonarCloud={false}
+    stepNumber={2}
+    token="abcd1234"
+  />
+</div>
+`;
+
+exports[`guides for sonarcloud 1`] = `
+<div
+  className="page page-limited"
+>
+  <header
+    className="page-header"
+  >
+    <h1
+      className="page-title"
+    >
+      onboarding.header.sonarcloud
+    </h1>
+    <div
+      className="page-actions"
+    >
+      <a
+        className="js-skip text-muted"
+        href="#"
+        onClick={[Function]}
+      >
+        tutorials.skip
+      </a>
+    </div>
+    <div
+      className="page-description"
+    >
+      onboarding.header.description
+    </div>
+  </header>
+  <OrganizationStep
+    currentUser={
+      Object {
+        "isLoggedIn": true,
+        "login": "admin",
+      }
+    }
+    onContinue={[Function]}
+    open={true}
+    stepNumber={1}
+  />
+  <TokenStep
+    onContinue={[Function]}
+    open={false}
+    stepNumber={2}
+  />
+  <AnalysisStep
+    onFinish={[Function]}
+    onReset={[Function]}
+    open={false}
+    sonarCloud={true}
+    stepNumber={3}
+  />
+</div>
+`;
+
+exports[`guides for sonarcloud 2`] = `
+<div
+  className="page page-limited"
+>
+  <header
+    className="page-header"
+  >
+    <h1
+      className="page-title"
+    >
+      onboarding.header.sonarcloud
+    </h1>
+    <div
+      className="page-actions"
+    >
+      <a
+        className="js-skip text-muted"
+        href="#"
+        onClick={[Function]}
+      >
+        tutorials.skip
+      </a>
+    </div>
+    <div
+      className="page-description"
+    >
+      onboarding.header.description
+    </div>
+  </header>
+  <OrganizationStep
+    currentUser={
+      Object {
+        "isLoggedIn": true,
+        "login": "admin",
+      }
+    }
+    onContinue={[Function]}
+    open={false}
+    stepNumber={1}
+  />
+  <TokenStep
+    onContinue={[Function]}
+    open={true}
+    stepNumber={2}
+  />
+  <AnalysisStep
+    onFinish={[Function]}
+    onReset={[Function]}
+    open={false}
+    organization="my-org"
+    sonarCloud={true}
+    stepNumber={3}
+  />
+</div>
+`;
+
+exports[`guides for sonarcloud 3`] = `
+<div
+  className="page page-limited"
+>
+  <header
+    className="page-header"
+  >
+    <h1
+      className="page-title"
+    >
+      onboarding.header.sonarcloud
+    </h1>
+    <div
+      className="page-actions"
+    >
+      <a
+        className="js-skip text-muted"
+        href="#"
+        onClick={[Function]}
+      >
+        tutorials.skip
+      </a>
+    </div>
+    <div
+      className="page-description"
+    >
+      onboarding.header.description
+    </div>
+  </header>
+  <OrganizationStep
+    currentUser={
+      Object {
+        "isLoggedIn": true,
+        "login": "admin",
+      }
+    }
+    onContinue={[Function]}
+    open={false}
+    stepNumber={1}
+  />
+  <TokenStep
+    onContinue={[Function]}
+    open={false}
+    stepNumber={2}
+  />
+  <AnalysisStep
+    onFinish={[Function]}
+    onReset={[Function]}
+    open={true}
+    organization="my-org"
+    sonarCloud={true}
+    stepNumber={3}
+    token="abcd1234"
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/routes.js b/server/sonar-web/src/main/js/apps/tutorials/routes.js
deleted file mode 100644 (file)
index 3a7111d..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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.
- */
-const routes = [
-  {
-    path: 'onboarding',
-    getComponent(_, callback) {
-      require.ensure([], require => {
-        callback(null, require('./onboarding/OnboardingContainer').default);
-      });
-    }
-  }
-];
-
-export default routes;
diff --git a/server/sonar-web/src/main/js/components/icons-components/HelpIcon.js b/server/sonar-web/src/main/js/components/icons-components/HelpIcon.js
new file mode 100644 (file)
index 0000000..86e5241
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+// @flow
+import React from 'react';
+
+type Props = { className?: string, size?: number };
+
+export default function HelpIcon({ className, size = 16 }: Props) {
+  /* eslint-disable max-len */
+  return (
+    <svg className={className} viewBox="0 0 16 16" width={size} height={size}>
+      <g transform="matrix(0.0364583,0,0,0.0364583,1,-0.166667)">
+        <path
+          fill="#fff"
+          d="M224,344L224,296C224,293.667 223.25,291.75 221.75,290.25C220.25,288.75 218.333,288 216,288L168,288C165.667,288 163.75,288.75 162.25,290.25C160.75,291.75 160,293.667 160,296L160,344C160,346.333 160.75,348.25 162.25,349.75C163.75,351.25 165.667,352 168,352L216,352C218.333,352 220.25,351.25 221.75,349.75C223.25,348.25 224,346.333 224,344ZM288,176C288,161.333 283.375,147.75 274.125,135.25C264.875,122.75 253.333,113.083 239.5,106.25C225.667,99.417 211.5,96 197,96C156.5,96 125.583,113.75 104.25,149.25C101.75,153.25 102.417,156.75 106.25,159.75L139.25,184.75C140.417,185.75 142,186.25 144,186.25C146.667,186.25 148.75,185.25 150.25,183.25C159.083,171.917 166.25,164.25 171.75,160.25C177.417,156.25 184.583,154.25 193.25,154.25C201.25,154.25 208.375,156.417 214.625,160.75C220.875,165.083 224,170 224,175.5C224,181.833 222.333,186.917 219,190.75C215.667,194.583 210,198.333 202,202C191.5,206.667 181.875,213.875 173.125,223.625C164.375,233.375 160,243.833 160,255L160,264C160,266.333 160.75,268.25 162.25,269.75C163.75,271.25 165.667,272 168,272L216,272C218.333,272 220.25,271.25 221.75,269.75C223.25,268.25 224,266.333 224,264C224,260.833 225.792,256.708 229.375,251.625C232.958,246.542 237.5,242.417 243,239.25C248.333,236.25 252.417,233.875 255.25,232.125C258.083,230.375 261.917,227.458 266.75,223.375C271.583,219.292 275.292,215.292 277.875,211.375C280.458,207.458 282.792,202.417 284.875,196.25C286.958,190.083 288,183.333 288,176ZM384,224C384,258.833 375.417,290.958 358.25,320.375C341.083,349.792 317.792,373.083 288.375,390.25C258.958,407.417 226.833,416 192,416C157.167,416 125.042,407.417 95.625,390.25C66.208,373.083 42.917,349.792 25.75,320.375C8.583,290.958 0,258.833 0,224C0,189.167 8.583,157.042 25.75,127.625C42.917,98.208 66.208,74.917 95.625,57.75C125.042,40.583 157.167,32 192,32C226.833,32 258.958,40.583 288.375,57.75C317.792,74.917 341.083,98.208 358.25,127.625C375.417,157.042 384,189.167 384,224Z"
+        />
+      </g>
+    </svg>
+  );
+}
index 4853c080759804e2f55db1e437d2e2ca13c071f0..aaae44af086c16167a05dce81ce00e9c7fa9db5b 100644 (file)
   margin-left: -45vw;
 }
 
+.modal-full-screen {
+  top: 30%;
+  width: 90vw;
+  height: 90vh;
+  margin-left: -45vw;
+  margin-top: -45vh;
+  border-radius: 2px;
+
+  &.ReactModal__Content--after-open {
+    top: 50%;
+  }
+}
+
 .modal-overlay,
 .ReactModal__Overlay {
   position: fixed;
index ee0c39066aacbffa64baef329ff809d4457a0956..ef5ccb3eb8e5f335a9f82daad2a5ebcf4cdbe0b1 100644 (file)
@@ -1091,6 +1091,10 @@ shortcuts.section.rules.deactivate=deactivate selected rule
 shortcuts.section.code=Code Page
 shortcuts.section.code.search=search components in the project scope
 
+tutorials.onboarding=Onboarding Tutorial
+tutorials.skip=Skip this tutorial
+tutorials.finish=Finish this tutorial
+
 
 #------------------------------------------------------------------------------
 #