aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/components.js3
-rw-r--r--server/sonar-web/src/main/js/api/organizations.js10
-rw-r--r--server/sonar-web/src/main/js/api/user-tokens.js25
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalFooterForSonarQubeDotCom.js2
-rw-r--r--server/sonar-web/src/main/js/app/components/help/GlobalHelp.js118
-rw-r--r--server/sonar-web/src/main/js/app/components/help/LinksHelp.js50
-rw-r--r--server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js45
-rw-r--r--server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js135
-rw-r--r--server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js34
-rw-r--r--server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js39
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js20
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelp.js188
-rw-r--r--server/sonar-web/src/main/js/app/styles/boxed-group.css2
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.js2
-rw-r--r--server/sonar-web/src/main/js/app/utils/throwGlobalError.js28
-rw-r--r--server/sonar-web/src/main/js/apps/account/tokens-view.js16
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/App.js6
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/styles.css53
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js181
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/LanguageStep.js188
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/NewOrganizationForm.js157
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/NewProjectForm.js145
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js109
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js39
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js240
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js145
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/Step.js47
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js180
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/LanguageStep-test.js106
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewOrganizationForm-test.js56
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewProjectForm-test.js55
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js63
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js55
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Step-test.js38
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js55
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/LanguageStep-test.js.snap687
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewOrganizationForm-test.js.snap168
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewProjectForm-test.js.snap219
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap219
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Step-test.js.snap48
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap383
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/BuildWrapper.js58
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/ClangGCC.js76
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Command.js89
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/DotNet.js68
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaGradle.js59
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaMaven.js50
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/MSBuildScanner.js46
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Msvc.js71
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Other.js64
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/SQScanner.js51
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/BuildWrapper-test.js29
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/ClangGCC-test.js45
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Command-test.js27
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/DotNet-test.js32
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaGradle-test.js30
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaMaven-test.js30
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/MSBuildScanner-test.js27
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Msvc-test.js30
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Other-test.js45
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/SQScanner-test.js29
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/BuildWrapper-test.js.snap85
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/ClangGCC-test.js.snap148
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Command-test.js.snap18
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/DotNet-test.js.snap99
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaGradle-test.js.snap93
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaMaven-test.js.snap67
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.js.snap28
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Msvc-test.js.snap109
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Other-test.js.snap124
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/SQScanner-test.js.snap82
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/styles.css59
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/routes.js31
-rw-r--r--server/sonar-web/src/main/js/apps/users/tokens-view.js16
-rw-r--r--server/sonar-web/src/main/js/helpers/testUtils.js8
-rw-r--r--server/sonar-web/src/main/less/components/modals.less5
-rw-r--r--server/sonar-web/src/main/less/components/side-tabs.less86
-rw-r--r--server/sonar-web/src/main/less/init/type.less1
-rw-r--r--server/sonar-web/src/main/less/sonar.less1
81 files changed, 6094 insertions, 286 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js
index 867100c4fae..e024ce947f5 100644
--- a/server/sonar-web/src/main/js/api/components.js
+++ b/server/sonar-web/src/main/js/api/components.js
@@ -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 }) {
diff --git a/server/sonar-web/src/main/js/api/organizations.js b/server/sonar-web/src/main/js/api/organizations.js
index 097a17ce5cb..7ca0263ba81 100644
--- a/server/sonar-web/src/main/js/api/organizations.js
+++ b/server/sonar-web/src/main/js/api/organizations.js
@@ -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 }
diff --git a/server/sonar-web/src/main/js/api/user-tokens.js b/server/sonar-web/src/main/js/api/user-tokens.js
index 4b3b97e2bba..3947b07f1be 100644
--- a/server/sonar-web/src/main/js/api/user-tokens.js
+++ b/server/sonar-web/src/main/js/api/user-tokens.js
@@ -17,14 +17,16 @@
* 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);
}
diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooterForSonarQubeDotCom.js b/server/sonar-web/src/main/js/app/components/GlobalFooterForSonarQubeDotCom.js
index 755a34e933d..ab8ec94f97a 100644
--- a/server/sonar-web/src/main/js/app/components/GlobalFooterForSonarQubeDotCom.js
+++ b/server/sonar-web/src/main/js/app/components/GlobalFooterForSonarQubeDotCom.js
@@ -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
index 00000000000..553f5e3cc71
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js
@@ -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
index 00000000000..e76ef2c6bb6
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/help/LinksHelp.js
@@ -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
index 00000000000..6efdac91b50
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js
@@ -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
index 00000000000..1787524f5dc
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js
@@ -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
index 00000000000..112b6393361
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js
@@ -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
index 00000000000..ff0a3dd6bef
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js
@@ -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 } } });
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
index 2e2659d7125..8d432d498a0 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
@@ -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
index 273b370f17a..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelp.js
+++ /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>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/app/styles/boxed-group.css b/server/sonar-web/src/main/js/app/styles/boxed-group.css
index 24c56850676..f41ae1927d6 100644
--- a/server/sonar-web/src/main/js/app/styles/boxed-group.css
+++ b/server/sonar-web/src/main/js/app/styles/boxed-group.css
@@ -34,6 +34,8 @@
}
.boxed-group-actions {
+ position: relative;
+ z-index: 12;
float: right;
margin-top: 15px;
margin-right: 20px;
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js
index 5b4a0d37e15..f0793da6798 100644
--- a/server/sonar-web/src/main/js/app/utils/startReactApp.js
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js
@@ -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
index 00000000000..75cedec4e68
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/utils/throwGlobalError.js
@@ -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();
+}
diff --git a/server/sonar-web/src/main/js/apps/account/tokens-view.js b/server/sonar-web/src/main/js/apps/account/tokens-view.js
index 62c6f8f9143..29ef6f5f9cd 100644
--- a/server/sonar-web/src/main/js/apps/account/tokens-view.js
+++ b/server/sonar-web/src/main/js/apps/account/tokens-view.js
@@ -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();
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js b/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js
index acc503123ed..0266656aa8d 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js
+++ b/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js
@@ -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);
}
}
);
diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js
index d357cff1fbc..9d45c3749e7 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/App.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/App.js
@@ -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 />}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js
index 3ad92ee2b96..676f5511174 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js
@@ -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)}
diff --git a/server/sonar-web/src/main/js/apps/settings/styles.css b/server/sonar-web/src/main/js/apps/settings/styles.css
index a529c5c5c5f..4d188e811dc 100644
--- a/server/sonar-web/src/main/js/apps/settings/styles.css
+++ b/server/sonar-web/src/main/js/apps/settings/styles.css
@@ -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
index 00000000000..78db84e6ef9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js
@@ -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
index 00000000000..44bfb4ed766
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/LanguageStep.js
@@ -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
index 00000000000..16a7c54d526
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewOrganizationForm.js
@@ -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
index 00000000000..2bf64f8fe5a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewProjectForm.js
@@ -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
index 00000000000..0382a33e0d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
@@ -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
index 00000000000..a7eecc2e9e4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js
@@ -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
index 00000000000..325e8e8ac56
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js
@@ -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
index 00000000000..156fc34b470
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js
@@ -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
index 00000000000..763cef635df
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Step.js
@@ -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
index 00000000000..9a171f7bc77
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js
@@ -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
index 00000000000..414b0dba7fe
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/LanguageStep-test.js
@@ -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
index 00000000000..79a5542b3ee
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewOrganizationForm-test.js
@@ -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
index 00000000000..7b05724cbe7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewProjectForm-test.js
@@ -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
index 00000000000..9352556f5d1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js
@@ -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
index 00000000000..2af50f6a3b5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js
@@ -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
index 00000000000..89d7a3b7ba4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Step-test.js
@@ -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
index 00000000000..9498fc27ada
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js
@@ -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
index 00000000000..8e82d384b3f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/LanguageStep-test.js.snap
@@ -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
index 00000000000..c5f05454644
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewOrganizationForm-test.js.snap
@@ -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
index 00000000000..50ef33521cc
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewProjectForm-test.js.snap
@@ -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
index 00000000000..cbfbf1da7ca
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap
@@ -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
index 00000000000..1df068a503c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Step-test.js.snap
@@ -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
index 00000000000..73ae3896df4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap
@@ -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
index 00000000000..3b0624e4629
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/BuildWrapper.js
@@ -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
index 00000000000..7940263931c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/ClangGCC.js
@@ -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
index 00000000000..ec9f980083d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Command.js
@@ -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
index 00000000000..8885458953b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/DotNet.js
@@ -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
index 00000000000..3f89a2bb241
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaGradle.js
@@ -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
index 00000000000..6317b1bac2e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaMaven.js
@@ -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
index 00000000000..3be38395ca9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/MSBuildScanner.js
@@ -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
index 00000000000..00fba90474a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Msvc.js
@@ -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
index 00000000000..534be2f8584
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Other.js
@@ -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
index 00000000000..d36c6379259
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/SQScanner.js
@@ -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
index 00000000000..2135abc7606
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/BuildWrapper-test.js
@@ -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
index 00000000000..16c458660b8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/ClangGCC-test.js
@@ -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
index 00000000000..19ee425c791
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Command-test.js
@@ -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
index 00000000000..f1e320e2b3f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/DotNet-test.js
@@ -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
index 00000000000..ef326ddc8c9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaGradle-test.js
@@ -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
index 00000000000..bb24f89e611
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaMaven-test.js
@@ -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
index 00000000000..e2c22bc157e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/MSBuildScanner-test.js
@@ -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
index 00000000000..28bfa947aa6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Msvc-test.js
@@ -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
index 00000000000..0eda78f2f94
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Other-test.js
@@ -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
index 00000000000..3b2881377ad
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/SQScanner-test.js
@@ -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
index 00000000000..4a1e0c3944c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/BuildWrapper-test.js.snap
@@ -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
index 00000000000..1ec053c4083
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/ClangGCC-test.js.snap
@@ -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
index 00000000000..9ce1c887f5f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Command-test.js.snap
@@ -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
index 00000000000..3672a2f4c8d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/DotNet-test.js.snap
@@ -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
index 00000000000..df6bf9db47f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaGradle-test.js.snap
@@ -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
index 00000000000..3cd4796988b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaMaven-test.js.snap
@@ -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
index 00000000000..c7f156bc53f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.js.snap
@@ -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
index 00000000000..c2f54b40496
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Msvc-test.js.snap
@@ -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
index 00000000000..699ff84d414
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Other-test.js.snap
@@ -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
index 00000000000..bcdac075ce4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/SQScanner-test.js.snap
@@ -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
index 00000000000..870da20acde
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/styles.css
@@ -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
index 00000000000..3a7111d0ae0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/routes.js
@@ -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;
diff --git a/server/sonar-web/src/main/js/apps/users/tokens-view.js b/server/sonar-web/src/main/js/apps/users/tokens-view.js
index 01fa9a6636e..01889e1d848 100644
--- a/server/sonar-web/src/main/js/apps/users/tokens-view.js
+++ b/server/sonar-web/src/main/js/apps/users/tokens-view.js
@@ -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();
diff --git a/server/sonar-web/src/main/js/helpers/testUtils.js b/server/sonar-web/src/main/js/helpers/testUtils.js
index 4ae17931866..a69169ceeae 100644
--- a/server/sonar-web/src/main/js/helpers/testUtils.js
+++ b/server/sonar-web/src/main/js/helpers/testUtils.js
@@ -54,3 +54,11 @@ export const elementKeydown = (element, keyCode) => {
preventDefault() {}
});
};
+
+export const doAsync = fn =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ fn();
+ resolve();
+ }, 0);
+ });
diff --git a/server/sonar-web/src/main/less/components/modals.less b/server/sonar-web/src/main/less/components/modals.less
index 2920733b485..4853c080759 100644
--- a/server/sonar-web/src/main/less/components/modals.less
+++ b/server/sonar-web/src/main/less/components/modals.less
@@ -43,6 +43,11 @@
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
index 00000000000..b7cd0da337c
--- /dev/null
+++ b/server/sonar-web/src/main/less/components/side-tabs.less
@@ -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;
+}
diff --git a/server/sonar-web/src/main/less/init/type.less b/server/sonar-web/src/main/less/init/type.less
index 75fe1839413..d26cae210ba 100644
--- a/server/sonar-web/src/main/less/init/type.less
+++ b/server/sonar-web/src/main/less/init/type.less
@@ -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
diff --git a/server/sonar-web/src/main/less/sonar.less b/server/sonar-web/src/main/less/sonar.less
index 876ad49a4e2..29e5875e709 100644
--- a/server/sonar-web/src/main/less/sonar.less
+++ b/server/sonar-web/src/main/less/sonar.less
@@ -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";