]> source.dussan.org Git - sonarqube.git/commitdiff
UI: SONAR-9355 Create onboarding tutorial (#2137)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 12 Jun 2017 10:50:27 +0000 (03:50 -0700)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 20 Jun 2017 11:10:53 +0000 (04:10 -0700)
83 files changed:
it/it-tests/src/test/java/pageobjects/settings/SettingsPage.java
server/sonar-web/src/main/js/api/components.js
server/sonar-web/src/main/js/api/organizations.js
server/sonar-web/src/main/js/api/user-tokens.js
server/sonar-web/src/main/js/app/components/GlobalFooterForSonarQubeDotCom.js
server/sonar-web/src/main/js/app/components/help/GlobalHelp.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/help/LinksHelp.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelp.js [deleted file]
server/sonar-web/src/main/js/app/styles/boxed-group.css
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/app/utils/throwGlobalError.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/tokens-view.js
server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js
server/sonar-web/src/main/js/apps/settings/components/App.js
server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js
server/sonar-web/src/main/js/apps/settings/styles.css
server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/LanguageStep.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/NewOrganizationForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/NewProjectForm.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/Step.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/LanguageStep-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewOrganizationForm-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewProjectForm-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Step-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/LanguageStep-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewOrganizationForm-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewProjectForm-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Step-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/BuildWrapper.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/ClangGCC.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Command.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/DotNet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaGradle.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaMaven.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/MSBuildScanner.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Msvc.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Other.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/SQScanner.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/BuildWrapper-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/ClangGCC-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Command-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/DotNet-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaGradle-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaMaven-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/MSBuildScanner-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Msvc-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Other-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/SQScanner-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/BuildWrapper-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/ClangGCC-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Command-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/DotNet-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaGradle-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaMaven-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Msvc-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Other-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/SQScanner-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/styles.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/routes.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/tokens-view.js
server/sonar-web/src/main/js/helpers/testUtils.js
server/sonar-web/src/main/less/components/modals.less
server/sonar-web/src/main/less/components/side-tabs.less [new file with mode: 0644]
server/sonar-web/src/main/less/init/type.less
server/sonar-web/src/main/less/sonar.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index dcb3794ebebae4a816a6213ffae6015d0aa04481..704ba3054ae70745d25f42242f11465b32a44fea 100644 (file)
@@ -35,7 +35,7 @@ public class SettingsPage {
   }
 
   public SettingsPage assertMenuContains(String categoryName) {
-    $(".settings-menu").$(By.linkText(categoryName)).shouldBe(visible);
+    $(".side-tabs-menu").$(By.linkText(categoryName)).shouldBe(visible);
     return this;
   }
 
@@ -50,7 +50,7 @@ public class SettingsPage {
   }
 
   public SettingsPage openCategory(String categoryName) {
-    $(".settings-menu").$(By.linkText(categoryName)).click();
+    $(".side-tabs-menu").$(By.linkText(categoryName)).click();
     return this;
   }
 
index 867100c4faeffcfb29c03ceca43e7739ecd608cd..e024ce947f5acdb948a254e0b3049e55821bd824 100644 (file)
@@ -19,6 +19,7 @@
  */
 // @flow
 import { getJSON, postJSON, post } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
 
 export function getComponents(data?: Object) {
   const url = '/api/projects/search';
@@ -55,7 +56,7 @@ export function createProject(
   }
 ) {
   const url = '/api/projects/create';
-  return postJSON(url, data);
+  return postJSON(url, data).catch(throwGlobalError);
 }
 
 export function searchProjectTags(data?: { ps?: number, q?: string }) {
index 097a17ce5cbfa6dba6425af5f8ac6d2facc3952a..7ca0263ba81c89c46930383ec724d46ca0089f6f 100644 (file)
@@ -20,6 +20,7 @@
 // @flow
 import { getJSON, post, postJSON } from '../helpers/request';
 import type { Organization } from '../store/organizations/duck';
+import throwGlobalError from '../app/utils/throwGlobalError';
 
 export const getOrganizations = (organizations?: Array<string>) => {
   const data = {};
@@ -44,7 +45,9 @@ type GetOrganizationNavigation = {
 };
 
 export const getOrganization = (key: string): Promise<GetOrganizationType> => {
-  return getOrganizations([key]).then(r => r.organizations.find(o => o.key === key));
+  return getOrganizations([key])
+    .then(r => r.organizations.find(o => o.key === key))
+    .catch(throwGlobalError);
 };
 
 export const getOrganizationNavigation = (key: string): Promise<GetOrganizationNavigation> => {
@@ -52,12 +55,13 @@ export const getOrganizationNavigation = (key: string): Promise<GetOrganizationN
 };
 
 export const createOrganization = (fields: {}): Promise<Organization> =>
-  postJSON('/api/organizations/create', fields).then(r => r.organization);
+  postJSON('/api/organizations/create', fields).then(r => r.organization, throwGlobalError);
 
 export const updateOrganization = (key: string, changes: {}) =>
   post('/api/organizations/update', { key, ...changes });
 
-export const deleteOrganization = (key: string) => post('/api/organizations/delete', { key });
+export const deleteOrganization = (key: string) =>
+  post('/api/organizations/delete', { key }).catch(throwGlobalError);
 
 export const searchMembers = (
   data: { organization?: string, p?: number, ps?: number, q?: string, selected?: string }
index 4b3b97e2bba4d2d9a70b7c4be6d8f3f1e1bf25b9..3947b07f1bee920e280b08e90de0a41de3ef1837 100644 (file)
  * 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 { getJSON, postJSON, post } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
 
 /**
  * List tokens for given user login
  * @param {string} login
  * @returns {Promise}
  */
-export function getTokens(login) {
+export function getTokens(login: string) {
   const url = '/api/user_tokens/search';
   const data = { login };
   return getJSON(url, data).then(r => r.userTokens);
@@ -36,10 +38,16 @@ export function getTokens(login) {
  * @param {string} tokenName
  * @returns {Promise}
  */
-export function generateToken(userLogin, tokenName) {
+export function generateToken(
+  tokenName: string,
+  userLogin?: string
+): Promise<{ name: string, token: string }> {
   const url = '/api/user_tokens/generate';
-  const data = { login: userLogin, name: tokenName };
-  return postJSON(url, data);
+  const data: { [string]: string } = { name: tokenName };
+  if (userLogin) {
+    data.login = userLogin;
+  }
+  return postJSON(url, data).catch(throwGlobalError);
 }
 
 /**
@@ -48,8 +56,11 @@ export function generateToken(userLogin, tokenName) {
  * @param {string} tokenName
  * @returns {Promise}
  */
-export function revokeToken(userLogin, tokenName) {
+export function revokeToken(tokenName: string, userLogin?: string) {
   const url = '/api/user_tokens/revoke';
-  const data = { login: userLogin, name: tokenName };
-  return post(url, data);
+  const data: { [string]: string } = { name: tokenName };
+  if (userLogin) {
+    data.login = userLogin;
+  }
+  return post(url, data).catch(throwGlobalError);
 }
index 755a34e933d7e1c0f55b8f8445474715cfa08b36..ab8ec94f97ab0a089281007b7f2d6a7ea8d54e25 100644 (file)
@@ -43,7 +43,7 @@ export default function GlobalFooterForSonarQubeDotCom() {
         {' - '}
         <a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a>
         {' - '}
-        {<Link to="/about">{translate('footer.about')}</Link>}
+        <Link to="/about">{translate('footer.about')}</Link>
       </div>
     </div>
   );
diff --git a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js
new file mode 100644 (file)
index 0000000..553f5e3
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * 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 classNames from 'classnames';
+import LinksHelp from './LinksHelp';
+import LinksHelpSonarCloud from './LinksHelpSonarCloud';
+import ShortcutsHelp from './ShortcutsHelp';
+import TutorialsHelp from './TutorialsHelp';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+  onClose: () => void,
+  sonarCloud?: boolean
+};
+
+type State = {
+  section: string
+};
+
+export default class GlobalHelp extends React.PureComponent {
+  props: Props;
+  state: State = { section: 'shortcuts' };
+
+  handleCloseClick = (event: Event) => {
+    event.preventDefault();
+    this.props.onClose();
+  };
+
+  handleSectionClick = (event: Event & { currentTarget: HTMLElement }) => {
+    event.preventDefault();
+    const { section } = event.currentTarget.dataset;
+    this.setState({ section });
+  };
+
+  renderSection = () => {
+    switch (this.state.section) {
+      case 'shortcuts':
+        return <ShortcutsHelp />;
+      case 'links':
+        return this.props.sonarCloud
+          ? <LinksHelpSonarCloud onClose={this.props.onClose} />
+          : <LinksHelp onClose={this.props.onClose} />;
+      case 'tutorials':
+        return <TutorialsHelp onClose={this.props.onClose} />;
+      default:
+        return null;
+    }
+  };
+
+  renderMenuItem = (section: string) => (
+    <li key={section}>
+      <a
+        className={classNames({ active: section === this.state.section })}
+        data-section={section}
+        href="#"
+        onClick={this.handleSectionClick}>
+        {translate('help.section', section)}
+      </a>
+    </li>
+  );
+
+  renderMenu = () => (
+    <ul className="side-tabs-menu">
+      {['shortcuts', 'tutorials', 'links'].map(this.renderMenuItem)}
+    </ul>
+  );
+
+  render() {
+    return (
+      <Modal
+        isOpen={true}
+        contentLabel={translate('help')}
+        className="modal modal-medium"
+        overlayClassName="modal-overlay"
+        onRequestClose={this.props.onClose}>
+
+        <div className="modal-head">
+          <h2>{translate('help')}</h2>
+        </div>
+
+        <div className="side-tabs-layout">
+          <div className="side-tabs-side">
+            {this.renderMenu()}
+          </div>
+          <div className="side-tabs-main">
+            {this.renderSection()}
+          </div>
+        </div>
+
+        <div className="modal-foot">
+          <a className="js-modal-close" href="#" onClick={this.handleCloseClick}>
+            {translate('close')}
+          </a>
+        </div>
+
+      </Modal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/app/components/help/LinksHelp.js b/server/sonar-web/src/main/js/app/components/help/LinksHelp.js
new file mode 100644 (file)
index 0000000..e76ef2c
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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 { Link } from 'react-router';
+import { translate } from '../../../helpers/l10n';
+
+type Props = { onClose: () => void };
+
+export default function LinksHelp({ onClose }: Props) {
+  return (
+    <div>
+      <h2 className="spacer-top spacer-bottom">{translate('help.section.links')}</h2>
+
+      <a href="http://www.sonarqube.org">{translate('footer.community')}</a>{' - '}
+      <a href="https://redirect.sonarsource.com/doc/home.html">
+        {translate('footer.documentation')}
+      </a>
+      {' - '}
+      <a href="https://redirect.sonarsource.com/doc/community.html">
+        {translate('footer.support')}
+      </a>
+      {' - '}
+      <a href="https://redirect.sonarsource.com/doc/plugin-library.html">
+        {translate('footer.plugins')}
+      </a>
+      {' - '}
+      <Link to="/web_api" onClick={onClose}>{translate('footer.web_api')}</Link>
+      {' - '}
+      <Link to="/about" onClick={onClose}>{translate('footer.about')}</Link>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js b/server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js
new file mode 100644 (file)
index 0000000..6efdac9
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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 { Link } from 'react-router';
+import { translate } from '../../../helpers/l10n';
+
+type Props = { onClose: () => void };
+
+export default function LinksHelpSonarCloud({ onClose }: Props) {
+  return (
+    <div>
+      <h2 className="spacer-top spacer-bottom">{translate('help.section.links')}</h2>
+
+      <a href="https://about.sonarcloud.io/news/">{translate('footer.news')}</a>
+      {' - '}
+      <a href="https://about.sonarcloud.io/terms.pdf">{translate('footer.terms')}</a>
+      {' - '}
+      <a href="https://twitter.com/sonarqube">{translate('footer.twitter')}</a>
+      {' - '}
+      <a href="https://about.sonarcloud.io/get-started/">{translate('footer.get_started')}</a>
+      {' - '}
+      <a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a>
+      {' - '}
+      <Link to="/about" onClick={onClose}>{translate('footer.about')}</Link>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js b/server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js
new file mode 100644 (file)
index 0000000..1787524
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * 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 { translate } from '../../../helpers/l10n';
+
+export default function ShortcutsHelp() {
+  return (
+    <div>
+      <h2 className="spacer-top spacer-bottom">{translate('help.section.shortcuts')}</h2>
+
+      <div className="columns">
+        <div className="column-half">
+          <div className="spacer-bottom">
+            <h3 className="shortcuts-section-title">{translate('shortcuts.section.global')}</h3>
+            <ul className="shortcuts-list">
+              <li>
+                <span className="shortcut-button spacer-right">s</span>
+                {translate('shortcuts.section.global.search')}
+              </li>
+              <li>
+                <span className="shortcut-button spacer-right">?</span>
+                {translate('shortcuts.section.global.shortcuts')}
+              </li>
+            </ul>
+          </div>
+
+          <h3 className="shortcuts-section-title">{translate('shortcuts.section.rules')}</h3>
+          <ul className="shortcuts-list">
+            <li>
+              <span className="shortcut-button little-spacer-right">↑</span>
+              <span className="shortcut-button spacer-right">↓</span>
+              {translate('shortcuts.section.rules.navigate_between_rules')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">→</span>
+              {translate('shortcuts.section.rules.open_details')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">←</span>
+              {translate('shortcuts.section.rules.return_to_list')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">a</span>
+              {translate('shortcuts.section.rules.activate')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">d</span>
+              {translate('shortcuts.section.rules.deactivate')}
+            </li>
+          </ul>
+        </div>
+
+        <div className="column-half">
+          <h3 className="shortcuts-section-title">{translate('shortcuts.section.issues')}</h3>
+          <ul className="shortcuts-list">
+            <li>
+              <span className="shortcut-button little-spacer-right">↑</span>
+              <span className="shortcut-button spacer-right">↓</span>
+              {translate('shortcuts.section.issues.navigate_between_issues')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">→</span>
+              {translate('shortcuts.section.issues.open_details')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">←</span>
+              {translate('shortcuts.section.issues.return_to_list')}
+            </li>
+            <li>
+              <span className="shortcut-button little-spacer-right">alt</span>
+              <span className="little-spacer-right">+</span>
+              <span className="shortcut-button little-spacer-right">↑</span>
+              <span className="shortcut-button spacer-right">↓</span>
+              {translate('issues.to_navigate_issue_locations')}
+            </li>
+            <li>
+              <span className="shortcut-button little-spacer-right">alt</span>
+              <span className="little-spacer-right">+</span>
+              <span className="shortcut-button little-spacer-right">←</span>
+              <span className="shortcut-button spacer-right">→</span>
+              {translate('issues.to_switch_flows')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">f</span>
+              {translate('shortcuts.section.issue.do_transition')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">a</span>
+              {translate('shortcuts.section.issue.assign')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">m</span>
+              {translate('shortcuts.section.issue.assign_to_me')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">i</span>
+              {translate('shortcuts.section.issue.change_severity')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">c</span>
+              {translate('shortcuts.section.issue.comment')}
+            </li>
+            <li>
+              <span className="shortcut-button little-spacer-right">ctrl</span>
+              <span className="shortcut-button spacer-right">enter</span>
+              {translate('shortcuts.section.issue.submit_comment')}
+            </li>
+            <li>
+              <span className="shortcut-button spacer-right">t</span>
+              {translate('shortcuts.section.issue.change_tags')}
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js b/server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js
new file mode 100644 (file)
index 0000000..112b639
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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 { Link } from 'react-router';
+import { translate } from '../../../helpers/l10n';
+
+type Props = { onClose: () => void };
+
+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>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js b/server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js
new file mode 100644 (file)
index 0000000..ff0a3dd
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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 } from 'enzyme';
+import GlobalHelp from '../GlobalHelp';
+import { click } from '../../../../helpers/testUtils';
+
+it('switches between tabs', () => {
+  const wrapper = shallow(<GlobalHelp onClose={jest.fn()} />);
+  expect(wrapper.find('ShortcutsHelp')).toHaveLength(1);
+  clickOnSection(wrapper, 'links');
+  expect(wrapper.find('LinksHelp')).toHaveLength(1);
+  clickOnSection(wrapper, 'tutorials');
+  expect(wrapper.find('TutorialsHelp')).toHaveLength(1);
+  clickOnSection(wrapper, 'shortcuts');
+  expect(wrapper.find('ShortcutsHelp')).toHaveLength(1);
+});
+
+function clickOnSection(wrapper: Object, section: string) {
+  click(wrapper.find(`[data-section="${section}"]`), { currentTarget: { dataset: { section } } });
+}
index 2e2659d7125d9a90550e029af8eccfcfdf5c7cbf..8d432d498a04fdf823ba0c7ac723d4d72db8ad40 100644 (file)
@@ -23,8 +23,8 @@ import GlobalNavBranding from './GlobalNavBranding';
 import GlobalNavMenu from './GlobalNavMenu';
 import GlobalNavUserContainer from './GlobalNavUserContainer';
 import Search from '../../search/Search';
-import ShortcutsHelp from './ShortcutsHelp';
-import { getCurrentUser, getAppState } from '../../../../store/rootReducer';
+import GlobalHelp from '../../help/GlobalHelp';
+import { getCurrentUser, getAppState, getSettingValue } from '../../../../store/rootReducer';
 
 class GlobalNav extends React.PureComponent {
   state = { helpOpen: false };
@@ -84,15 +84,21 @@ class GlobalNav extends React.PureComponent {
           </ul>
         </div>
 
-        {this.state.helpOpen && <ShortcutsHelp onClose={this.closeHelp} />}
+        {this.state.helpOpen &&
+          <GlobalHelp onClose={this.closeHelp} sonarCloud={this.props.sonarCloud} />}
       </nav>
     );
   }
 }
 
-const mapStateToProps = state => ({
-  currentUser: getCurrentUser(state),
-  appState: getAppState(state)
-});
+const mapStateToProps = state => {
+  const sonarCloudSetting = getSettingValue(state, 'sonar.lf.sonarqube.com.enabled');
+
+  return {
+    currentUser: getCurrentUser(state),
+    appState: getAppState(state),
+    sonarCloud: sonarCloudSetting != null && sonarCloudSetting.value === 'true'
+  };
+};
 
 export default connect(mapStateToProps)(GlobalNav);
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelp.js b/server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelp.js
deleted file mode 100644 (file)
index 273b370..0000000
+++ /dev/null
@@ -1,188 +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.
- */
-// @flow
-import React from 'react';
-import Modal from 'react-modal';
-import { Link } from 'react-router';
-import { translate } from '../../../../helpers/l10n';
-
-type Props = {
-  onClose: () => void
-};
-
-export default class ShortcutsHelp extends React.PureComponent {
-  props: Props;
-
-  handleCloseClick = (event: Event) => {
-    event.preventDefault();
-    this.props.onClose();
-  };
-
-  render() {
-    return (
-      <Modal
-        isOpen={true}
-        contentLabel="shortcuts help"
-        className="modal modal-large"
-        overlayClassName="modal-overlay"
-        onRequestClose={this.props.onClose}>
-
-        <div className="modal-head">
-          <h2>{translate('help')}</h2>
-        </div>
-
-        <div className="modal-body modal-container">
-          <div className="spacer-bottom">
-            <a href="http://www.sonarqube.org">{translate('footer.community')}</a>{' - '}
-            <a href="https://redirect.sonarsource.com/doc/home.html">
-              {translate('footer.documentation')}
-            </a>
-            {' - '}
-            <a href="https://redirect.sonarsource.com/doc/community.html">
-              {translate('footer.support')}
-            </a>
-            {' - '}
-            <a href="https://redirect.sonarsource.com/doc/plugin-library.html">
-              {translate('footer.plugins')}
-            </a>
-            {' - '}
-            <Link to="/web_api" onClick={this.props.onClose}>{translate('footer.web_api')}</Link>
-            {' - '}
-            <Link to="/about" onClick={this.props.onClose}>{translate('footer.about')}</Link>
-          </div>
-
-          <h2 className="spacer-top spacer-bottom">{translate('shortcuts.modal_title')}</h2>
-
-          <div className="columns">
-            <div className="column-half">
-              <div className="spacer-bottom">
-                <h3 className="shortcuts-section-title">{translate('shortcuts.section.global')}</h3>
-                <ul className="shortcuts-list">
-                  <li>
-                    <span className="shortcut-button spacer-right">s</span>
-                    {translate('shortcuts.section.global.search')}
-                  </li>
-                  <li>
-                    <span className="shortcut-button spacer-right">?</span>
-                    {translate('shortcuts.section.global.shortcuts')}
-                  </li>
-                </ul>
-              </div>
-
-              <h3 className="shortcuts-section-title">{translate('shortcuts.section.rules')}</h3>
-              <ul className="shortcuts-list">
-                <li>
-                  <span className="shortcut-button little-spacer-right">↑</span>
-                  <span className="shortcut-button spacer-right">↓</span>
-                  {translate('shortcuts.section.rules.navigate_between_rules')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">→</span>
-                  {translate('shortcuts.section.rules.open_details')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">←</span>
-                  {translate('shortcuts.section.rules.return_to_list')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">a</span>
-                  {translate('shortcuts.section.rules.activate')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">d</span>
-                  {translate('shortcuts.section.rules.deactivate')}
-                </li>
-              </ul>
-            </div>
-
-            <div className="column-half">
-              <h3 className="shortcuts-section-title">{translate('shortcuts.section.issues')}</h3>
-              <ul className="shortcuts-list">
-                <li>
-                  <span className="shortcut-button little-spacer-right">↑</span>
-                  <span className="shortcut-button spacer-right">↓</span>
-                  {translate('shortcuts.section.issues.navigate_between_issues')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">→</span>
-                  {translate('shortcuts.section.issues.open_details')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">←</span>
-                  {translate('shortcuts.section.issues.return_to_list')}
-                </li>
-                <li>
-                  <span className="shortcut-button little-spacer-right">alt</span>
-                  <span className="little-spacer-right">+</span>
-                  <span className="shortcut-button little-spacer-right">↑</span>
-                  <span className="shortcut-button spacer-right">↓</span>
-                  {translate('issues.to_navigate_issue_locations')}
-                </li>
-                <li>
-                  <span className="shortcut-button little-spacer-right">alt</span>
-                  <span className="little-spacer-right">+</span>
-                  <span className="shortcut-button little-spacer-right">←</span>
-                  <span className="shortcut-button spacer-right">→</span>
-                  {translate('issues.to_switch_flows')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">f</span>
-                  {translate('shortcuts.section.issue.do_transition')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">a</span>
-                  {translate('shortcuts.section.issue.assign')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">m</span>
-                  {translate('shortcuts.section.issue.assign_to_me')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">i</span>
-                  {translate('shortcuts.section.issue.change_severity')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">c</span>
-                  {translate('shortcuts.section.issue.comment')}
-                </li>
-                <li>
-                  <span className="shortcut-button little-spacer-right">ctrl</span>
-                  <span className="shortcut-button spacer-right">enter</span>
-                  {translate('shortcuts.section.issue.submit_comment')}
-                </li>
-                <li>
-                  <span className="shortcut-button spacer-right">t</span>
-                  {translate('shortcuts.section.issue.change_tags')}
-                </li>
-              </ul>
-            </div>
-          </div>
-        </div>
-
-        <div className="modal-foot">
-          <a className="js-modal-close" href="#" onClick={this.handleCloseClick}>
-            {translate('close')}
-          </a>
-        </div>
-
-      </Modal>
-    );
-  }
-}
index 24c568506769e64e6e8728488d2e91b22b626b0a..f41ae1927d66647addc3872cf72d347cb83fe616 100644 (file)
@@ -34,6 +34,8 @@
 }
 
 .boxed-group-actions {
+  position: relative;
+  z-index: 12;
   float: right;
   margin-top: 15px;
   margin-right: 20px;
index 5b4a0d37e1563fe8f72796020690942c35969384..f0793da6798f946c3a9dee31f51c176ac290a954 100644 (file)
@@ -63,6 +63,7 @@ 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';
@@ -159,6 +160,7 @@ 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}>
diff --git a/server/sonar-web/src/main/js/app/utils/throwGlobalError.js b/server/sonar-web/src/main/js/app/utils/throwGlobalError.js
new file mode 100644 (file)
index 0000000..75cedec
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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 getStore from './getStore';
+import { onFail } from '../../store/rootActions';
+
+export default function throwGlobalError(error: Object) {
+  const store = getStore();
+  onFail(store.dispatch)(error);
+  return Promise.reject();
+}
index 62c6f8f9143e18f3e4551229ae39a5681fccbfd9..29ef6f5f9cd1f07f1487ab13a01c01e6fba39595 100644 (file)
@@ -52,17 +52,13 @@ export default Marionette.ItemView.extend({
     this.errors = [];
     this.newToken = null;
     const tokenName = this.$('.js-generate-token-form input').val();
-    generateToken(this.model.id, tokenName)
-      .then(response => {
+    generateToken(tokenName, this.model.id).then(
+      response => {
         this.newToken = response;
         this.requestTokens();
-      })
-      .catch(error => {
-        error.response.json().then(response => {
-          this.errors = response.errors;
-          this.render();
-        });
-      });
+      },
+      () => {}
+    );
   },
 
   onRevokeTokenFormSubmit(e) {
@@ -71,7 +67,7 @@ export default Marionette.ItemView.extend({
     const token = this.tokens.find(token => token.name === `${tokenName}`);
     if (token) {
       if (token.deleting) {
-        revokeToken(this.model.id, tokenName).then(this.requestTokens.bind(this));
+        revokeToken(tokenName, this.model.id).then(this.requestTokens.bind(this), () => {});
       } else {
         token.deleting = true;
         this.render();
index acc503123ed9d00674728e8bcc7f4f24c9280003..0266656aa8d5bf5e6ba60b6bffb7796da8c08bb4 100644 (file)
@@ -103,10 +103,9 @@ export default class CreateProjectForm extends React.PureComponent {
           this.props.onProjectCreated();
         }
       },
-      error => {
+      () => {
         if (this.mounted) {
           this.setState({ loading: false });
-          this.props.onRequestFail(error);
         }
       }
     );
index d357cff1fbc8201de73b7c0a90f1cf4adb5c24ee..9d45c3749e77996dd274934c4a13be1dad7e886a 100644 (file)
@@ -83,15 +83,15 @@ class App extends React.PureComponent {
         <Helmet title={translate('settings.page')} />
 
         <PageHeader component={this.props.component} />
-        <div className="settings-layout">
-          <div className="settings-side">
+        <div className="side-tabs-layout settings-layout">
+          <div className="side-tabs-side">
             <AllCategoriesList
               component={this.props.component}
               selectedCategory={selectedCategory}
               defaultCategory={this.props.defaultCategory}
             />
           </div>
-          <div className="settings-main">
+          <div className="side-tabs-main">
             <CategoryDefinitionsList component={this.props.component} category={selectedCategory} />
 
             {selectedCategory === 'exclusions' && <WildcardsHelp />}
index 3ad92ee2b963ea35d2cf684c51e78b32b5668969..676f5511174e39884e7233d2ccd7c6489123131e 100644 (file)
@@ -70,7 +70,7 @@ export default class CategoriesList extends React.PureComponent {
     const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase());
 
     return (
-      <ul className="settings-menu">
+      <ul className="side-tabs-menu">
         {sortedCategories.map(category => (
           <li key={category.key}>
             {this.renderLink(category)}
index a529c5c5c5f173711885958db990ee634a44c3e4..4d188e811dc4ce0cbc068857d3b5771c0e8b998b 100644 (file)
@@ -1,60 +1,7 @@
 .settings-layout {
-  display: flex;
-  justify-content: space-between;
-  align-items: stretch;
   margin-bottom: 60px;
 }
 
-.settings-main {
-  position: relative;
-  z-index: 2;
-  flex-grow: 1;
-  padding: 15px 20px;
-  border: 1px solid #e6e6e6;
-  box-sizing: border-box;
-  background-color: #fff;
-}
-
-.settings-side {
-  position: relative;
-  z-index: 3;
-  width: 160px;
-  flex-shrink: 0;
-  padding: 10px 0;
-  box-sizing: border-box;
-  transform: translateX(1px);
-}
-
-.settings-menu {}
-
-.settings-menu > li {
-  margin-bottom: 4px;
-}
-
-.settings-menu > li > a {
-  display: block;
-  padding: 10px 10px;
-  line-height: 1.5;
-  border-top-left-radius: 3px;
-  border-bottom-left-radius: 3px;
-  border: 1px solid #e6e6e6;
-  border-right: none;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  transition: color 0.3s ease, background-color 0.3s ease;
-}
-
-.settings-menu > li > a:hover,
-.settings-menu > li > a:focus,
-.settings-menu > li > a.active {
-  background-color: #fff;
-}
-
-.settings-menu > li > a.active {
-  color: #444;
-  cursor: default;
-}
-
 .settings-definitions-list > li + li {
   margin-top: 30px;
 }
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js
new file mode 100644 (file)
index 0000000..78db84e
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * 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 Step from './Step';
+import LanguageStep from './LanguageStep';
+import type { Result } from './LanguageStep';
+import JavaMaven from './commands/JavaMaven';
+import JavaGradle from './commands/JavaGradle';
+import DotNet from './commands/DotNet';
+import Msvc from './commands/Msvc';
+import ClangGCC from './commands/ClangGCC';
+import Other from './commands/Other';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+  open: boolean,
+  organization?: string,
+  sonarCloud: boolean,
+  stepNumber: number,
+  token: string
+|};
+
+type State = {
+  result?: Result
+};
+
+export default class AnalysisStep extends React.PureComponent {
+  props: Props;
+  state: State = {};
+
+  handleLanguageSelect = (result?: Result) => {
+    this.setState({ result });
+  };
+
+  handleLanguageReset = () => {
+    this.setState({ result: undefined });
+  };
+
+  getHost = () => window.location.origin + window.baseUrl;
+
+  renderForm = () => {
+    return (
+      <div className="boxed-group-inner">
+        <div className="flex-columns">
+          <div className="flex-column flex-column-half bordered-right">
+            <LanguageStep
+              onDone={this.handleLanguageSelect}
+              onReset={this.handleLanguageReset}
+              sonarCloud={this.props.sonarCloud}
+            />
+          </div>
+          <div className="flex-column flex-column-half">
+            {this.renderCommand()}
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  renderFormattedCommand = (...lines: Array<string>) => (
+    <pre>{lines.join(' ' + '\\' + '\n' + '  ')}</pre>
+  );
+
+  renderCommand = () => {
+    const { result } = this.state;
+
+    if (!result) {
+      return null;
+    }
+
+    if (result.language === 'java') {
+      return result.javaBuild === 'maven'
+        ? this.renderCommandForMaven()
+        : this.renderCommandForGradle();
+    } else if (result.language === 'dotnet') {
+      return this.renderCommandForDotNet();
+    } else if (result.language === 'c-family') {
+      return result.cFamilyCompiler === 'msvc'
+        ? this.renderCommandForMSVC()
+        : this.renderCommandForClangGCC();
+    } else {
+      return this.renderCommandForOther();
+    }
+  };
+
+  renderCommandForMaven = () => (
+    <JavaMaven
+      host={this.getHost()}
+      organization={this.props.organization}
+      token={this.props.token}
+    />
+  );
+
+  renderCommandForGradle = () => (
+    <JavaGradle
+      host={this.getHost()}
+      organization={this.props.organization}
+      token={this.props.token}
+    />
+  );
+
+  renderCommandForDotNet = () => {
+    return (
+      <DotNet
+        host={this.getHost()}
+        organization={this.props.organization}
+        // $FlowFixMe
+        projectKey={this.state.result.projectKey}
+        token={this.props.token}
+      />
+    );
+  };
+
+  renderCommandForMSVC = () => {
+    return (
+      <Msvc
+        host={this.getHost()}
+        organization={this.props.organization}
+        // $FlowFixMe
+        projectKey={this.state.result.projectKey}
+        token={this.props.token}
+      />
+    );
+  };
+
+  renderCommandForClangGCC = () => (
+    <ClangGCC
+      host={this.getHost()}
+      organization={this.props.organization}
+      // $FlowFixMe
+      os={this.state.result.os}
+      // $FlowFixMe
+      projectKey={this.state.result.projectKey}
+      token={this.props.token}
+    />
+  );
+
+  renderCommandForOther = () => (
+    <Other
+      host={this.getHost()}
+      organization={this.props.organization}
+      // $FlowFixMe
+      os={this.state.result.os}
+      // $FlowFixMe
+      projectKey={this.state.result.projectKey}
+      token={this.props.token}
+    />
+  );
+
+  renderResult = () => null;
+
+  render() {
+    return (
+      <Step
+        open={this.props.open}
+        renderForm={this.renderForm}
+        renderResult={this.renderResult}
+        stepNumber={this.props.stepNumber}
+        stepTitle={translate('onboarding.analysis.header')}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/LanguageStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/LanguageStep.js
new file mode 100644 (file)
index 0000000..44bfb4e
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * 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 NewProjectForm from './NewProjectForm';
+import RadioToggle from '../../../components/controls/RadioToggle';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+  onDone: (result: Result) => void,
+  onReset: () => void,
+  organization?: string,
+  sonarCloud: boolean
+|};
+
+type State = {
+  language?: string,
+  javaBuild?: string,
+  cFamilyCompiler?: string,
+  os?: string,
+  projectKey?: string
+};
+
+export type Result = State;
+
+export default class LanguageStep extends React.PureComponent {
+  props: Props;
+
+  static defaultProps = { sonarCloud: false };
+
+  state: State = {};
+
+  isConfigured = () => {
+    const { language, javaBuild, cFamilyCompiler, os, projectKey } = this.state;
+    const isJavaConfigured = language === 'java' && javaBuild != null;
+    const isDotNetConfigured = language === 'dotnet' && projectKey != null;
+    const isCFamilyConfigured =
+      language === 'c-family' && (cFamilyCompiler === 'msvc' || os != null) && projectKey != null;
+    const isOtherConfigured = language === 'other' && projectKey != null;
+
+    return isJavaConfigured || isDotNetConfigured || isCFamilyConfigured || isOtherConfigured;
+  };
+
+  handleChange = () => {
+    if (this.isConfigured()) {
+      this.props.onDone(this.state);
+    } else {
+      this.props.onReset();
+    }
+  };
+
+  handleLanguageChange = (language: string) => {
+    this.setState({ language }, this.handleChange);
+  };
+
+  handleJavaBuildChange = (javaBuild: string) => {
+    this.setState({ javaBuild }, this.handleChange);
+  };
+
+  handleCFamilyCompilerChange = (cFamilyCompiler: string) => {
+    this.setState({ cFamilyCompiler }, this.handleChange);
+  };
+
+  handleOSChange = (os: string) => {
+    this.setState({ os }, this.handleChange);
+  };
+
+  handleProjectKeyDone = (projectKey: string) => {
+    this.setState({ projectKey }, this.handleChange);
+  };
+
+  handleProjectKeyDelete = () => {
+    this.setState({ projectKey: undefined }, this.handleChange);
+  };
+
+  renderJavaBuild = () => (
+    <div className="big-spacer-top">
+      <h4 className="spacer-bottom">
+        {translate('onboarding.language.java.build_technology')}
+      </h4>
+      <RadioToggle
+        name="java-build"
+        onCheck={this.handleJavaBuildChange}
+        options={['maven', 'gradle'].map(build => ({
+          label: translate('onboarding.language.java.build_technology', build),
+          value: build
+        }))}
+        value={this.state.javaBuild}
+      />
+    </div>
+  );
+
+  renderCFamilyCompiler = () => (
+    <div className="big-spacer-top">
+      <h4 className="spacer-bottom">
+        {translate('onboarding.language.c-family.compiler')}
+      </h4>
+      <RadioToggle
+        name="c-family-compiler"
+        onCheck={this.handleCFamilyCompilerChange}
+        options={['msvc', 'clang-gcc'].map(compiler => ({
+          label: translate('onboarding.language.c-family.compiler', compiler),
+          value: compiler
+        }))}
+        value={this.state.cFamilyCompiler}
+      />
+    </div>
+  );
+
+  renderOS = () => (
+    <div className="big-spacer-top">
+      <h4 className="spacer-bottom">
+        {translate('onboarding.language.os')}
+      </h4>
+      <RadioToggle
+        name="os"
+        onCheck={this.handleOSChange}
+        options={['linux', 'win', 'mac'].map(os => ({
+          label: translate('onboarding.language.os', os),
+          value: os
+        }))}
+        value={this.state.os}
+      />
+    </div>
+  );
+
+  renderProjectKey = () => (
+    <NewProjectForm
+      onDelete={this.handleProjectKeyDelete}
+      onDone={this.handleProjectKeyDone}
+      organization={this.props.organization}
+      projectKey={this.state.projectKey}
+    />
+  );
+
+  render() {
+    const shouldAskProjectKey =
+      this.state.language === 'dotnet' ||
+      (this.state.language === 'c-family' &&
+        (this.state.cFamilyCompiler === 'msvc' ||
+          (this.state.cFamilyCompiler === 'clang-gcc' && this.state.os != null))) ||
+      (this.state.language === 'other' && this.state.os !== undefined);
+
+    const languages = this.props.sonarCloud
+      ? ['java', 'dotnet', 'c-family', 'other']
+      : ['java', 'dotnet', 'other'];
+
+    return (
+      <div>
+        <div>
+          <h4 className="spacer-bottom">{translate('onboarding.language')}</h4>
+          <RadioToggle
+            name="language"
+            onCheck={this.handleLanguageChange}
+            options={languages.map(language => ({
+              label: translate('onboarding.language', language),
+              value: language
+            }))}
+            value={this.state.language}
+          />
+        </div>
+        {this.state.language === 'java' && this.renderJavaBuild()}
+        {this.state.language === 'c-family' && this.renderCFamilyCompiler()}
+        {((this.state.language === 'c-family' && this.state.cFamilyCompiler === 'clang-gcc') ||
+          this.state.language === 'other') &&
+          this.renderOS()}
+        {shouldAskProjectKey && this.renderProjectKey()}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewOrganizationForm.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewOrganizationForm.js
new file mode 100644 (file)
index 0000000..16a7c54
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * 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 { debounce } from 'lodash';
+import {
+  createOrganization,
+  deleteOrganization,
+  getOrganization
+} from '../../../api/organizations';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+  onDelete: () => void,
+  onDone: (organization: string) => void,
+  organization?: string
+|};
+
+type State = {
+  done: boolean,
+  loading: boolean,
+  organization: string,
+  unique: boolean
+};
+
+export default class NewOrganizationForm extends React.PureComponent {
+  mounted: boolean;
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      done: props.organization != null,
+      loading: false,
+      organization: props.organization || '',
+      unique: true
+    };
+    this.validateOrganization = debounce(this.validateOrganization, 500);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  validateOrganization = (organization: string) => {
+    getOrganization(organization).then(response => {
+      if (this.mounted) {
+        this.setState({ unique: response == null });
+      }
+    });
+  };
+
+  sanitizeOrganization = (organization: string) =>
+    organization.toLowerCase().replace(/[^a-z0-9-]/, '').replace(/^-/, '');
+
+  handleOrganizationChange = (event: { target: HTMLInputElement }) => {
+    const organization = this.sanitizeOrganization(event.target.value);
+    this.setState({ organization });
+    this.validateOrganization(organization);
+  };
+
+  handleOrganizationCreate = (event: Event) => {
+    event.preventDefault();
+    const { organization } = this.state;
+    if (organization) {
+      this.setState({ loading: true });
+      createOrganization({ key: organization, name: organization }).then(() => {
+        if (this.mounted) {
+          this.setState({ done: true, loading: false });
+          this.props.onDone(organization);
+        }
+      }, this.stopLoading);
+    }
+  };
+
+  handleOrganizationDelete = (event: Event) => {
+    event.preventDefault();
+    const { organization } = this.state;
+    if (organization) {
+      this.setState({ loading: true });
+      deleteOrganization(organization).then(() => {
+        if (this.mounted) {
+          this.setState({ done: false, loading: false, organization: '' });
+          this.props.onDelete();
+        }
+      }, this.stopLoading);
+    }
+  };
+
+  render() {
+    const { done, loading, organization, unique } = this.state;
+
+    const valid = unique && organization.length >= 2;
+
+    return done
+      ? <form onSubmit={this.handleOrganizationDelete}>
+          <span className="spacer-right text-middle">{organization}</span>
+          {loading
+            ? <i className="spinner" />
+            : <button className="button-clean">
+                <i className="icon-delete" />
+              </button>}
+        </form>
+      : <form onSubmit={this.handleOrganizationCreate}>
+          <input
+            autoFocus={true}
+            className="input-super-large spacer-right text-middle"
+            onChange={this.handleOrganizationChange}
+            maxLength={32}
+            minLength={2}
+            placeholder={translate('onboarding.organization.placeholder')}
+            required={true}
+            type="text"
+            value={organization}
+          />
+          {loading
+            ? <i className="spinner" />
+            : <button className="text-middle" disabled={!valid}>{translate('create')}</button>}
+          {!unique &&
+            <span className="big-spacer-left text-danger text-middle">
+              <i className="icon-alert-error little-spacer-right text-text-top" />
+              {translate('this_name_is_already_taken')}
+            </span>}
+          <div className="note spacer-top abs-width-300">
+            {translate('onboarding.organization.key_requirement')}
+          </div>
+        </form>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewProjectForm.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewProjectForm.js
new file mode 100644 (file)
index 0000000..2bf64f8
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * 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 { createProject, deleteProject } from '../../../api/components';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+  onDelete: () => void,
+  onDone: (projectKey: string) => void,
+  organization?: string,
+  projectKey?: string
+|};
+
+type State = {
+  done: boolean,
+  loading: boolean,
+  projectKey: string
+};
+
+export default class NewProjectForm extends React.PureComponent {
+  mounted: boolean;
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      done: props.projectKey != null,
+      loading: false,
+      projectKey: props.projectKey || ''
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  sanitizeProjectKey = (projectKey: string) => projectKey.replace(/[^a-zA-Z0-9-_\.:]/, '');
+
+  handleProjectKeyChange = (event: { target: HTMLInputElement }) => {
+    this.setState({ projectKey: this.sanitizeProjectKey(event.target.value) });
+  };
+
+  handleProjectCreate = (event: Event) => {
+    event.preventDefault();
+    const { projectKey } = this.state;
+    const data: { [string]: string } = {
+      name: projectKey,
+      project: projectKey
+    };
+    if (this.props.organization) {
+      data.organization = this.props.organization;
+    }
+    this.setState({ loading: true });
+    createProject(data).then(() => {
+      if (this.mounted) {
+        this.setState({ done: true, loading: false });
+        this.props.onDone(projectKey);
+      }
+    }, this.stopLoading);
+  };
+
+  handleProjectDelete = (event: Event) => {
+    event.preventDefault();
+    const { projectKey } = this.state;
+    this.setState({ loading: true });
+    deleteProject(projectKey).then(() => {
+      if (this.mounted) {
+        this.setState({ done: false, loading: false, projectKey: '' });
+        this.props.onDelete();
+      }
+    }, this.stopLoading);
+  };
+
+  render() {
+    const { done, loading, projectKey } = this.state;
+
+    const valid = projectKey.length > 0;
+
+    const form = done
+      ? <form onSubmit={this.handleProjectDelete}>
+          <span className="spacer-right text-middle">{projectKey}</span>
+          {loading
+            ? <i className="spinner" />
+            : <button className="button-clean">
+                <i className="icon-delete" />
+              </button>}
+        </form>
+      : <form onSubmit={this.handleProjectCreate}>
+          <input
+            autoFocus={true}
+            className="input-large spacer-right text-middle"
+            minLength={1}
+            maxLength={400}
+            onChange={this.handleProjectKeyChange}
+            required={true}
+            type="text"
+            value={projectKey}
+          />
+          {loading
+            ? <i className="spinner" />
+            : <button className="text-middle" disabled={!valid}>{translate('Done')}</button>}
+          <div className="note spacer-top abs-width-300">
+            {translate('onboarding.project_key_requirement')}
+          </div>
+        </form>;
+
+    return (
+      <div className="big-spacer-top">
+        <h4 className="spacer-bottom">
+          {translate('onboarding.language.project_key')}
+        </h4>
+        {form}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
new file mode 100644 (file)
index 0000000..0382a33
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * 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 TokenStep from './TokenStep';
+import OrganizationStep from './OrganizationStep';
+import AnalysisStep from './AnalysisStep';
+import { translate } from '../../../helpers/l10n';
+import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
+import './styles.css';
+
+type Props = {
+  currentUser: { login: string, isLoggedIn: boolean },
+  organizationsEnabled: boolean,
+  sonarCloud: boolean
+};
+
+type State = {
+  organization?: string,
+  step: string,
+  token?: string
+};
+
+export default class Onboarding extends React.PureComponent {
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = { step: props.organizationsEnabled ? 'organization' : 'token' };
+  }
+
+  componentDidMount() {
+    if (!this.props.currentUser.isLoggedIn) {
+      handleRequiredAuthentication();
+    }
+  }
+
+  handleTokenDone = (token: string) => {
+    this.setState({ step: 'analysis', token });
+  };
+
+  handleOrganizationDone = (organization: string) => {
+    this.setState({ organization, step: 'token' });
+  };
+
+  render() {
+    if (!this.props.currentUser.isLoggedIn) {
+      return null;
+    }
+
+    const { organizationsEnabled, sonarCloud } = this.props;
+    const { step, token } = this.state;
+
+    let stepNumber = 1;
+
+    return (
+      <div className="page page-limited">
+        <header className="page-header">
+          <h1 className="page-title">
+            {translate(sonarCloud ? 'onboarding.header.sonarcloud' : 'onboarding.header')}
+          </h1>
+          <div className="page-description">
+            {translate('onboarding.header.description')}
+          </div>
+        </header>
+
+        {organizationsEnabled &&
+          <OrganizationStep
+            currentUser={this.props.currentUser}
+            onContinue={this.handleOrganizationDone}
+            open={step === 'organization'}
+            stepNumber={stepNumber++}
+          />}
+
+        <TokenStep
+          onContinue={this.handleTokenDone}
+          open={step === 'token'}
+          stepNumber={stepNumber++}
+        />
+
+        <AnalysisStep
+          organization={this.state.organization}
+          open={step === 'analysis'}
+          sonarCloud={sonarCloud}
+          stepNumber={stepNumber}
+          token={token}
+        />
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js
new file mode 100644 (file)
index 0000000..a7eecc2
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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 { connect } from 'react-redux';
+import Onboarding from './Onboarding';
+import {
+  getCurrentUser,
+  areThereCustomOrganizations,
+  getSettingValue
+} from '../../../store/rootReducer';
+
+const mapStateToProps = state => {
+  const sonarCloudSetting = getSettingValue(state, 'sonar.lf.sonarqube.com.enabled');
+
+  return {
+    currentUser: getCurrentUser(state),
+    organizationsEnabled: areThereCustomOrganizations(state),
+    sonarCloud: sonarCloudSetting != null && sonarCloudSetting.value === 'true'
+  };
+};
+
+export default connect(mapStateToProps)(Onboarding);
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js
new file mode 100644 (file)
index 0000000..325e8e8
--- /dev/null
@@ -0,0 +1,240 @@
+/*
+ * 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 Select from 'react-select';
+import classNames from 'classnames';
+import { sortBy } from 'lodash';
+import Step from './Step';
+import NewOrganizationForm from './NewOrganizationForm';
+import { getMyOrganizations } from '../../../api/organizations';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+  currentUser: { login: string, isLoggedIn: boolean },
+  open: boolean,
+  onContinue: (organization: string) => void
+};
+
+type State = {
+  loading: boolean,
+  newOrganization?: string,
+  existingOrganization?: string,
+  existingOrganizations: Array<string>,
+  selection: 'personal' | 'existing' | 'new'
+};
+
+export default class OrganizationStep extends React.PureComponent {
+  mounted: boolean;
+  props: Props;
+  state: State = {
+    loading: true,
+    existingOrganizations: [],
+    selection: 'personal'
+  };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchOrganizations();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchOrganizations = () => {
+    getMyOrganizations().then(
+      organizations => {
+        if (this.mounted) {
+          this.setState({
+            loading: false,
+            existingOrganizations: sortBy(
+              organizations.filter(organization => organization !== this.props.currentUser.login)
+            )
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  getSelectedOrganization = () => {
+    switch (this.state.selection) {
+      case 'personal':
+        return this.props.currentUser.login;
+      case 'existing':
+        return this.state.existingOrganization;
+      case 'new':
+        return this.state.newOrganization;
+      default:
+        return null;
+    }
+  };
+
+  handlePersonalClick = (event: Event) => {
+    event.preventDefault();
+    this.setState({ selection: 'personal' });
+  };
+
+  handleExistingClick = (event: Event) => {
+    event.preventDefault();
+    this.setState({ selection: 'existing' });
+  };
+
+  handleNewClick = (event: Event) => {
+    event.preventDefault();
+    this.setState({ selection: 'new' });
+  };
+
+  handleOrganizationCreate = (newOrganization: string) => {
+    this.setState({ newOrganization });
+  };
+
+  handleOrganizationDelete = () => {
+    this.setState({ newOrganization: undefined });
+  };
+
+  handleExistingOrganizationSelect = ({ value }: { value: string }) => {
+    this.setState({ existingOrganization: value });
+  };
+
+  handleContinueClick = (event: Event) => {
+    event.preventDefault();
+    const organization = this.getSelectedOrganization();
+    if (organization) {
+      this.props.onContinue(organization);
+    }
+  };
+
+  renderPersonalOrganizationOption = () => (
+    <div>
+      <a className="link-base-color link-no-underline" href="#" onClick={this.handlePersonalClick}>
+        <i
+          className={classNames('icon-radio', 'spacer-right', {
+            'is-checked': this.state.selection === 'personal'
+          })}
+        />
+        {translate('onboarding.organization.my_personal_organization')}
+        <span className="note spacer-left">{this.props.currentUser.login}</span>
+      </a>
+    </div>
+  );
+
+  renderExistingOrganizationOption = () => (
+    <div className="big-spacer-top">
+      <a
+        className="js-existing link-base-color link-no-underline"
+        href="#"
+        onClick={this.handleExistingClick}>
+        <i
+          className={classNames('icon-radio', 'spacer-right', {
+            'is-checked': this.state.selection === 'existing'
+          })}
+        />
+        {translate('onboarding.organization.exising_organization')}
+      </a>
+      {this.state.selection === 'existing' &&
+        <div className="big-spacer-top">
+          <Select
+            className="input-super-large"
+            clearable={false}
+            onChange={this.handleExistingOrganizationSelect}
+            options={this.state.existingOrganizations.map(organization => ({
+              label: organization,
+              value: organization
+            }))}
+            value={this.state.existingOrganization}
+          />
+        </div>}
+    </div>
+  );
+
+  renderNewOrganizationOption = () => (
+    <div className="big-spacer-top">
+      <a
+        className="js-new link-base-color link-no-underline"
+        href="#"
+        onClick={this.handleNewClick}>
+        <i
+          className={classNames('icon-radio', 'spacer-right', {
+            'is-checked': this.state.selection === 'new'
+          })}
+        />
+        {translate('onboarding.organization.create_another_organization')}
+      </a>
+      {this.state.selection === 'new' &&
+        <div className="big-spacer-top">
+          <NewOrganizationForm
+            onDelete={this.handleOrganizationDelete}
+            onDone={this.handleOrganizationCreate}
+            organization={this.state.newOrganization}
+          />
+        </div>}
+    </div>
+  );
+
+  renderForm = () => {
+    return (
+      <div className="boxed-group-inner">
+        <div className="big-spacer-bottom width-50">
+          {translate('onboarding.organization.text')}
+        </div>
+
+        {this.renderPersonalOrganizationOption()}
+        {this.state.existingOrganizations.length > 0 && this.renderExistingOrganizationOption()}
+        {this.renderNewOrganizationOption()}
+
+        {this.getSelectedOrganization() != null &&
+          <div className="big-spacer-top">
+            <button className="js-continue" onClick={this.handleContinueClick}>
+              {translate('continue')}
+            </button>
+          </div>}
+      </div>
+    );
+  };
+
+  renderResult = () => {
+    const result = this.getSelectedOrganization();
+
+    return result != null
+      ? <div className="boxed-group-actions">
+          <i className="icon-check spacer-right" />
+          <strong>{result}</strong>
+        </div>
+      : null;
+  };
+
+  render() {
+    return (
+      <Step
+        open={this.props.open}
+        renderForm={this.renderForm}
+        renderResult={this.renderResult}
+        stepNumber={1}
+        stepTitle={translate('onboarding.organization.header')}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js
new file mode 100644 (file)
index 0000000..156fc34
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * 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 { createProject, deleteProject } from '../../../api/components';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+  onDelete: () => void,
+  onDone: (projectKey: string) => void,
+  organization?: string,
+  projectKey?: string
+};
+
+type State = {
+  done: boolean,
+  loading: boolean,
+  projectKey: string
+};
+
+export default class ProjectKeyStep extends React.PureComponent {
+  mounted: boolean;
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      done: props.projectKey != null,
+      loading: false,
+      projectKey: props.projectKey || ''
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  sanitizeProjectKey = (projectKey: string) => projectKey.replace(/[^a-zA-Z0-9-_\.:]/, '');
+
+  handleProjectKeyChange = (event: { target: HTMLInputElement }) => {
+    this.setState({ projectKey: this.sanitizeProjectKey(event.target.value) });
+  };
+
+  handleProjectCreate = (event: Event) => {
+    event.preventDefault();
+    const { projectKey } = this.state;
+    const data: { [string]: string } = {
+      name: projectKey,
+      project: projectKey
+    };
+    if (this.props.organization) {
+      data.organization = this.props.organization;
+    }
+    this.setState({ loading: true });
+    createProject(data).then(() => {
+      if (this.mounted) {
+        this.setState({ done: true, loading: false });
+        this.props.onDone(projectKey);
+      }
+    }, this.stopLoading);
+  };
+
+  handleProjectDelete = (event: Event) => {
+    event.preventDefault();
+    const { projectKey } = this.state;
+    this.setState({ loading: true });
+    deleteProject(projectKey).then(() => {
+      if (this.mounted) {
+        this.setState({ done: false, loading: false, projectKey: '' });
+        this.props.onDelete();
+      }
+    }, this.stopLoading);
+  };
+
+  render() {
+    const { done, loading, projectKey } = this.state;
+
+    const valid = projectKey.length > 0;
+
+    const form = done
+      ? <form onSubmit={this.handleProjectDelete}>
+          <span className="spacer-right text-middle">{projectKey}</span>
+          {loading
+            ? <i className="spinner" />
+            : <button className="button-clean">
+                <i className="icon-delete" />
+              </button>}
+        </form>
+      : <form onSubmit={this.handleProjectCreate}>
+          <input
+            autoFocus={true}
+            className="input-large spacer-right text-middle"
+            minLength={1}
+            maxLength={400}
+            onChange={this.handleProjectKeyChange}
+            required={true}
+            type="text"
+            value={projectKey}
+          />
+          {loading
+            ? <i className="spinner" />
+            : <button className="text-middle" disabled={!valid}>{translate('Done')}</button>}
+          <div className="note spacer-top abs-width-300">
+            {translate('onboarding.project_key_requirement')}
+          </div>
+        </form>;
+
+    return (
+      <div className="big-spacer-top">
+        <h4 className="spacer-bottom">
+          {translate('onboarding.language.project_key')}
+        </h4>
+        {form}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Step.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Step.js
new file mode 100644 (file)
index 0000000..763cef6
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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 classNames from 'classnames';
+
+type Props = {
+  open: boolean,
+  renderForm: () => React.Element<*>,
+  renderResult: () => ?React.Element<*>,
+  stepNumber: number,
+  stepTitle: string
+};
+
+export default function Step(props: Props) {
+  const className = classNames('boxed-group', 'onboarding-step', {
+    'onboarding-step-open': props.open
+  });
+
+  return (
+    <div className={className}>
+      <div className="onboarding-step-number">{props.stepNumber}</div>
+      {!props.open && props.renderResult()}
+      <div className="boxed-group-header">
+        <h2>{props.stepTitle}</h2>
+      </div>
+      {props.open ? props.renderForm() : <div className="boxed-group-inner" />}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js
new file mode 100644 (file)
index 0000000..9a171f7
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * 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 Step from './Step';
+import { generateToken, revokeToken } from '../../../api/user-tokens';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+  open: boolean,
+  onContinue: (token: string) => void,
+  stepNumber: number
+};
+
+type State = {
+  loading: boolean,
+  tokenName?: string,
+  token?: string
+};
+
+export default class TokenStep extends React.PureComponent {
+  mounted: boolean;
+  props: Props;
+
+  static defaultProps = {
+    stepNumber: 1
+  };
+
+  state: State = {
+    loading: false
+  };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleTokenNameChange = (event: { target: HTMLInputElement }) => {
+    this.setState({ tokenName: event.target.value });
+  };
+
+  handleTokenGenerate = (event: Event) => {
+    event.preventDefault();
+    const { tokenName } = this.state;
+    if (tokenName) {
+      this.setState({ loading: true });
+      generateToken(tokenName).then(
+        ({ token }) => {
+          if (this.mounted) {
+            this.setState({ loading: false, token });
+          }
+        },
+        () => {
+          if (this.mounted) {
+            this.setState({ loading: false });
+          }
+        }
+      );
+    }
+  };
+
+  handleTokenRevoke = (event: Event) => {
+    event.preventDefault();
+    const { tokenName } = this.state;
+    if (tokenName) {
+      this.setState({ loading: true });
+      revokeToken(tokenName).then(
+        () => {
+          if (this.mounted) {
+            this.setState({ loading: false, token: undefined, tokenName: undefined });
+          }
+        },
+        () => {
+          if (this.mounted) {
+            this.setState({ loading: false });
+          }
+        }
+      );
+    }
+  };
+
+  handleContinueClick = (event: Event) => {
+    event.preventDefault();
+    if (this.state.token) {
+      this.props.onContinue(this.state.token);
+    }
+  };
+
+  renderForm = () => {
+    const { loading, token, tokenName } = this.state;
+
+    return (
+      <div className="boxed-group-inner">
+        <div className="big-spacer-bottom width-50">
+          {translate('onboarding.token.text')}
+        </div>
+
+        {token != null
+          ? <form onSubmit={this.handleTokenRevoke}>
+              {tokenName}{': '}
+              <span className="monospaced spacer-right">{token}</span>
+              {loading
+                ? <i className="spinner" />
+                : <button className="button-clean" onClick={this.handleTokenRevoke}>
+                    <i className="icon-delete" />
+                  </button>}
+            </form>
+          : <form onSubmit={this.handleTokenGenerate}>
+              <input
+                autoFocus={true}
+                className="input-large spacer-right"
+                onChange={this.handleTokenNameChange}
+                placeholder={translate('onboarding.token.placeholder')}
+                required={true}
+                type="text"
+                value={tokenName || ''}
+              />
+              {loading
+                ? <i className="spinner" />
+                : <button>{translate('onboarding.token.generate')}</button>}
+            </form>}
+
+        {token != null &&
+          <div className="big-spacer-top">
+            <button className="js-continue" onClick={this.handleContinueClick}>
+              {translate('continue')}
+            </button>
+          </div>}
+      </div>
+    );
+  };
+
+  renderResult = () => {
+    const { token, tokenName } = this.state;
+
+    if (!token) {
+      return null;
+    }
+
+    return (
+      <div className="boxed-group-actions">
+        <i className="icon-check spacer-right" />
+        {tokenName}{': '}
+        <strong className="monospaced">{token}</strong>
+      </div>
+    );
+  };
+
+  render() {
+    return (
+      <Step
+        open={this.props.open}
+        renderForm={this.renderForm}
+        renderResult={this.renderResult}
+        stepNumber={this.props.stepNumber}
+        stepTitle={translate('onboarding.token.header')}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/LanguageStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/LanguageStep-test.js
new file mode 100644 (file)
index 0000000..414b0db
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * 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 } from 'enzyme';
+import LanguageStep from '../LanguageStep';
+
+it('selects java', () => {
+  const onDone = jest.fn();
+  const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />);
+
+  wrapper.find('RadioToggle').prop('onCheck')('java');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('RadioToggle').at(1).prop('onCheck')('maven');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+  expect(onDone).lastCalledWith({ language: 'java', javaBuild: 'maven' });
+
+  wrapper.find('RadioToggle').at(1).prop('onCheck')('gradle');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+  expect(onDone).lastCalledWith({ language: 'java', javaBuild: 'gradle' });
+});
+
+it('selects c#', () => {
+  const onDone = jest.fn();
+  const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />);
+
+  wrapper.find('RadioToggle').prop('onCheck')('dotnet');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('NewProjectForm').prop('onDone')('project-foo');
+  expect(onDone).lastCalledWith({ language: 'dotnet', projectKey: 'project-foo' });
+});
+
+it('selects c-family', () => {
+  const onDone = jest.fn();
+  const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} sonarCloud={true} />);
+
+  wrapper.find('RadioToggle').prop('onCheck')('c-family');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('RadioToggle').at(1).prop('onCheck')('msvc');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('NewProjectForm').prop('onDone')('project-foo');
+  expect(onDone).lastCalledWith({
+    language: 'c-family',
+    cFamilyCompiler: 'msvc',
+    projectKey: 'project-foo'
+  });
+
+  wrapper.find('RadioToggle').at(1).prop('onCheck')('clang-gcc');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('RadioToggle').at(2).prop('onCheck')('linux');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('NewProjectForm').prop('onDone')('project-foo');
+  expect(onDone).lastCalledWith({
+    language: 'c-family',
+    cFamilyCompiler: 'clang-gcc',
+    os: 'linux',
+    projectKey: 'project-foo'
+  });
+});
+
+it('selects other', () => {
+  const onDone = jest.fn();
+  const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />);
+
+  wrapper.find('RadioToggle').prop('onCheck')('other');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('RadioToggle').at(1).prop('onCheck')('mac');
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.find('NewProjectForm').prop('onDone')('project-foo');
+  expect(onDone).lastCalledWith({ language: 'other', os: 'mac', projectKey: 'project-foo' });
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewOrganizationForm-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewOrganizationForm-test.js
new file mode 100644 (file)
index 0000000..79a5542
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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 { mount } from 'enzyme';
+import NewOrganizationForm from '../NewOrganizationForm';
+import { change, doAsync, submit } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/organizations', () => ({
+  createOrganization: () => Promise.resolve(),
+  deleteOrganization: () => Promise.resolve(),
+  getOrganization: () => Promise.resolve(null)
+}));
+
+it('creates new organization', () => {
+  const onDone = jest.fn();
+  const wrapper = mount(<NewOrganizationForm onDelete={jest.fn()} onDone={onDone} />);
+  expect(wrapper).toMatchSnapshot();
+  change(wrapper.find('input'), 'foo');
+  submit(wrapper.find('form'));
+  expect(wrapper).toMatchSnapshot(); // spinner
+  return doAsync(() => {
+    expect(wrapper).toMatchSnapshot();
+    expect(onDone).toBeCalledWith('foo');
+  });
+});
+
+it('deletes organization', () => {
+  const onDelete = jest.fn();
+  const wrapper = mount(<NewOrganizationForm onDelete={onDelete} onDone={jest.fn()} />);
+  wrapper.setState({ done: true, loading: false, organization: 'foo' });
+  expect(wrapper).toMatchSnapshot();
+  submit(wrapper.find('form'));
+  expect(wrapper).toMatchSnapshot(); // spinner
+  return doAsync(() => {
+    expect(wrapper).toMatchSnapshot();
+    expect(onDelete).toBeCalled();
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewProjectForm-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewProjectForm-test.js
new file mode 100644 (file)
index 0000000..7b05724
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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 { mount } from 'enzyme';
+import NewProjectForm from '../NewProjectForm';
+import { change, doAsync, submit } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/components', () => ({
+  createProject: () => Promise.resolve(),
+  deleteProject: () => Promise.resolve()
+}));
+
+it('creates new project', () => {
+  const onDone = jest.fn();
+  const wrapper = mount(<NewProjectForm onDelete={jest.fn()} onDone={onDone} />);
+  expect(wrapper).toMatchSnapshot();
+  change(wrapper.find('input'), 'foo');
+  submit(wrapper.find('form'));
+  expect(wrapper).toMatchSnapshot(); // spinner
+  return doAsync(() => {
+    expect(wrapper).toMatchSnapshot();
+    expect(onDone).toBeCalledWith('foo');
+  });
+});
+
+it('deletes project', () => {
+  const onDelete = jest.fn();
+  const wrapper = mount(<NewProjectForm onDelete={onDelete} onDone={jest.fn()} />);
+  wrapper.setState({ done: true, loading: false, projectKey: 'foo' });
+  expect(wrapper).toMatchSnapshot();
+  submit(wrapper.find('form'));
+  expect(wrapper).toMatchSnapshot(); // spinner
+  return doAsync(() => {
+    expect(wrapper).toMatchSnapshot();
+    expect(onDelete).toBeCalled();
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js
new file mode 100644 (file)
index 0000000..9352556
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 { mount } from 'enzyme';
+import OrganizationStep from '../OrganizationStep';
+import { click, doAsync } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/organizations', () => ({
+  getMyOrganizations: () => Promise.resolve(['user', 'another'])
+}));
+
+const currentUser = { isLoggedIn: true, login: 'user' };
+
+it('works with personal organization', () => {
+  const onContinue = jest.fn();
+  const wrapper = mount(
+    <OrganizationStep currentUser={currentUser} onContinue={onContinue} open={true} />
+  );
+  click(wrapper.find('.js-continue'));
+  expect(onContinue).toBeCalledWith('user');
+});
+
+it('works with existing organization', () => {
+  const onContinue = jest.fn();
+  const wrapper = mount(
+    <OrganizationStep currentUser={currentUser} onContinue={onContinue} open={true} />
+  );
+  return doAsync(() => {
+    click(wrapper.find('.js-existing'));
+    wrapper.find('Select').prop('onChange')({ value: 'another' });
+    click(wrapper.find('.js-continue'));
+    expect(onContinue).toBeCalledWith('another');
+  });
+});
+
+it('works with new organization', () => {
+  const onContinue = jest.fn();
+  const wrapper = mount(
+    <OrganizationStep currentUser={currentUser} onContinue={onContinue} open={true} />
+  );
+  click(wrapper.find('.js-new'));
+  wrapper.find('NewOrganizationForm').prop('onDone')('new');
+  click(wrapper.find('.js-continue'));
+  expect(onContinue).toBeCalledWith('new');
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js
new file mode 100644 (file)
index 0000000..2af50f6
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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 { mount } from 'enzyme';
+import ProjectKeyStep from '../ProjectKeyStep';
+import { change, doAsync, submit } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/components', () => ({
+  createProject: () => Promise.resolve(),
+  deleteProject: () => Promise.resolve()
+}));
+
+it('creates new project', () => {
+  const onDone = jest.fn();
+  const wrapper = mount(<ProjectKeyStep onDelete={jest.fn()} onDone={onDone} />);
+  expect(wrapper).toMatchSnapshot();
+  change(wrapper.find('input'), 'foo');
+  submit(wrapper.find('form'));
+  expect(wrapper).toMatchSnapshot(); // spinner
+  return doAsync(() => {
+    expect(wrapper).toMatchSnapshot();
+    expect(onDone).toBeCalledWith('foo');
+  });
+});
+
+it('deletes project', () => {
+  const onDelete = jest.fn();
+  const wrapper = mount(<ProjectKeyStep onDelete={onDelete} onDone={jest.fn()} />);
+  wrapper.setState({ done: true, loading: false, projectKey: 'foo' });
+  expect(wrapper).toMatchSnapshot();
+  submit(wrapper.find('form'));
+  expect(wrapper).toMatchSnapshot(); // spinner
+  return doAsync(() => {
+    expect(wrapper).toMatchSnapshot();
+    expect(onDelete).toBeCalled();
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Step-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Step-test.js
new file mode 100644 (file)
index 0000000..89d7a3b
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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 } from 'enzyme';
+import Step from '../Step';
+
+it('renders', () => {
+  const wrapper = shallow(
+    <Step
+      open={true}
+      renderForm={() => <div>form</div>}
+      renderResult={() => <div>result</div>}
+      stepNumber={1}
+      stepTitle="First Step"
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setProps({ open: false });
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js
new file mode 100644 (file)
index 0000000..9498fc2
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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 { mount } from 'enzyme';
+import TokenStep from '../TokenStep';
+import { change, click, doAsync, submit } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/user-tokens', () => ({
+  generateToken: () => Promise.resolve({ token: 'abcd1234' }),
+  revokeToken: () => Promise.resolve()
+}));
+
+it('generates token', () => {
+  const wrapper = mount(<TokenStep open={true} onContinue={jest.fn()} />);
+  expect(wrapper).toMatchSnapshot();
+  change(wrapper.find('input'), 'my token');
+  submit(wrapper.find('form'));
+  expect(wrapper).toMatchSnapshot(); // spinner
+  return doAsync(() => expect(wrapper).toMatchSnapshot());
+});
+
+it('revokes token', () => {
+  const wrapper = mount(<TokenStep open={true} onContinue={jest.fn()} />);
+  wrapper.setState({ token: 'abcd1234', tokenName: 'my token' });
+  expect(wrapper).toMatchSnapshot();
+  submit(wrapper.find('form'));
+  expect(wrapper).toMatchSnapshot(); // spinner
+  return doAsync(() => expect(wrapper).toMatchSnapshot());
+});
+
+it('continues', () => {
+  const onContinue = jest.fn();
+  const wrapper = mount(<TokenStep open={true} onContinue={onContinue} />);
+  wrapper.setState({ token: 'abcd1234', tokenName: 'my token' });
+  click(wrapper.find('.js-continue'));
+  expect(onContinue).toBeCalledWith('abcd1234');
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/LanguageStep-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/LanguageStep-test.js.snap
new file mode 100644 (file)
index 0000000..8e82d38
--- /dev/null
@@ -0,0 +1,687 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`selects c# 1`] = `
+<div>
+  <div>
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="language"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java",
+            "value": "java",
+          },
+          Object {
+            "label": "onboarding.language.dotnet",
+            "value": "dotnet",
+          },
+          Object {
+            "label": "onboarding.language.other",
+            "value": "other",
+          },
+        ]
+      }
+      value="dotnet"
+    />
+  </div>
+  <NewProjectForm
+    onDelete={[Function]}
+    onDone={[Function]}
+  />
+</div>
+`;
+
+exports[`selects c-family 1`] = `
+<div>
+  <div>
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="language"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java",
+            "value": "java",
+          },
+          Object {
+            "label": "onboarding.language.dotnet",
+            "value": "dotnet",
+          },
+          Object {
+            "label": "onboarding.language.c-family",
+            "value": "c-family",
+          },
+          Object {
+            "label": "onboarding.language.other",
+            "value": "other",
+          },
+        ]
+      }
+      value="c-family"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.c-family.compiler
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="c-family-compiler"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.c-family.compiler.msvc",
+            "value": "msvc",
+          },
+          Object {
+            "label": "onboarding.language.c-family.compiler.clang-gcc",
+            "value": "clang-gcc",
+          },
+        ]
+      }
+      value={null}
+    />
+  </div>
+</div>
+`;
+
+exports[`selects c-family 2`] = `
+<div>
+  <div>
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="language"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java",
+            "value": "java",
+          },
+          Object {
+            "label": "onboarding.language.dotnet",
+            "value": "dotnet",
+          },
+          Object {
+            "label": "onboarding.language.c-family",
+            "value": "c-family",
+          },
+          Object {
+            "label": "onboarding.language.other",
+            "value": "other",
+          },
+        ]
+      }
+      value="c-family"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.c-family.compiler
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="c-family-compiler"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.c-family.compiler.msvc",
+            "value": "msvc",
+          },
+          Object {
+            "label": "onboarding.language.c-family.compiler.clang-gcc",
+            "value": "clang-gcc",
+          },
+        ]
+      }
+      value="msvc"
+    />
+  </div>
+  <NewProjectForm
+    onDelete={[Function]}
+    onDone={[Function]}
+  />
+</div>
+`;
+
+exports[`selects c-family 3`] = `
+<div>
+  <div>
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="language"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java",
+            "value": "java",
+          },
+          Object {
+            "label": "onboarding.language.dotnet",
+            "value": "dotnet",
+          },
+          Object {
+            "label": "onboarding.language.c-family",
+            "value": "c-family",
+          },
+          Object {
+            "label": "onboarding.language.other",
+            "value": "other",
+          },
+        ]
+      }
+      value="c-family"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.c-family.compiler
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="c-family-compiler"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.c-family.compiler.msvc",
+            "value": "msvc",
+          },
+          Object {
+            "label": "onboarding.language.c-family.compiler.clang-gcc",
+            "value": "clang-gcc",
+          },
+        ]
+      }
+      value="clang-gcc"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.os
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="os"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.os.linux",
+            "value": "linux",
+          },
+          Object {
+            "label": "onboarding.language.os.win",
+            "value": "win",
+          },
+          Object {
+            "label": "onboarding.language.os.mac",
+            "value": "mac",
+          },
+        ]
+      }
+      value={null}
+    />
+  </div>
+</div>
+`;
+
+exports[`selects c-family 4`] = `
+<div>
+  <div>
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="language"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java",
+            "value": "java",
+          },
+          Object {
+            "label": "onboarding.language.dotnet",
+            "value": "dotnet",
+          },
+          Object {
+            "label": "onboarding.language.c-family",
+            "value": "c-family",
+          },
+          Object {
+            "label": "onboarding.language.other",
+            "value": "other",
+          },
+        ]
+      }
+      value="c-family"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.c-family.compiler
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="c-family-compiler"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.c-family.compiler.msvc",
+            "value": "msvc",
+          },
+          Object {
+            "label": "onboarding.language.c-family.compiler.clang-gcc",
+            "value": "clang-gcc",
+          },
+        ]
+      }
+      value="clang-gcc"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.os
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="os"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.os.linux",
+            "value": "linux",
+          },
+          Object {
+            "label": "onboarding.language.os.win",
+            "value": "win",
+          },
+          Object {
+            "label": "onboarding.language.os.mac",
+            "value": "mac",
+          },
+        ]
+      }
+      value="linux"
+    />
+  </div>
+  <NewProjectForm
+    onDelete={[Function]}
+    onDone={[Function]}
+    projectKey="project-foo"
+  />
+</div>
+`;
+
+exports[`selects java 1`] = `
+<div>
+  <div>
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="language"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java",
+            "value": "java",
+          },
+          Object {
+            "label": "onboarding.language.dotnet",
+            "value": "dotnet",
+          },
+          Object {
+            "label": "onboarding.language.other",
+            "value": "other",
+          },
+        ]
+      }
+      value="java"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.java.build_technology
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="java-build"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java.build_technology.maven",
+            "value": "maven",
+          },
+          Object {
+            "label": "onboarding.language.java.build_technology.gradle",
+            "value": "gradle",
+          },
+        ]
+      }
+      value={null}
+    />
+  </div>
+</div>
+`;
+
+exports[`selects java 2`] = `
+<div>
+  <div>
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="language"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java",
+            "value": "java",
+          },
+          Object {
+            "label": "onboarding.language.dotnet",
+            "value": "dotnet",
+          },
+          Object {
+            "label": "onboarding.language.other",
+            "value": "other",
+          },
+        ]
+      }
+      value="java"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.java.build_technology
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="java-build"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java.build_technology.maven",
+            "value": "maven",
+          },
+          Object {
+            "label": "onboarding.language.java.build_technology.gradle",
+            "value": "gradle",
+          },
+        ]
+      }
+      value="maven"
+    />
+  </div>
+</div>
+`;
+
+exports[`selects java 3`] = `
+<div>
+  <div>
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="language"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java",
+            "value": "java",
+          },
+          Object {
+            "label": "onboarding.language.dotnet",
+            "value": "dotnet",
+          },
+          Object {
+            "label": "onboarding.language.other",
+            "value": "other",
+          },
+        ]
+      }
+      value="java"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.java.build_technology
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="java-build"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java.build_technology.maven",
+            "value": "maven",
+          },
+          Object {
+            "label": "onboarding.language.java.build_technology.gradle",
+            "value": "gradle",
+          },
+        ]
+      }
+      value="gradle"
+    />
+  </div>
+</div>
+`;
+
+exports[`selects other 1`] = `
+<div>
+  <div>
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="language"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java",
+            "value": "java",
+          },
+          Object {
+            "label": "onboarding.language.dotnet",
+            "value": "dotnet",
+          },
+          Object {
+            "label": "onboarding.language.other",
+            "value": "other",
+          },
+        ]
+      }
+      value="other"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.os
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="os"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.os.linux",
+            "value": "linux",
+          },
+          Object {
+            "label": "onboarding.language.os.win",
+            "value": "win",
+          },
+          Object {
+            "label": "onboarding.language.os.mac",
+            "value": "mac",
+          },
+        ]
+      }
+      value={null}
+    />
+  </div>
+</div>
+`;
+
+exports[`selects other 2`] = `
+<div>
+  <div>
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="language"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.java",
+            "value": "java",
+          },
+          Object {
+            "label": "onboarding.language.dotnet",
+            "value": "dotnet",
+          },
+          Object {
+            "label": "onboarding.language.other",
+            "value": "other",
+          },
+        ]
+      }
+      value="other"
+    />
+  </div>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.os
+    </h4>
+    <RadioToggle
+      disabled={false}
+      name="os"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "label": "onboarding.language.os.linux",
+            "value": "linux",
+          },
+          Object {
+            "label": "onboarding.language.os.win",
+            "value": "win",
+          },
+          Object {
+            "label": "onboarding.language.os.mac",
+            "value": "mac",
+          },
+        ]
+      }
+      value="mac"
+    />
+  </div>
+  <NewProjectForm
+    onDelete={[Function]}
+    onDone={[Function]}
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewOrganizationForm-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewOrganizationForm-test.js.snap
new file mode 100644 (file)
index 0000000..c5f0545
--- /dev/null
@@ -0,0 +1,168 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`creates new organization 1`] = `
+<NewOrganizationForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <input
+      autoFocus={true}
+      className="input-super-large spacer-right text-middle"
+      maxLength={32}
+      minLength={2}
+      onChange={[Function]}
+      placeholder="onboarding.organization.placeholder"
+      required={true}
+      type="text"
+      value=""
+    />
+    <button
+      className="text-middle"
+      disabled={true}
+    >
+      create
+    </button>
+    <div
+      className="note spacer-top abs-width-300"
+    >
+      onboarding.organization.key_requirement
+    </div>
+  </form>
+</NewOrganizationForm>
+`;
+
+exports[`creates new organization 2`] = `
+<NewOrganizationForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <input
+      autoFocus={true}
+      className="input-super-large spacer-right text-middle"
+      maxLength={32}
+      minLength={2}
+      onChange={[Function]}
+      placeholder="onboarding.organization.placeholder"
+      required={true}
+      type="text"
+      value="foo"
+    />
+    <i
+      className="spinner"
+    />
+    <div
+      className="note spacer-top abs-width-300"
+    >
+      onboarding.organization.key_requirement
+    </div>
+  </form>
+</NewOrganizationForm>
+`;
+
+exports[`creates new organization 3`] = `
+<NewOrganizationForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <span
+      className="spacer-right text-middle"
+    >
+      foo
+    </span>
+    <button
+      className="button-clean"
+    >
+      <i
+        className="icon-delete"
+      />
+    </button>
+  </form>
+</NewOrganizationForm>
+`;
+
+exports[`deletes organization 1`] = `
+<NewOrganizationForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <span
+      className="spacer-right text-middle"
+    >
+      foo
+    </span>
+    <button
+      className="button-clean"
+    >
+      <i
+        className="icon-delete"
+      />
+    </button>
+  </form>
+</NewOrganizationForm>
+`;
+
+exports[`deletes organization 2`] = `
+<NewOrganizationForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <span
+      className="spacer-right text-middle"
+    >
+      foo
+    </span>
+    <i
+      className="spinner"
+    />
+  </form>
+</NewOrganizationForm>
+`;
+
+exports[`deletes organization 3`] = `
+<NewOrganizationForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <input
+      autoFocus={true}
+      className="input-super-large spacer-right text-middle"
+      maxLength={32}
+      minLength={2}
+      onChange={[Function]}
+      placeholder="onboarding.organization.placeholder"
+      required={true}
+      type="text"
+      value=""
+    />
+    <button
+      className="text-middle"
+      disabled={true}
+    >
+      create
+    </button>
+    <div
+      className="note spacer-top abs-width-300"
+    >
+      onboarding.organization.key_requirement
+    </div>
+  </form>
+</NewOrganizationForm>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewProjectForm-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewProjectForm-test.js.snap
new file mode 100644 (file)
index 0000000..50ef335
--- /dev/null
@@ -0,0 +1,219 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`creates new project 1`] = `
+<NewProjectForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <input
+        autoFocus={true}
+        className="input-large spacer-right text-middle"
+        maxLength={400}
+        minLength={1}
+        onChange={[Function]}
+        required={true}
+        type="text"
+        value=""
+      />
+      <button
+        className="text-middle"
+        disabled={true}
+      >
+        Done
+      </button>
+      <div
+        className="note spacer-top abs-width-300"
+      >
+        onboarding.project_key_requirement
+      </div>
+    </form>
+  </div>
+</NewProjectForm>
+`;
+
+exports[`creates new project 2`] = `
+<NewProjectForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <input
+        autoFocus={true}
+        className="input-large spacer-right text-middle"
+        maxLength={400}
+        minLength={1}
+        onChange={[Function]}
+        required={true}
+        type="text"
+        value="foo"
+      />
+      <i
+        className="spinner"
+      />
+      <div
+        className="note spacer-top abs-width-300"
+      >
+        onboarding.project_key_requirement
+      </div>
+    </form>
+  </div>
+</NewProjectForm>
+`;
+
+exports[`creates new project 3`] = `
+<NewProjectForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <span
+        className="spacer-right text-middle"
+      >
+        foo
+      </span>
+      <button
+        className="button-clean"
+      >
+        <i
+          className="icon-delete"
+        />
+      </button>
+    </form>
+  </div>
+</NewProjectForm>
+`;
+
+exports[`deletes project 1`] = `
+<NewProjectForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <span
+        className="spacer-right text-middle"
+      >
+        foo
+      </span>
+      <button
+        className="button-clean"
+      >
+        <i
+          className="icon-delete"
+        />
+      </button>
+    </form>
+  </div>
+</NewProjectForm>
+`;
+
+exports[`deletes project 2`] = `
+<NewProjectForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <span
+        className="spacer-right text-middle"
+      >
+        foo
+      </span>
+      <i
+        className="spinner"
+      />
+    </form>
+  </div>
+</NewProjectForm>
+`;
+
+exports[`deletes project 3`] = `
+<NewProjectForm
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <input
+        autoFocus={true}
+        className="input-large spacer-right text-middle"
+        maxLength={400}
+        minLength={1}
+        onChange={[Function]}
+        required={true}
+        type="text"
+        value=""
+      />
+      <button
+        className="text-middle"
+        disabled={true}
+      >
+        Done
+      </button>
+      <div
+        className="note spacer-top abs-width-300"
+      >
+        onboarding.project_key_requirement
+      </div>
+    </form>
+  </div>
+</NewProjectForm>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap
new file mode 100644 (file)
index 0000000..cbfbf1d
--- /dev/null
@@ -0,0 +1,219 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`creates new project 1`] = `
+<ProjectKeyStep
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <input
+        autoFocus={true}
+        className="input-large spacer-right text-middle"
+        maxLength={400}
+        minLength={1}
+        onChange={[Function]}
+        required={true}
+        type="text"
+        value=""
+      />
+      <button
+        className="text-middle"
+        disabled={true}
+      >
+        Done
+      </button>
+      <div
+        className="note spacer-top abs-width-300"
+      >
+        onboarding.project_key_requirement
+      </div>
+    </form>
+  </div>
+</ProjectKeyStep>
+`;
+
+exports[`creates new project 2`] = `
+<ProjectKeyStep
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <input
+        autoFocus={true}
+        className="input-large spacer-right text-middle"
+        maxLength={400}
+        minLength={1}
+        onChange={[Function]}
+        required={true}
+        type="text"
+        value="foo"
+      />
+      <i
+        className="spinner"
+      />
+      <div
+        className="note spacer-top abs-width-300"
+      >
+        onboarding.project_key_requirement
+      </div>
+    </form>
+  </div>
+</ProjectKeyStep>
+`;
+
+exports[`creates new project 3`] = `
+<ProjectKeyStep
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <span
+        className="spacer-right text-middle"
+      >
+        foo
+      </span>
+      <button
+        className="button-clean"
+      >
+        <i
+          className="icon-delete"
+        />
+      </button>
+    </form>
+  </div>
+</ProjectKeyStep>
+`;
+
+exports[`deletes project 1`] = `
+<ProjectKeyStep
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <span
+        className="spacer-right text-middle"
+      >
+        foo
+      </span>
+      <button
+        className="button-clean"
+      >
+        <i
+          className="icon-delete"
+        />
+      </button>
+    </form>
+  </div>
+</ProjectKeyStep>
+`;
+
+exports[`deletes project 2`] = `
+<ProjectKeyStep
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <span
+        className="spacer-right text-middle"
+      >
+        foo
+      </span>
+      <i
+        className="spinner"
+      />
+    </form>
+  </div>
+</ProjectKeyStep>
+`;
+
+exports[`deletes project 3`] = `
+<ProjectKeyStep
+  onDelete={[Function]}
+  onDone={[Function]}
+>
+  <div
+    className="big-spacer-top"
+  >
+    <h4
+      className="spacer-bottom"
+    >
+      onboarding.language.project_key
+    </h4>
+    <form
+      onSubmit={[Function]}
+    >
+      <input
+        autoFocus={true}
+        className="input-large spacer-right text-middle"
+        maxLength={400}
+        minLength={1}
+        onChange={[Function]}
+        required={true}
+        type="text"
+        value=""
+      />
+      <button
+        className="text-middle"
+        disabled={true}
+      >
+        Done
+      </button>
+      <div
+        className="note spacer-top abs-width-300"
+      >
+        onboarding.project_key_requirement
+      </div>
+    </form>
+  </div>
+</ProjectKeyStep>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Step-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Step-test.js.snap
new file mode 100644 (file)
index 0000000..1df068a
--- /dev/null
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div
+  className="boxed-group onboarding-step onboarding-step-open"
+>
+  <div
+    className="onboarding-step-number"
+  >
+    1
+  </div>
+  <div
+    className="boxed-group-header"
+  >
+    <h2>
+      First Step
+    </h2>
+  </div>
+  <div>
+    form
+  </div>
+</div>
+`;
+
+exports[`renders 2`] = `
+<div
+  className="boxed-group onboarding-step"
+>
+  <div
+    className="onboarding-step-number"
+  >
+    1
+  </div>
+  <div>
+    result
+  </div>
+  <div
+    className="boxed-group-header"
+  >
+    <h2>
+      First Step
+    </h2>
+  </div>
+  <div
+    className="boxed-group-inner"
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap
new file mode 100644 (file)
index 0000000..73ae389
--- /dev/null
@@ -0,0 +1,383 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`generates token 1`] = `
+<TokenStep
+  onContinue={[Function]}
+  open={true}
+  stepNumber={1}
+>
+  <Step
+    open={true}
+    renderForm={[Function]}
+    renderResult={[Function]}
+    stepNumber={1}
+    stepTitle="onboarding.token.header"
+  >
+    <div
+      className="boxed-group onboarding-step onboarding-step-open"
+    >
+      <div
+        className="onboarding-step-number"
+      >
+        1
+      </div>
+      <div
+        className="boxed-group-header"
+      >
+        <h2>
+          onboarding.token.header
+        </h2>
+      </div>
+      <div
+        className="boxed-group-inner"
+      >
+        <div
+          className="big-spacer-bottom width-50"
+        >
+          onboarding.token.text
+        </div>
+        <form
+          onSubmit={[Function]}
+        >
+          <input
+            autoFocus={true}
+            className="input-large spacer-right"
+            onChange={[Function]}
+            placeholder="onboarding.token.placeholder"
+            required={true}
+            type="text"
+            value=""
+          />
+          <button>
+            onboarding.token.generate
+          </button>
+        </form>
+      </div>
+    </div>
+  </Step>
+</TokenStep>
+`;
+
+exports[`generates token 2`] = `
+<TokenStep
+  onContinue={[Function]}
+  open={true}
+  stepNumber={1}
+>
+  <Step
+    open={true}
+    renderForm={[Function]}
+    renderResult={[Function]}
+    stepNumber={1}
+    stepTitle="onboarding.token.header"
+  >
+    <div
+      className="boxed-group onboarding-step onboarding-step-open"
+    >
+      <div
+        className="onboarding-step-number"
+      >
+        1
+      </div>
+      <div
+        className="boxed-group-header"
+      >
+        <h2>
+          onboarding.token.header
+        </h2>
+      </div>
+      <div
+        className="boxed-group-inner"
+      >
+        <div
+          className="big-spacer-bottom width-50"
+        >
+          onboarding.token.text
+        </div>
+        <form
+          onSubmit={[Function]}
+        >
+          <input
+            autoFocus={true}
+            className="input-large spacer-right"
+            onChange={[Function]}
+            placeholder="onboarding.token.placeholder"
+            required={true}
+            type="text"
+            value="my token"
+          />
+          <i
+            className="spinner"
+          />
+        </form>
+      </div>
+    </div>
+  </Step>
+</TokenStep>
+`;
+
+exports[`generates token 3`] = `
+<TokenStep
+  onContinue={[Function]}
+  open={true}
+  stepNumber={1}
+>
+  <Step
+    open={true}
+    renderForm={[Function]}
+    renderResult={[Function]}
+    stepNumber={1}
+    stepTitle="onboarding.token.header"
+  >
+    <div
+      className="boxed-group onboarding-step onboarding-step-open"
+    >
+      <div
+        className="onboarding-step-number"
+      >
+        1
+      </div>
+      <div
+        className="boxed-group-header"
+      >
+        <h2>
+          onboarding.token.header
+        </h2>
+      </div>
+      <div
+        className="boxed-group-inner"
+      >
+        <div
+          className="big-spacer-bottom width-50"
+        >
+          onboarding.token.text
+        </div>
+        <form
+          onSubmit={[Function]}
+        >
+          my token
+          : 
+          <span
+            className="monospaced spacer-right"
+          >
+            abcd1234
+          </span>
+          <button
+            className="button-clean"
+            onClick={[Function]}
+          >
+            <i
+              className="icon-delete"
+            />
+          </button>
+        </form>
+        <div
+          className="big-spacer-top"
+        >
+          <button
+            className="js-continue"
+            onClick={[Function]}
+          >
+            continue
+          </button>
+        </div>
+      </div>
+    </div>
+  </Step>
+</TokenStep>
+`;
+
+exports[`revokes token 1`] = `
+<TokenStep
+  onContinue={[Function]}
+  open={true}
+  stepNumber={1}
+>
+  <Step
+    open={true}
+    renderForm={[Function]}
+    renderResult={[Function]}
+    stepNumber={1}
+    stepTitle="onboarding.token.header"
+  >
+    <div
+      className="boxed-group onboarding-step onboarding-step-open"
+    >
+      <div
+        className="onboarding-step-number"
+      >
+        1
+      </div>
+      <div
+        className="boxed-group-header"
+      >
+        <h2>
+          onboarding.token.header
+        </h2>
+      </div>
+      <div
+        className="boxed-group-inner"
+      >
+        <div
+          className="big-spacer-bottom width-50"
+        >
+          onboarding.token.text
+        </div>
+        <form
+          onSubmit={[Function]}
+        >
+          my token
+          : 
+          <span
+            className="monospaced spacer-right"
+          >
+            abcd1234
+          </span>
+          <button
+            className="button-clean"
+            onClick={[Function]}
+          >
+            <i
+              className="icon-delete"
+            />
+          </button>
+        </form>
+        <div
+          className="big-spacer-top"
+        >
+          <button
+            className="js-continue"
+            onClick={[Function]}
+          >
+            continue
+          </button>
+        </div>
+      </div>
+    </div>
+  </Step>
+</TokenStep>
+`;
+
+exports[`revokes token 2`] = `
+<TokenStep
+  onContinue={[Function]}
+  open={true}
+  stepNumber={1}
+>
+  <Step
+    open={true}
+    renderForm={[Function]}
+    renderResult={[Function]}
+    stepNumber={1}
+    stepTitle="onboarding.token.header"
+  >
+    <div
+      className="boxed-group onboarding-step onboarding-step-open"
+    >
+      <div
+        className="onboarding-step-number"
+      >
+        1
+      </div>
+      <div
+        className="boxed-group-header"
+      >
+        <h2>
+          onboarding.token.header
+        </h2>
+      </div>
+      <div
+        className="boxed-group-inner"
+      >
+        <div
+          className="big-spacer-bottom width-50"
+        >
+          onboarding.token.text
+        </div>
+        <form
+          onSubmit={[Function]}
+        >
+          my token
+          : 
+          <span
+            className="monospaced spacer-right"
+          >
+            abcd1234
+          </span>
+          <i
+            className="spinner"
+          />
+        </form>
+        <div
+          className="big-spacer-top"
+        >
+          <button
+            className="js-continue"
+            onClick={[Function]}
+          >
+            continue
+          </button>
+        </div>
+      </div>
+    </div>
+  </Step>
+</TokenStep>
+`;
+
+exports[`revokes token 3`] = `
+<TokenStep
+  onContinue={[Function]}
+  open={true}
+  stepNumber={1}
+>
+  <Step
+    open={true}
+    renderForm={[Function]}
+    renderResult={[Function]}
+    stepNumber={1}
+    stepTitle="onboarding.token.header"
+  >
+    <div
+      className="boxed-group onboarding-step onboarding-step-open"
+    >
+      <div
+        className="onboarding-step-number"
+      >
+        1
+      </div>
+      <div
+        className="boxed-group-header"
+      >
+        <h2>
+          onboarding.token.header
+        </h2>
+      </div>
+      <div
+        className="boxed-group-inner"
+      >
+        <div
+          className="big-spacer-bottom width-50"
+        >
+          onboarding.token.text
+        </div>
+        <form
+          onSubmit={[Function]}
+        >
+          <input
+            autoFocus={true}
+            className="input-large spacer-right"
+            onChange={[Function]}
+            placeholder="onboarding.token.placeholder"
+            required={true}
+            type="text"
+            value=""
+          />
+          <button>
+            onboarding.token.generate
+          </button>
+        </form>
+      </div>
+    </div>
+  </Step>
+</TokenStep>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/BuildWrapper.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/BuildWrapper.js
new file mode 100644 (file)
index 0000000..3b0624e
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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 { translate } from '../../../../helpers/l10n';
+
+type Props = {
+  className?: string,
+  os: string
+};
+
+const filenames = {
+  linux: 'build-wrapper-win-x86.zip',
+  win: 'build-wrapper-linux-x86.zip',
+  mac: 'build-wrapper-macosx-x86.zip'
+};
+
+export default function BuildWrapper(props: Props) {
+  return (
+    <div className={props.className}>
+      <h4 className="spacer-bottom">
+        {translate('onboarding.analysis.build_wrapper.header', props.os)}
+      </h4>
+      <p
+        className="spacer-bottom markdown"
+        dangerouslySetInnerHTML={{
+          __html: translate('onboarding.analysis.build_wrapper.text', props.os)
+        }}
+      />
+      <p>
+        <a
+          className="button"
+          download={filenames[props.os]}
+          href={window.baseUrl + '/static/cpp/' + filenames[props.os]}
+          target="_blank">
+          {translate('download_verb')}
+        </a>
+      </p>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/ClangGCC.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/ClangGCC.js
new file mode 100644 (file)
index 0000000..7940263
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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 Command from './Command';
+import SQScanner from './SQScanner';
+import BuildWrapper from './BuildWrapper';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {
+  host: string,
+  os: string,
+  organization?: string,
+  projectKey: string,
+  token: string
+};
+
+const executables = {
+  linux: 'build-wrapper-linux-x86-64',
+  win: 'build-wrapper-win-x86-64.exe',
+  mac: 'build-wrapper-macosx-x86'
+};
+
+export default function ClangGCC(props: Props) {
+  const command1 = `${executables[props.os]} --out-dir bw-output make clean all`;
+
+  const command2 = [
+    props.os === 'win' ? 'sonar-scanner.bat' : 'sonar-scanner',
+    `-Dsonar.projectKey=${props.projectKey}`,
+    props.organization && `-Dsonar.organization=${props.organization}`,
+    '-Dsonar.sources=.',
+    '-Dsonar.cfamily.build-wrapper-output=bw-output',
+    `-Dsonar.host.url=${props.host}`,
+    `-Dsonar.login=${props.token}`
+  ];
+
+  return (
+    <div>
+      <SQScanner os={props.os} />
+      <BuildWrapper className="huge-spacer-top" os={props.os} />
+
+      <h4 className="huge-spacer-top spacer-bottom">
+        {translate('onboarding.analysis.sq_scanner.execute')}
+      </h4>
+      <p
+        className="spacer-bottom markdown"
+        dangerouslySetInnerHTML={{
+          __html: translate('onboarding.analysis.sq_scanner.execute.text')
+        }}
+      />
+      <Command command={command1} />
+      <Command command={command2} />
+      <p
+        className="big-spacer-top markdown"
+        dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.sq_scanner.docs') }}
+      />
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Command.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Command.js
new file mode 100644 (file)
index 0000000..ec9f980
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * 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 Clipboard from 'clipboard';
+import Tooltip from '../../../../components/controls/Tooltip';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {
+  command: string | Array<?string>
+};
+
+type State = {
+  tooltipShown: boolean
+};
+
+const s = ' \\' + '\n  ';
+
+export default class Command extends React.PureComponent {
+  clipboard: Object;
+  copyButton: HTMLButtonElement;
+  mounted: boolean;
+  props: Props;
+  state: State = { tooltipShown: false };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.clipboard = new Clipboard(this.copyButton);
+    this.clipboard.on('success', this.showTooltip);
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    this.clipboard.destroy();
+  }
+
+  showTooltip = () => {
+    if (this.mounted) {
+      this.setState({ tooltipShown: true });
+      setTimeout(this.hideTooltip, 1000);
+    }
+  };
+
+  hideTooltip = () => {
+    if (this.mounted) {
+      this.setState({ tooltipShown: false });
+    }
+  };
+
+  render() {
+    const { command } = this.props;
+    const commandArray = Array.isArray(command) ? command.filter(line => line != null) : [command];
+    const finalCommand = commandArray.join(s);
+
+    const button = (
+      <button data-clipboard-text={finalCommand} ref={node => (this.copyButton = node)}>
+        {translate('copy')}
+      </button>
+    );
+
+    return (
+      <div className="onboarding-command">
+        <pre>{finalCommand}</pre>
+        {this.state.tooltipShown
+          ? <Tooltip defaultVisible={true} placement="top" overlay="Copied!" trigger="manual">
+              {button}
+            </Tooltip>
+          : button}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/DotNet.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/DotNet.js
new file mode 100644 (file)
index 0000000..8885458
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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 Command from './Command';
+import MSBuildScanner from './MSBuildScanner';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {|
+  host: string,
+  organization?: string,
+  projectKey: string,
+  token: string
+|};
+
+export default function DotNet(props: Props) {
+  const command1 = [
+    'SonarQube.Scanner.MSBuild.exe begin',
+    `/k:"${props.projectKey}"`,
+    props.organization && `/d:"sonar.organization=${props.organization}"`,
+    `/d:"sonar.host.url=${props.host}`,
+    `/d:"sonar.login=${props.token}"`
+  ];
+
+  const command2 = 'MsBuild.exe /t:Rebuild';
+
+  const command3 = ['SonarQube.Scanner.MSBuild.exe end', `/d:"sonar.login=${props.token}"`];
+
+  return (
+    <div>
+      <MSBuildScanner />
+
+      <h4 className="huge-spacer-top spacer-bottom">
+        {translate('onboarding.analysis.msbuild.execute')}
+      </h4>
+      <p
+        className="spacer-bottom markdown"
+        dangerouslySetInnerHTML={{
+          __html: translate('onboarding.analysis.msbuild.execute.text')
+        }}
+      />
+      <Command command={command1} />
+      <Command command={command2} />
+      <Command command={command3} />
+      <p
+        className="big-spacer-top markdown"
+        dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.msbuild.docs') }}
+      />
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaGradle.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaGradle.js
new file mode 100644 (file)
index 0000000..3f89a2b
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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 Command from './Command';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {|
+  host: string,
+  organization?: string,
+  token: string
+|};
+
+export default function JavaGradle(props: Props) {
+  const config = 'plugins {\n  id "org.sonarqube" version "2.2"\n}';
+
+  const command = [
+    './gradlew sonarqube',
+    props.organization && `-Dsonar.organization=${props.organization}`,
+    `-Dsonar.host.url=${props.host}`,
+    `-Dsonar.login=${props.token}`
+  ];
+
+  return (
+    <div>
+      <h4 className="spacer-bottom">{translate('onboarding.analysis.java.gradle.header')}</h4>
+      <p
+        className="spacer-bottom markdown"
+        dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.java.gradle.text.1') }}
+      />
+      <Command command={config} />
+      <p className="spacer-top spacer-bottom markdown">
+        {translate('onboarding.analysis.java.gradle.text.2')}
+      </p>
+      <Command command={command} />
+      <p
+        className="big-spacer-top markdown"
+        dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.java.gradle.docs') }}
+      />
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaMaven.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaMaven.js
new file mode 100644 (file)
index 0000000..6317b1b
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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 Command from './Command';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {|
+  host: string,
+  organization?: string,
+  token: string
+|};
+
+export default function JavaMaven(props: Props) {
+  const command = [
+    'mvn sonar:sonar',
+    props.organization && `-Dsonar.organization=${props.organization}`,
+    `-Dsonar.host.url=${props.host}`,
+    `-Dsonar.login=${props.token}`
+  ];
+
+  return (
+    <div>
+      <h4 className="spacer-bottom">{translate('onboarding.analysis.java.maven.header')}</h4>
+      <p className="spacer-bottom markdown">{translate('onboarding.analysis.java.maven.text')}</p>
+      <Command command={command} />
+      <p
+        className="big-spacer-top markdown"
+        dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.java.maven.docs') }}
+      />
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/MSBuildScanner.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/MSBuildScanner.js
new file mode 100644 (file)
index 0000000..3be3839
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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 { translate } from '../../../../helpers/l10n';
+
+type Props = {
+  className?: string
+};
+
+export default function MSBuildScanner(props: Props) {
+  return (
+    <div className={props.className}>
+      <h4 className="spacer-bottom">{translate('onboarding.analysis.msbuild.header')}</h4>
+      <p
+        className="spacer-bottom markdown"
+        dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.msbuild.text') }}
+      />
+      <p>
+        <a
+          className="button"
+          href="http://redirect.sonarsource.com/doc/install-configure-scanner-msbuild.html"
+          target="_blank">
+          {translate('download_verb')}
+        </a>
+      </p>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Msvc.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Msvc.js
new file mode 100644 (file)
index 0000000..00fba90
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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 Command from './Command';
+import MSBuildScanner from './MSBuildScanner';
+import BuildWrapper from './BuildWrapper';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {|
+  host: string,
+  organization?: string,
+  projectKey: string,
+  token: string
+|};
+
+export default function Msvc(props: Props) {
+  const command1 = [
+    'SonarQube.Scanner.MSBuild.exe begin',
+    `/k:"${props.projectKey}"`,
+    props.organization && `/d:"sonar.organization=${props.organization}"`,
+    '/d:"sonar.cfamily.build-wrapper-output=bw-output"',
+    `/d:"sonar.host.url=${props.host}`,
+    `/d:"sonar.login=${props.token}"`
+  ];
+
+  const command2 = 'build-wrapper-win-x86-64.exe --out-dir bw-output MsBuild.exe /t:Rebuild';
+
+  const command3 = ['SonarQube.Scanner.MSBuild.exe end', `/d:"sonar.login=${props.token}"`];
+
+  return (
+    <div>
+      <MSBuildScanner />
+      <BuildWrapper className="huge-spacer-top" os="win" />
+
+      <h4 className="huge-spacer-top spacer-bottom">
+        {translate('onboarding.analysis.msbuild.execute')}
+      </h4>
+      <p
+        className="spacer-bottom markdown"
+        dangerouslySetInnerHTML={{
+          __html: translate('onboarding.analysis.msbuild.execute.text')
+        }}
+      />
+      <Command command={command1} />
+      <Command command={command2} />
+      <Command command={command3} />
+      <p
+        className="big-spacer-top markdown"
+        dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.msbuild.docs') }}
+      />
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Other.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Other.js
new file mode 100644 (file)
index 0000000..534be2f
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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 Command from './Command';
+import SQScanner from './SQScanner';
+import { translate } from '../../../../helpers/l10n';
+
+type Props = {|
+  host: string,
+  organization?: string,
+  os: string,
+  projectKey: string,
+  token: string
+|};
+
+export default function Other(props: Props) {
+  const command = [
+    props.os === 'win' ? 'sonar-scanner.bat' : 'sonar-scanner',
+    `-Dsonar.projectKey=${props.projectKey}`,
+    props.organization && `-Dsonar.organization=${props.organization}`,
+    '-Dsonar.sources=.',
+    `-Dsonar.host.url=${props.host}`,
+    `-Dsonar.login=${props.token}`
+  ];
+
+  return (
+    <div>
+      <SQScanner os={props.os} />
+
+      <h4 className="huge-spacer-top spacer-bottom">
+        {translate('onboarding.analysis.sq_scanner.execute')}
+      </h4>
+      <p
+        className="spacer-bottom markdown"
+        dangerouslySetInnerHTML={{
+          __html: translate('onboarding.analysis.sq_scanner.execute.text')
+        }}
+      />
+      <Command command={command} />
+      <p
+        className="big-spacer-top markdown"
+        dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.sq_scanner.docs') }}
+      />
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/SQScanner.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/SQScanner.js
new file mode 100644 (file)
index 0000000..d36c637
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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 { translate } from '../../../../helpers/l10n';
+
+type Props = {
+  className?: string,
+  os: string
+};
+
+export default function SQScanner(props: Props) {
+  return (
+    <div className={props.className}>
+      <h4 className="spacer-bottom">
+        {translate('onboarding.analysis.sq_scanner.header', props.os)}
+      </h4>
+      <p
+        className="spacer-bottom markdown"
+        dangerouslySetInnerHTML={{
+          __html: translate('onboarding.analysis.sq_scanner.text', props.os)
+        }}
+      />
+      <p>
+        <a
+          className="button"
+          href="http://redirect.sonarsource.com/doc/install-configure-scanner.html"
+          target="_blank">
+          {translate('download_verb')}
+        </a>
+      </p>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/BuildWrapper-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/BuildWrapper-test.js
new file mode 100644 (file)
index 0000000..2135abc
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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 } from 'enzyme';
+import BuildWrapper from '../BuildWrapper';
+
+it('renders correctly', () => {
+  expect(shallow(<BuildWrapper os="win" />)).toMatchSnapshot();
+  expect(shallow(<BuildWrapper os="linux" />)).toMatchSnapshot();
+  expect(shallow(<BuildWrapper os="mac" />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/ClangGCC-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/ClangGCC-test.js
new file mode 100644 (file)
index 0000000..16c4586
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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 } from 'enzyme';
+import ClangGCC from '../ClangGCC';
+
+it('renders correctly', () => {
+  expect(
+    shallow(<ClangGCC host="host" os="win" projectKey="projectKey" token="token" />)
+  ).toMatchSnapshot();
+
+  expect(
+    shallow(<ClangGCC host="host" os="linux" projectKey="projectKey" token="token" />)
+  ).toMatchSnapshot();
+
+  expect(
+    shallow(
+      <ClangGCC
+        host="host"
+        os="linux"
+        organization="organization"
+        projectKey="projectKey"
+        token="token"
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Command-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Command-test.js
new file mode 100644 (file)
index 0000000..19ee425
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * 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 } from 'enzyme';
+import Command from '../Command';
+
+it('renders correctly', () => {
+  expect(shallow(<Command command={'foo\nbar'} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/DotNet-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/DotNet-test.js
new file mode 100644 (file)
index 0000000..f1e320e
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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 } from 'enzyme';
+import DotNet from '../DotNet';
+
+it('renders correctly', () => {
+  expect(shallow(<DotNet host="host" projectKey="projectKey" token="token" />)).toMatchSnapshot();
+  expect(
+    shallow(
+      <DotNet host="host" organization="organization" projectKey="projectKey" token="token" />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaGradle-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaGradle-test.js
new file mode 100644 (file)
index 0000000..ef326dd
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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 } from 'enzyme';
+import JavaGradle from '../JavaGradle';
+
+it('renders correctly', () => {
+  expect(shallow(<JavaGradle host="host" token="token" />)).toMatchSnapshot();
+  expect(
+    shallow(<JavaGradle host="host" organization="organization" token="token" />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaMaven-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaMaven-test.js
new file mode 100644 (file)
index 0000000..bb24f89
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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 } from 'enzyme';
+import JavaMaven from '../JavaMaven';
+
+it('renders correctly', () => {
+  expect(shallow(<JavaMaven host="host" token="token" />)).toMatchSnapshot();
+  expect(
+    shallow(<JavaMaven host="host" organization="organization" token="token" />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/MSBuildScanner-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/MSBuildScanner-test.js
new file mode 100644 (file)
index 0000000..e2c22bc
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * 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 } from 'enzyme';
+import MSBuildScanner from '../MSBuildScanner';
+
+it('renders correctly', () => {
+  expect(shallow(<MSBuildScanner />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Msvc-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Msvc-test.js
new file mode 100644 (file)
index 0000000..28bfa94
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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 } from 'enzyme';
+import Msvc from '../Msvc';
+
+it('renders correctly', () => {
+  expect(shallow(<Msvc host="host" projectKey="projectKey" token="token" />)).toMatchSnapshot();
+  expect(
+    shallow(<Msvc host="host" organization="organization" projectKey="projectKey" token="token" />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Other-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Other-test.js
new file mode 100644 (file)
index 0000000..0eda78f
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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 } from 'enzyme';
+import Other from '../Other';
+
+it('renders correctly', () => {
+  expect(
+    shallow(<Other host="host" os="win" projectKey="projectKey" token="token" />)
+  ).toMatchSnapshot();
+
+  expect(
+    shallow(<Other host="host" os="linux" projectKey="projectKey" token="token" />)
+  ).toMatchSnapshot();
+
+  expect(
+    shallow(
+      <Other
+        host="host"
+        os="linux"
+        organization="organization"
+        projectKey="projectKey"
+        token="token"
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/SQScanner-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/SQScanner-test.js
new file mode 100644 (file)
index 0000000..3b28813
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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 } from 'enzyme';
+import SQScanner from '../SQScanner';
+
+it('renders correctly', () => {
+  expect(shallow(<SQScanner os="win" />)).toMatchSnapshot();
+  expect(shallow(<SQScanner os="linux" />)).toMatchSnapshot();
+  expect(shallow(<SQScanner os="mac" />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/BuildWrapper-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/BuildWrapper-test.js.snap
new file mode 100644 (file)
index 0000000..4a1e0c3
--- /dev/null
@@ -0,0 +1,85 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.build_wrapper.header.win
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.build_wrapper.text.win",
+      }
+    }
+  />
+  <p>
+    <a
+      className="button"
+      download="build-wrapper-linux-x86.zip"
+      href="/static/cpp/build-wrapper-linux-x86.zip"
+      target="_blank"
+    >
+      download_verb
+    </a>
+  </p>
+</div>
+`;
+
+exports[`renders correctly 2`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.build_wrapper.header.linux
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.build_wrapper.text.linux",
+      }
+    }
+  />
+  <p>
+    <a
+      className="button"
+      download="build-wrapper-win-x86.zip"
+      href="/static/cpp/build-wrapper-win-x86.zip"
+      target="_blank"
+    >
+      download_verb
+    </a>
+  </p>
+</div>
+`;
+
+exports[`renders correctly 3`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.build_wrapper.header.mac
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.build_wrapper.text.mac",
+      }
+    }
+  />
+  <p>
+    <a
+      className="button"
+      download="build-wrapper-macosx-x86.zip"
+      href="/static/cpp/build-wrapper-macosx-x86.zip"
+      target="_blank"
+    >
+      download_verb
+    </a>
+  </p>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/ClangGCC-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/ClangGCC-test.js.snap
new file mode 100644 (file)
index 0000000..1ec053c
--- /dev/null
@@ -0,0 +1,148 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<div>
+  <SQScanner
+    os="win"
+  />
+  <BuildWrapper
+    className="huge-spacer-top"
+    os="win"
+  />
+  <h4
+    className="huge-spacer-top spacer-bottom"
+  >
+    onboarding.analysis.sq_scanner.execute
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.execute.text",
+      }
+    }
+  />
+  <Command
+    command="build-wrapper-win-x86-64.exe --out-dir bw-output make clean all"
+  />
+  <Command
+    command={
+      Array [
+        "sonar-scanner.bat",
+        "-Dsonar.projectKey=projectKey",
+        undefined,
+        "-Dsonar.sources=.",
+        "-Dsonar.cfamily.build-wrapper-output=bw-output",
+        "-Dsonar.host.url=host",
+        "-Dsonar.login=token",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.docs",
+      }
+    }
+  />
+</div>
+`;
+
+exports[`renders correctly 2`] = `
+<div>
+  <SQScanner
+    os="linux"
+  />
+  <BuildWrapper
+    className="huge-spacer-top"
+    os="linux"
+  />
+  <h4
+    className="huge-spacer-top spacer-bottom"
+  >
+    onboarding.analysis.sq_scanner.execute
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.execute.text",
+      }
+    }
+  />
+  <Command
+    command="build-wrapper-linux-x86-64 --out-dir bw-output make clean all"
+  />
+  <Command
+    command={
+      Array [
+        "sonar-scanner",
+        "-Dsonar.projectKey=projectKey",
+        undefined,
+        "-Dsonar.sources=.",
+        "-Dsonar.cfamily.build-wrapper-output=bw-output",
+        "-Dsonar.host.url=host",
+        "-Dsonar.login=token",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.docs",
+      }
+    }
+  />
+</div>
+`;
+
+exports[`renders correctly 3`] = `
+<div>
+  <SQScanner
+    os="linux"
+  />
+  <BuildWrapper
+    className="huge-spacer-top"
+    os="linux"
+  />
+  <h4
+    className="huge-spacer-top spacer-bottom"
+  >
+    onboarding.analysis.sq_scanner.execute
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.execute.text",
+      }
+    }
+  />
+  <Command
+    command="build-wrapper-linux-x86-64 --out-dir bw-output make clean all"
+  />
+  <Command
+    command={
+      Array [
+        "sonar-scanner",
+        "-Dsonar.projectKey=projectKey",
+        "-Dsonar.organization=organization",
+        "-Dsonar.sources=.",
+        "-Dsonar.cfamily.build-wrapper-output=bw-output",
+        "-Dsonar.host.url=host",
+        "-Dsonar.login=token",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.docs",
+      }
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Command-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Command-test.js.snap
new file mode 100644 (file)
index 0000000..9ce1c88
--- /dev/null
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<div
+  className="onboarding-command"
+>
+  <pre>
+    foo
+    bar
+  </pre>
+  <button
+    data-clipboard-text="foo
+  bar"
+  >
+    copy
+  </button>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/DotNet-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/DotNet-test.js.snap
new file mode 100644 (file)
index 0000000..3672a2f
--- /dev/null
@@ -0,0 +1,99 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<div>
+  <MSBuildScanner />
+  <h4
+    className="huge-spacer-top spacer-bottom"
+  >
+    onboarding.analysis.msbuild.execute
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.msbuild.execute.text",
+      }
+    }
+  />
+  <Command
+    command={
+      Array [
+        "SonarQube.Scanner.MSBuild.exe begin",
+        "/k:\\"projectKey\\"",
+        undefined,
+        "/d:\\"sonar.host.url=host",
+        "/d:\\"sonar.login=token\\"",
+      ]
+    }
+  />
+  <Command
+    command="MsBuild.exe /t:Rebuild"
+  />
+  <Command
+    command={
+      Array [
+        "SonarQube.Scanner.MSBuild.exe end",
+        "/d:\\"sonar.login=token\\"",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.msbuild.docs",
+      }
+    }
+  />
+</div>
+`;
+
+exports[`renders correctly 2`] = `
+<div>
+  <MSBuildScanner />
+  <h4
+    className="huge-spacer-top spacer-bottom"
+  >
+    onboarding.analysis.msbuild.execute
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.msbuild.execute.text",
+      }
+    }
+  />
+  <Command
+    command={
+      Array [
+        "SonarQube.Scanner.MSBuild.exe begin",
+        "/k:\\"projectKey\\"",
+        "/d:\\"sonar.organization=organization\\"",
+        "/d:\\"sonar.host.url=host",
+        "/d:\\"sonar.login=token\\"",
+      ]
+    }
+  />
+  <Command
+    command="MsBuild.exe /t:Rebuild"
+  />
+  <Command
+    command={
+      Array [
+        "SonarQube.Scanner.MSBuild.exe end",
+        "/d:\\"sonar.login=token\\"",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.msbuild.docs",
+      }
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaGradle-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaGradle-test.js.snap
new file mode 100644 (file)
index 0000000..df6bf9d
--- /dev/null
@@ -0,0 +1,93 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.java.gradle.header
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.java.gradle.text.1",
+      }
+    }
+  />
+  <Command
+    command="plugins {
+    id \\"org.sonarqube\\" version \\"2.2\\"
+  }"
+  />
+  <p
+    className="spacer-top spacer-bottom markdown"
+  >
+    onboarding.analysis.java.gradle.text.2
+  </p>
+  <Command
+    command={
+      Array [
+        "./gradlew sonarqube",
+        undefined,
+        "-Dsonar.host.url=host",
+        "-Dsonar.login=token",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.java.gradle.docs",
+      }
+    }
+  />
+</div>
+`;
+
+exports[`renders correctly 2`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.java.gradle.header
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.java.gradle.text.1",
+      }
+    }
+  />
+  <Command
+    command="plugins {
+    id \\"org.sonarqube\\" version \\"2.2\\"
+  }"
+  />
+  <p
+    className="spacer-top spacer-bottom markdown"
+  >
+    onboarding.analysis.java.gradle.text.2
+  </p>
+  <Command
+    command={
+      Array [
+        "./gradlew sonarqube",
+        "-Dsonar.organization=organization",
+        "-Dsonar.host.url=host",
+        "-Dsonar.login=token",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.java.gradle.docs",
+      }
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaMaven-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaMaven-test.js.snap
new file mode 100644 (file)
index 0000000..3cd4796
--- /dev/null
@@ -0,0 +1,67 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.java.maven.header
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+  >
+    onboarding.analysis.java.maven.text
+  </p>
+  <Command
+    command={
+      Array [
+        "mvn sonar:sonar",
+        undefined,
+        "-Dsonar.host.url=host",
+        "-Dsonar.login=token",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.java.maven.docs",
+      }
+    }
+  />
+</div>
+`;
+
+exports[`renders correctly 2`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.java.maven.header
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+  >
+    onboarding.analysis.java.maven.text
+  </p>
+  <Command
+    command={
+      Array [
+        "mvn sonar:sonar",
+        "-Dsonar.organization=organization",
+        "-Dsonar.host.url=host",
+        "-Dsonar.login=token",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.java.maven.docs",
+      }
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.js.snap
new file mode 100644 (file)
index 0000000..c7f156b
--- /dev/null
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.msbuild.header
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.msbuild.text",
+      }
+    }
+  />
+  <p>
+    <a
+      className="button"
+      href="http://redirect.sonarsource.com/doc/install-configure-scanner-msbuild.html"
+      target="_blank"
+    >
+      download_verb
+    </a>
+  </p>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Msvc-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Msvc-test.js.snap
new file mode 100644 (file)
index 0000000..c2f54b4
--- /dev/null
@@ -0,0 +1,109 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<div>
+  <MSBuildScanner />
+  <BuildWrapper
+    className="huge-spacer-top"
+    os="win"
+  />
+  <h4
+    className="huge-spacer-top spacer-bottom"
+  >
+    onboarding.analysis.msbuild.execute
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.msbuild.execute.text",
+      }
+    }
+  />
+  <Command
+    command={
+      Array [
+        "SonarQube.Scanner.MSBuild.exe begin",
+        "/k:\\"projectKey\\"",
+        undefined,
+        "/d:\\"sonar.cfamily.build-wrapper-output=bw-output\\"",
+        "/d:\\"sonar.host.url=host",
+        "/d:\\"sonar.login=token\\"",
+      ]
+    }
+  />
+  <Command
+    command="build-wrapper-win-x86-64.exe --out-dir bw-output MsBuild.exe /t:Rebuild"
+  />
+  <Command
+    command={
+      Array [
+        "SonarQube.Scanner.MSBuild.exe end",
+        "/d:\\"sonar.login=token\\"",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.msbuild.docs",
+      }
+    }
+  />
+</div>
+`;
+
+exports[`renders correctly 2`] = `
+<div>
+  <MSBuildScanner />
+  <BuildWrapper
+    className="huge-spacer-top"
+    os="win"
+  />
+  <h4
+    className="huge-spacer-top spacer-bottom"
+  >
+    onboarding.analysis.msbuild.execute
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.msbuild.execute.text",
+      }
+    }
+  />
+  <Command
+    command={
+      Array [
+        "SonarQube.Scanner.MSBuild.exe begin",
+        "/k:\\"projectKey\\"",
+        "/d:\\"sonar.organization=organization\\"",
+        "/d:\\"sonar.cfamily.build-wrapper-output=bw-output\\"",
+        "/d:\\"sonar.host.url=host",
+        "/d:\\"sonar.login=token\\"",
+      ]
+    }
+  />
+  <Command
+    command="build-wrapper-win-x86-64.exe --out-dir bw-output MsBuild.exe /t:Rebuild"
+  />
+  <Command
+    command={
+      Array [
+        "SonarQube.Scanner.MSBuild.exe end",
+        "/d:\\"sonar.login=token\\"",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.msbuild.docs",
+      }
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Other-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Other-test.js.snap
new file mode 100644 (file)
index 0000000..699ff84
--- /dev/null
@@ -0,0 +1,124 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<div>
+  <SQScanner
+    os="win"
+  />
+  <h4
+    className="huge-spacer-top spacer-bottom"
+  >
+    onboarding.analysis.sq_scanner.execute
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.execute.text",
+      }
+    }
+  />
+  <Command
+    command={
+      Array [
+        "sonar-scanner.bat",
+        "-Dsonar.projectKey=projectKey",
+        undefined,
+        "-Dsonar.sources=.",
+        "-Dsonar.host.url=host",
+        "-Dsonar.login=token",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.docs",
+      }
+    }
+  />
+</div>
+`;
+
+exports[`renders correctly 2`] = `
+<div>
+  <SQScanner
+    os="linux"
+  />
+  <h4
+    className="huge-spacer-top spacer-bottom"
+  >
+    onboarding.analysis.sq_scanner.execute
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.execute.text",
+      }
+    }
+  />
+  <Command
+    command={
+      Array [
+        "sonar-scanner",
+        "-Dsonar.projectKey=projectKey",
+        undefined,
+        "-Dsonar.sources=.",
+        "-Dsonar.host.url=host",
+        "-Dsonar.login=token",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.docs",
+      }
+    }
+  />
+</div>
+`;
+
+exports[`renders correctly 3`] = `
+<div>
+  <SQScanner
+    os="linux"
+  />
+  <h4
+    className="huge-spacer-top spacer-bottom"
+  >
+    onboarding.analysis.sq_scanner.execute
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.execute.text",
+      }
+    }
+  />
+  <Command
+    command={
+      Array [
+        "sonar-scanner",
+        "-Dsonar.projectKey=projectKey",
+        "-Dsonar.organization=organization",
+        "-Dsonar.sources=.",
+        "-Dsonar.host.url=host",
+        "-Dsonar.login=token",
+      ]
+    }
+  />
+  <p
+    className="big-spacer-top markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.docs",
+      }
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/SQScanner-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/SQScanner-test.js.snap
new file mode 100644 (file)
index 0000000..bcdac07
--- /dev/null
@@ -0,0 +1,82 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.sq_scanner.header.win
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.text.win",
+      }
+    }
+  />
+  <p>
+    <a
+      className="button"
+      href="http://redirect.sonarsource.com/doc/install-configure-scanner.html"
+      target="_blank"
+    >
+      download_verb
+    </a>
+  </p>
+</div>
+`;
+
+exports[`renders correctly 2`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.sq_scanner.header.linux
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.text.linux",
+      }
+    }
+  />
+  <p>
+    <a
+      className="button"
+      href="http://redirect.sonarsource.com/doc/install-configure-scanner.html"
+      target="_blank"
+    >
+      download_verb
+    </a>
+  </p>
+</div>
+`;
+
+exports[`renders correctly 3`] = `
+<div>
+  <h4
+    className="spacer-bottom"
+  >
+    onboarding.analysis.sq_scanner.header.mac
+  </h4>
+  <p
+    className="spacer-bottom markdown"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "onboarding.analysis.sq_scanner.text.mac",
+      }
+    }
+  />
+  <p>
+    <a
+      className="button"
+      href="http://redirect.sonarsource.com/doc/install-configure-scanner.html"
+      target="_blank"
+    >
+      download_verb
+    </a>
+  </p>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/styles.css b/server/sonar-web/src/main/js/apps/tutorials/onboarding/styles.css
new file mode 100644 (file)
index 0000000..870da20
--- /dev/null
@@ -0,0 +1,59 @@
+.onboarding-step {
+  position: relative;
+  padding-left: 34px;
+}
+
+.onboarding-step .boxed-group-actions {
+  height: 24px;
+  line-height: 24px;
+}
+
+.onboarding-step-number {
+  position: absolute;
+  top: 15px;
+  left: 15px;
+  width: 24px;
+  height: 24px;
+  line-height: 24px;
+  border-radius: 24px;
+  background-color: #cdcdcd;
+  color: #fff;
+  font-size: 14px;
+  text-align: center;
+}
+
+.onboarding-step-open .onboarding-step-number {
+  background-color: #236a97;
+}
+
+.onboarding-command {
+  position: relative;
+  margin: 8px 0;
+}
+
+.onboarding-command pre {
+  padding: 15px;
+  border-radius: 2px;
+  background: #404040;
+  color: #f0f0f0;
+  overflow: auto;
+}
+
+.onboarding-command button {
+  position: absolute;
+  top: 15px;
+  right: 15px;
+  height: 20px;
+  line-height: 18px;
+  border: 1px solid #fff;
+  color: #fff;
+  font-size: 11px;
+  font-weight: normal;
+}
+
+.onboarding-command button:hover,
+.onboarding-command button:focus,
+.onboarding-command button:active {
+  background-color: #fff;
+  color: #404040;
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/routes.js b/server/sonar-web/src/main/js/apps/tutorials/routes.js
new file mode 100644 (file)
index 0000000..3a7111d
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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;
index 01fa9a6636e5b18bc01608090ef55b87d1391086..01889e1d848c982613b2bca04ffb9081806a4c85 100644 (file)
@@ -54,17 +54,13 @@ export default Modal.extend({
     this.errors = [];
     this.newToken = null;
     const tokenName = this.$('.js-generate-token-form input').val();
-    generateToken(this.model.id, tokenName)
-      .then(response => {
+    generateToken(tokenName, this.model.id).then(
+      response => {
         this.newToken = response;
         this.requestTokens();
-      })
-      .catch(error => {
-        error.response.json().then(response => {
-          this.errors = response.errors;
-          this.render();
-        });
-      });
+      },
+      () => {}
+    );
   },
 
   onRevokeTokenFormSubmit(e) {
@@ -73,7 +69,7 @@ export default Modal.extend({
     const token = this.tokens.find(t => t.name === `${tokenName}`);
     if (token) {
       if (token.deleting) {
-        revokeToken(this.model.id, tokenName).then(this.requestTokens.bind(this));
+        revokeToken(tokenName, this.model.id).then(this.requestTokens.bind(this), () => {});
       } else {
         token.deleting = true;
         this.render();
index 4ae1793186654404eb5cdc96a6cbc3dce3b1d8a6..a69169ceeaee3be9fbff133cc3a88d8ab59bcd6f 100644 (file)
@@ -54,3 +54,11 @@ export const elementKeydown = (element, keyCode) => {
     preventDefault() {}
   });
 };
+
+export const doAsync = fn =>
+  new Promise(resolve => {
+    setTimeout(() => {
+      fn();
+      resolve();
+    }, 0);
+  });
index 2920733b4854ac8283f108ab321e56b7219c7f8e..4853c080759804e2f55db1e437d2e2ca13c071f0 100644 (file)
   opacity: 1;
 }
 
+.modal-medium {
+  width: 800px;
+  margin-left: -400px;
+}
+
 .modal-large {
   width: 90vw;
   margin-left: -45vw;
diff --git a/server/sonar-web/src/main/less/components/side-tabs.less b/server/sonar-web/src/main/less/components/side-tabs.less
new file mode 100644 (file)
index 0000000..b7cd0da
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+@import (reference) "../mixins";
+@import (reference) "../variables";
+
+.side-tabs-layout {
+  display: flex;
+  justify-content: space-between;
+  align-items: stretch;
+
+  .modal & {
+    padding-left: 10px;
+    background-color: @barBackgroundColor;
+  }
+}
+
+.side-tabs-main {
+  position: relative;
+  z-index: 2;
+  flex-grow: 1;
+  padding: 15px 20px;
+  border: 1px solid @barBorderColor;
+  box-sizing: border-box;
+  background-color: #fff;
+
+  .modal & {
+    border-top: none;
+    border-bottom: none;
+    border-right: none;
+  }
+}
+
+.side-tabs-side {
+  position: relative;
+  z-index: 3;
+  width: 160px;
+  flex-shrink: 0;
+  padding: 10px 0;
+  box-sizing: border-box;
+  transform: translateX(1px);
+}
+
+.side-tabs-menu > li {
+  margin-bottom: 4px;
+}
+
+.side-tabs-menu > li > a {
+  display: block;
+  padding: 10px 10px;
+  line-height: 1.5;
+  border-top-left-radius: 3px;
+  border-bottom-left-radius: 3px;
+  border: 1px solid @barBorderColor;
+  border-right: none;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  transition: color 0.3s ease, background-color 0.3s ease;
+}
+
+.side-tabs-menu > li > a:hover,
+.side-tabs-menu > li > a:focus,
+.side-tabs-menu > li > a.active {
+  background-color: #fff;
+}
+
+.side-tabs-menu > li > a.active {
+  color: #444;
+  cursor: default;
+}
index 75fe183941352f7ab19f59a39c000ba5707a5741..d26cae210ba201b5725e0d5085d35ce0a1580a7a 100644 (file)
@@ -114,6 +114,7 @@ small,
 .text-top     { vertical-align: top; }
 .text-middle  { vertical-align: middle; }
 .text-bottom  { vertical-align: bottom; }
+.text-text-top { vertical-align: text-top !important; }
 
 
 // Overflow
index 876ad49a4e2717bedc6ce7d1ee0809c92187b240..29e5875e709bee51d1156d6c896fe717fa6bd5d0 100644 (file)
@@ -58,6 +58,7 @@
 @import "components/search";
 @import "components/pills";
 @import "components/react-select";
+@import "components/side-tabs";
 
 @import "pages/coding-rules";
 @import "pages/maintenance";
index 8ebbb162e05477dcde7962350297324945bd70ee..b64d272115811240dda41575873a050b6d13c4e1 100644 (file)
@@ -47,6 +47,7 @@ component=Component
 configurable=Configurable
 configure=Configure
 confirm=Confirm
+continue=Continue
 copy=Copy
 create=Create
 created=Created
@@ -301,6 +302,7 @@ since_previous_version.short=\u0394 version
 since_previous_version_detailed=since previous version ({0} - {1})
 since_previous_version_with_only_date=since previous version ({0})
 since_previous_version_detailed.short=\u0394 version ({0})
+this_name_is_already_taken=This name is already taken.
 time_changes=Time changes
 work_duration.x_days={0}d
 work_duration.x_hours={0}h
@@ -1054,10 +1056,12 @@ search.placeholder=Search for projects, sub-projects and files...
 
 #------------------------------------------------------------------------------
 #
-# SHORTCUTS
+# GLOBAL HELP
 #
 #------------------------------------------------------------------------------
-shortcuts.modal_title=Shortcuts
+help.section.links=Links
+help.section.shortcuts=Shortcuts
+help.section.tutorials=Tutorials
 
 shortcuts.section.global=Global
 shortcuts.section.global.search=quickly open search bar
@@ -2937,3 +2941,79 @@ footer.terms=Terms
 footer.twitter=Twitter
 footer.version_x=Version {0}
 footer.web_api=Web API
+
+
+#------------------------------------------------------------------------------
+#
+# ONBOARDING
+#
+#------------------------------------------------------------------------------
+onboarding.header=Welcome to SonarQube!
+onboarding.header.sonarcloud=Welcome to SonarCloud!
+onboarding.header.description=Let's learn how to analyze your first public project.
+
+onboarding.token.header=Generate a token
+onboarding.token.text=We'll use it as a replacement of the user login. This will increase the security of your installation by not letting your analysis user's password going through your network.
+onboarding.token.generate=Generate
+onboarding.token.placeholder=Enter a name for your token
+
+onboarding.organization.header=Choose an organization for your project
+onboarding.organization.text=Organizations are where your projects belong. You can add your team members to your organization later to allow them to contribute to your projects.
+onboarding.organization.placeholder=Enter a name for your organization
+onboarding.organization.my_personal_organization=My personal organization
+onboarding.organization.exising_organization=Existing organization
+onboarding.organization.create_another_organization=Create another organization
+onboarding.organization.key_requirement=2 to 32 characters. All chars must be lower-case letters (a to z), digits or dash (but dash can neither be trailing nor heading)
+
+onboarding.project_key_requirement=Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit. 400 characters max.
+
+onboarding.analysis.header=Run analysis on your project
+
+onboarding.language=What is your project's main language?
+onboarding.language.java=Java
+onboarding.language.java.build_technology=You are developing primarily in Java: what is your build technology?
+onboarding.language.java.build_technology.maven=Maven
+onboarding.language.java.build_technology.gradle=Gradle
+onboarding.language.dotnet=C# or VB.NET
+onboarding.language.c-family=C, C++, Objective-C
+onboarding.language.c-family.compiler=Which compiler are you using?
+onboarding.language.c-family.compiler.msvc=Microsoft Visual C++
+onboarding.language.c-family.compiler.clang-gcc=CLang or GGC
+onboarding.language.other=Other (JS, Python, PHP, ...)
+onboarding.language.os=What is your OS?
+onboarding.language.os.linux=Linux
+onboarding.language.os.win=Windows
+onboarding.language.os.mac=macOS
+onboarding.language.project_key=Define a unique project key
+
+onboarding.analysis.java.maven.header=Execute the SonarQube Scanner for Maven from your computer
+onboarding.analysis.java.maven.text=Running a SonarQube analysis with Maven is straighforward. You just need to run the following command in your project's folder.
+onboarding.analysis.java.maven.docs=Please visit the <a href="http://redirect.sonarsource.com/doc/install-configure-scanner-maven.html" target="_blank">official documentation of the SonarQube Scanner for Maven</a> for more details.
+
+onboarding.analysis.java.gradle.header=Execute the SonarQube Scanner for Gradle from your computer
+onboarding.analysis.java.gradle.text.1=Running a SonarQube analysis with Gradle is straighforward. You just need to declare the <code>org.sonarqube</code> plugin in your <code>build.gradle</code> file:
+onboarding.analysis.java.gradle.text.2=and run the following command:
+onboarding.analysis.java.gradle.docs=Please visit the <a href="http://redirect.sonarsource.com/doc/gradle.html" target="_blank">official documentation of the SonarQube Scanner for Gradle</a> for more details.
+
+onboarding.analysis.msbuild.header=Download and unzip the SonarQube Scanner for MSBuild
+onboarding.analysis.msbuild.text=And add the executable's directory to the <code>%PATH%</code> environment variable
+onboarding.analysis.msbuild.execute=Execute the SonarQube Scanner for MSBuild from your computer
+onboarding.analysis.msbuild.execute.text=Running a SonarQube analysis is straighforward. You just need to execute the following commands at the root of your solution.
+onboarding.analysis.msbuild.docs=Please visit the <a href="http://redirect.sonarsource.com/doc/install-configure-scanner-msbuild.html" target="_blank">official documentation of the SonarQube Scanner for MSBuild</a> for more details.
+
+onboarding.analysis.build_wrapper.header.linux=Download and unzip the Build Wrapper for Linux
+onboarding.analysis.build_wrapper.header.win=Download and unzip the Build Wrapper for Windows
+onboarding.analysis.build_wrapper.header.mac=Download and unzip the Build Wrapper for macOS
+onboarding.analysis.build_wrapper.text.linux=And add the executable's directory to the <code>PATH</code> environment variable
+onboarding.analysis.build_wrapper.text.win=And add the executable's directory to the <code>%PATH%</code> environment variable
+onboarding.analysis.build_wrapper.text.mac=And add the executable's directory to the <code>PATH</code> environment variable
+
+onboarding.analysis.sq_scanner.header.linux=Download and unzip the SonarQube Scanner for Linux
+onboarding.analysis.sq_scanner.header.win=Download and unzip the SonarQube Scanner for Windows
+onboarding.analysis.sq_scanner.header.mac=Download and unzip the SonarQube Scanner for macOS
+onboarding.analysis.sq_scanner.text.linux=And add the <code>bin</code> directory to the <code>PATH</code> environment variable
+onboarding.analysis.sq_scanner.text.win=And add the <code>bin</code> directory to the <code>%PATH%</code> environment variable
+onboarding.analysis.sq_scanner.text.mac=And add the <code>bin</code> directory to the <code>PATH</code> environment variable
+onboarding.analysis.sq_scanner.execute=Execute the SonarQube Scanner from your computer
+onboarding.analysis.sq_scanner.execute.text=Running a SonarQube analysis is straighforward. You just need to execute the following commands in your project's folder.
+onboarding.analysis.sq_scanner.docs=Please visit the <a href="http://redirect.sonarsource.com/doc/install-configure-scanner.html" target="_blank">official documentation of the SonarQube Scanner</a> for more details.