aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/tutorials
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-07-20 16:57:23 +0200
committerSonarTech <sonartech@sonarsource.com>2018-08-10 20:21:28 +0200
commitb08814f7807c1443592af65cd68c2a51dfd4ee37 (patch)
treed7bbaf30c5c0633cd212a30e52db073945ba61ea /server/sonar-web/src/main/js/apps/tutorials
parent3a39b4fa08b15912c928af35fb7b77cd4b85ab64 (diff)
downloadsonarqube-b08814f7807c1443592af65cd68c2a51dfd4ee37.tar.gz
sonarqube-b08814f7807c1443592af65cd68c2a51dfd4ee37.zip
SONAR-11036 Install integration with GitHub or BitBucket Cloud
* SONAR-11040 Update tutorial choices modal * SONAR-11041 Migrate manual installation tab * SONAR-11041 Rename button to start new project tutorial * SONAR-11041 Rework sonarcloud tabbed page styling * SONAR-11042 Add alm app install buttons in create project page * Make start script compatible with ALM integration
Diffstat (limited to 'server/sonar-web/src/main/js/apps/tutorials')
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx82
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap116
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx132
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx182
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx227
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx69
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx58
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx79
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap39
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap97
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap125
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/routes.ts36
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/styles.css40
14 files changed, 1196 insertions, 103 deletions
diff --git a/server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx b/server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx
index e6a6d887bd3..e7edd09e99f 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx
@@ -18,19 +18,22 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as PropTypes from 'prop-types';
import { connect } from 'react-redux';
import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication';
import Modal from '../../components/controls/Modal';
-import { ResetButtonLink, Button } from '../../components/ui/buttons';
+import OnboardingPrivateIcon from '../../components/icons-components/OnboardingPrivateIcon';
+import OnboardingProjectIcon from '../../components/icons-components/OnboardingProjectIcon';
+import OnboardingTeamIcon from '../../components/icons-components/OnboardingTeamIcon';
+import { Button, ResetButtonLink } from '../../components/ui/buttons';
import { translate } from '../../helpers/l10n';
import { CurrentUser, isLoggedIn } from '../../app/types';
import { getCurrentUser } from '../../store/rootReducer';
import './styles.css';
interface OwnProps {
- onFinish: () => void;
+ onClose: (doSkipOnboarding?: boolean) => void;
onOpenOrganizationOnboarding: () => void;
- onOpenProjectOnboarding: () => void;
onOpenTeamOnboarding: () => void;
}
@@ -41,12 +44,25 @@ interface StateProps {
type Props = OwnProps & StateProps;
export class Onboarding extends React.PureComponent<Props> {
+ static contextTypes = {
+ router: PropTypes.object
+ };
+
componentDidMount() {
if (!isLoggedIn(this.props.currentUser)) {
handleRequiredAuthentication();
}
}
+ openProjectOnboarding = () => {
+ this.props.onClose(false);
+ this.context.router.push('/onboarding');
+ };
+
+ onFinish = () => {
+ this.props.onClose(true);
+ };
+
render() {
if (!isLoggedIn(this.props.currentUser)) {
return null;
@@ -57,41 +73,35 @@ export class Onboarding extends React.PureComponent<Props> {
<Modal
contentLabel={header}
medium={true}
- onRequestClose={this.props.onFinish}
+ onRequestClose={this.onFinish}
shouldCloseOnOverlayClick={false}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
- <div className="modal-body">
- <p className="spacer-top big-spacer-bottom">
- {translate('onboarding.header.description')}
- </p>
- <ul className="onboarding-choices">
- <li className="text-center">
- <p className="big-spacer-bottom">{translate('onboarding.analyze_public_code')}</p>
- <Button onClick={this.props.onOpenProjectOnboarding}>
- {translate('onboarding.analyze_public_code.button')}
- </Button>
- </li>
- <li className="text-center">
- <p className="big-spacer-bottom">{translate('onboarding.analyze_private_code')}</p>
- <Button onClick={this.props.onOpenOrganizationOnboarding}>
- {translate('onboarding.analyze_private_code.button')}
- </Button>
- </li>
- <li className="text-center">
- <p className="big-spacer-bottom">
- {translate('onboarding.contribute_existing_project')}
- </p>
- <Button onClick={this.props.onOpenTeamOnboarding}>
- {translate('onboarding.contribute_existing_project.button')}
- </Button>
- </li>
- </ul>
+ <div className="modal-simple-head text-center">
+ <h1>{translate('onboarding.header')}</h1>
+ <p className="spacer-top">{translate('onboarding.header.description')}</p>
+ </div>
+ <div className="modal-simple-body text-center onboarding-choices">
+ <Button className="onboarding-choice" onClick={this.openProjectOnboarding}>
+ <OnboardingProjectIcon />
+ <span>{translate('onboarding.analyze_public_code')}</span>
+ <p className="note">{translate('onboarding.analyze_public_code.note')}</p>
+ </Button>
+ <Button className="onboarding-choice" onClick={this.props.onOpenOrganizationOnboarding}>
+ <OnboardingPrivateIcon />
+ <span>{translate('onboarding.analyze_private_code')}</span>
+ <p className="note">{translate('onboarding.analyze_private_code.note')}</p>
+ </Button>
+ <Button className="onboarding-choice" onClick={this.props.onOpenTeamOnboarding}>
+ <OnboardingTeamIcon />
+ <span>{translate('onboarding.contribute_existing_project')}</span>
+ <p className="note">{translate('onboarding.contribute_existing_project.note')}</p>
+ </Button>
+ </div>
+ <div className="modal-simple-footer text-center">
+ <ResetButtonLink className="spacer-bottom" onClick={this.onFinish}>
+ {translate('not_now')}
+ </ResetButtonLink>
+ <p className="note">{translate('onboarding.footer')}</p>
</div>
- <footer className="modal-foot">
- <ResetButtonLink onClick={this.props.onFinish}>{translate('close')}</ResetButtonLink>
- </footer>
</Modal>
);
}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx
index c9b3e77500e..d0350d43cc0 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx
@@ -27,9 +27,8 @@ it('renders correctly', () => {
shallow(
<Onboarding
currentUser={{ isLoggedIn: true }}
- onFinish={jest.fn()}
+ onClose={jest.fn()}
onOpenOrganizationOnboarding={jest.fn()}
- onOpenProjectOnboarding={jest.fn()}
onOpenTeamOnboarding={jest.fn()}
/>
)
@@ -37,25 +36,25 @@ it('renders correctly', () => {
});
it('should correctly open the different tutorials', () => {
- const onFinish = jest.fn();
+ const onClose = jest.fn();
const onOpenOrganizationOnboarding = jest.fn();
- const onOpenProjectOnboarding = jest.fn();
const onOpenTeamOnboarding = jest.fn();
+ const push = jest.fn();
const wrapper = shallow(
<Onboarding
currentUser={{ isLoggedIn: true }}
- onFinish={onFinish}
+ onClose={onClose}
onOpenOrganizationOnboarding={onOpenOrganizationOnboarding}
- onOpenProjectOnboarding={onOpenProjectOnboarding}
onOpenTeamOnboarding={onOpenTeamOnboarding}
- />
+ />,
+ { context: { router: { push } } }
);
click(wrapper.find('ResetButtonLink'));
- expect(onFinish).toHaveBeenCalled();
+ expect(onClose).toHaveBeenCalled();
wrapper.find('Button').forEach(button => click(button));
expect(onOpenOrganizationOnboarding).toHaveBeenCalled();
- expect(onOpenProjectOnboarding).toHaveBeenCalled();
expect(onOpenTeamOnboarding).toHaveBeenCalled();
+ expect(push).toHaveBeenCalledWith('/onboarding');
});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap
index e017f778601..e0a56e3fc79 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap
@@ -4,79 +4,81 @@ exports[`renders correctly 1`] = `
<Modal
contentLabel="onboarding.header"
medium={true}
- onRequestClose={[MockFunction]}
+ onRequestClose={[Function]}
shouldCloseOnOverlayClick={false}
>
- <header
- className="modal-head"
- >
- <h2>
- onboarding.header
- </h2>
- </header>
<div
- className="modal-body"
+ className="modal-simple-head text-center"
>
+ <h1>
+ onboarding.header
+ </h1>
<p
- className="spacer-top big-spacer-bottom"
+ className="spacer-top"
>
onboarding.header.description
</p>
- <ul
- className="onboarding-choices"
+ </div>
+ <div
+ className="modal-simple-body text-center onboarding-choices"
+ >
+ <Button
+ className="onboarding-choice"
+ onClick={[Function]}
>
- <li
- className="text-center"
+ <OnboardingProjectIcon />
+ <span>
+ onboarding.analyze_public_code
+ </span>
+ <p
+ className="note"
>
- <p
- className="big-spacer-bottom"
- >
- onboarding.analyze_public_code
- </p>
- <Button
- onClick={[MockFunction]}
- >
- onboarding.analyze_public_code.button
- </Button>
- </li>
- <li
- className="text-center"
+ onboarding.analyze_public_code.note
+ </p>
+ </Button>
+ <Button
+ className="onboarding-choice"
+ onClick={[MockFunction]}
+ >
+ <OnboardingPrivateIcon />
+ <span>
+ onboarding.analyze_private_code
+ </span>
+ <p
+ className="note"
>
- <p
- className="big-spacer-bottom"
- >
- onboarding.analyze_private_code
- </p>
- <Button
- onClick={[MockFunction]}
- >
- onboarding.analyze_private_code.button
- </Button>
- </li>
- <li
- className="text-center"
+ onboarding.analyze_private_code.note
+ </p>
+ </Button>
+ <Button
+ className="onboarding-choice"
+ onClick={[MockFunction]}
+ >
+ <OnboardingTeamIcon />
+ <span>
+ onboarding.contribute_existing_project
+ </span>
+ <p
+ className="note"
>
- <p
- className="big-spacer-bottom"
- >
- onboarding.contribute_existing_project
- </p>
- <Button
- onClick={[MockFunction]}
- >
- onboarding.contribute_existing_project.button
- </Button>
- </li>
- </ul>
+ onboarding.contribute_existing_project.note
+ </p>
+ </Button>
</div>
- <footer
- className="modal-foot"
+ <div
+ className="modal-simple-footer text-center"
>
<ResetButtonLink
- onClick={[MockFunction]}
+ className="spacer-bottom"
+ onClick={[Function]}
>
- close
+ not_now
</ResetButtonLink>
- </footer>
+ <p
+ className="note"
+ >
+ onboarding.footer
+ </p>
+ </div>
</Modal>
`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx
new file mode 100644
index 00000000000..58c86e34696
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx
@@ -0,0 +1,132 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
+import { getIdentityProviders } from '../../../api/users';
+import { getRepositories } from '../../../api/alm-integration';
+import { translateWithParameters } from '../../../helpers/l10n';
+import { IdentityProvider, LoggedInUser } from '../../../app/types';
+
+interface Props {
+ currentUser: LoggedInUser;
+}
+
+interface State {
+ identityProviders: IdentityProvider[];
+ installationUrl?: string;
+ installed?: boolean;
+ loading: boolean;
+}
+
+export default class AutoProjectCreate extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { identityProviders: [], loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ Promise.all([this.fetchIdentityProviders(), this.fetchRepositories()]).then(
+ this.stopLoading,
+ this.stopLoading
+ );
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchIdentityProviders = () => {
+ return getIdentityProviders().then(
+ ({ identityProviders }) => {
+ if (this.mounted) {
+ this.setState({ identityProviders });
+ }
+ },
+ () => {
+ return Promise.resolve();
+ }
+ );
+ };
+
+ fetchRepositories = () => {
+ return getRepositories().then(({ installation }) => {
+ if (this.mounted) {
+ this.setState({
+ installationUrl: installation.installationUrl,
+ installed: installation.enabled
+ });
+ }
+ });
+ };
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ render() {
+ if (this.state.loading) {
+ return <DeferredSpinner />;
+ }
+
+ const { currentUser } = this.props;
+ const identityProvider = this.state.identityProviders.find(
+ identityProvider => identityProvider.key === currentUser.externalProvider
+ );
+
+ if (!identityProvider) {
+ return null;
+ }
+
+ return (
+ <>
+ <p className="alert alert-info width-60 big-spacer-bottom">
+ {translateWithParameters(
+ 'onboarding.create_project.beta_feature_x',
+ identityProvider.name
+ )}
+ </p>
+ {this.state.installed ? (
+ 'Repositories list'
+ ) : (
+ <div>
+ <p className="spacer-bottom">
+ {translateWithParameters(
+ 'onboarding.create_project.install_app_x',
+ identityProvider.name
+ )}
+ </p>
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={identityProvider}
+ small={true}
+ url={this.state.installationUrl}>
+ {translateWithParameters(
+ 'onboarding.create_project.install_app_x.button',
+ identityProvider.name
+ )}
+ </IdentityProviderLink>
+ </div>
+ )}
+ </>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx
new file mode 100644
index 00000000000..7803335143c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx
@@ -0,0 +1,182 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import * as PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import Helmet from 'react-helmet';
+import AutoProjectCreate from './AutoProjectCreate';
+import ManualProjectCreate from './ManualProjectCreate';
+import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
+import { getCurrentUser } from '../../../store/rootReducer';
+import { skipOnboarding } from '../../../store/users/actions';
+import { CurrentUser, isLoggedIn } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+import { ProjectBase } from '../../../api/components';
+import { getProjectUrl, getOrganizationUrl } from '../../../helpers/urls';
+import '../../../app/styles/sonarcloud.css';
+import '../styles.css';
+
+interface OwnProps {
+ onFinishOnboarding: () => void;
+}
+
+interface StateProps {
+ currentUser: CurrentUser;
+}
+
+interface DispatchProps {
+ skipOnboarding: () => void;
+}
+
+enum Tabs {
+ AUTO,
+ MANUAL
+}
+
+type Props = OwnProps & StateProps & DispatchProps;
+
+interface State {
+ activeTab: Tabs;
+}
+
+export class CreateProjectOnboarding extends React.PureComponent<Props, State> {
+ mounted = false;
+ static contextTypes = {
+ router: PropTypes.object
+ };
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { activeTab: this.shouldDisplayTabs(props) ? Tabs.AUTO : Tabs.MANUAL };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ if (!isLoggedIn(this.props.currentUser)) {
+ handleRequiredAuthentication();
+ }
+ document.body.classList.add('white-page');
+ document.documentElement.classList.add('white-page');
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ document.body.classList.remove('white-page');
+ document.documentElement.classList.remove('white-page');
+ }
+
+ handleProjectCreate = (projects: Pick<ProjectBase, 'key'>[], organization?: string) => {
+ if (projects.length > 1 && organization) {
+ this.context.router.push(getOrganizationUrl(organization) + '/projects');
+ } else if (projects.length === 1) {
+ this.context.router.push(getProjectUrl(projects[0].key));
+ }
+ };
+
+ shouldDisplayTabs = ({ currentUser } = this.props) => {
+ return (
+ isLoggedIn(currentUser) &&
+ ['bitbucket', 'github'].includes(currentUser.externalProvider || '')
+ );
+ };
+
+ showAuto = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.setState({ activeTab: Tabs.AUTO });
+ };
+
+ showManual = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.setState({ activeTab: Tabs.MANUAL });
+ };
+
+ render() {
+ const { currentUser } = this.props;
+ if (!isLoggedIn(currentUser)) {
+ return null;
+ }
+
+ const { activeTab } = this.state;
+ const header = translate('onboarding.create_project.header');
+ return (
+ <>
+ <Helmet title={header} titleTemplate="%s" />
+ <div className="sonarcloud page page-limited">
+ <div className="page-header">
+ <h1 className="page-title">{header}</h1>
+ </div>
+
+ {this.shouldDisplayTabs() && (
+ <ul className="flex-tabs">
+ <li>
+ <a
+ className={classNames('js-auto', { selected: activeTab === Tabs.AUTO })}
+ href="#"
+ onClick={this.showAuto}>
+ {translate('onboarding.create_project.select_repositories')}
+ <span
+ className={classNames(
+ 'rounded alert alert-small spacer-left display-inline-block',
+ {
+ 'alert-info': activeTab === Tabs.AUTO,
+ 'alert-muted': activeTab !== Tabs.AUTO
+ }
+ )}>
+ {translate('beta')}
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ className={classNames('js-manual', { selected: activeTab === Tabs.MANUAL })}
+ href="#"
+ onClick={this.showManual}>
+ {translate('onboarding.create_project.create_manually')}
+ </a>
+ </li>
+ </ul>
+ )}
+
+ {activeTab === Tabs.AUTO ? (
+ <AutoProjectCreate currentUser={currentUser} />
+ ) : (
+ <ManualProjectCreate
+ currentUser={currentUser}
+ onProjectCreate={this.handleProjectCreate}
+ />
+ )}
+ </div>
+ </>
+ );
+ }
+}
+
+const mapStateToProps = (state: any): StateProps => {
+ return {
+ currentUser: getCurrentUser(state)
+ };
+};
+
+const mapDispatchToProps: DispatchProps = { skipOnboarding };
+
+export default connect<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)(
+ CreateProjectOnboarding
+);
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx
new file mode 100644
index 00000000000..59d615583c7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx
@@ -0,0 +1,227 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { sortBy } from 'lodash';
+import { connect } from 'react-redux';
+import CreateOrganizationForm from '../../account/organizations/CreateOrganizationForm';
+import Select from '../../../components/controls/Select';
+import { Button, SubmitButton } from '../../../components/ui/buttons';
+import { LoggedInUser, Organization } from '../../../app/types';
+import { fetchMyOrganizations } from '../../account/organizations/actions';
+import { getMyOrganizations } from '../../../store/rootReducer';
+import { translate } from '../../../helpers/l10n';
+import { createProject, ProjectBase } from '../../../api/components';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+
+interface StateProps {
+ userOrganizations: Organization[];
+}
+
+interface DispatchProps {
+ fetchMyOrganizations: () => Promise<void>;
+}
+
+interface OwnProps {
+ currentUser: LoggedInUser;
+ onProjectCreate: (project: ProjectBase[]) => void;
+}
+
+type Props = OwnProps & StateProps & DispatchProps;
+
+interface State {
+ createOrganizationModal: boolean;
+ projectName: string;
+ projectKey: string;
+ selectedOrganization: string;
+ submitting: boolean;
+}
+
+export class ManualProjectCreate extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ createOrganizationModal: false,
+ projectName: '',
+ projectKey: '',
+ selectedOrganization:
+ props.userOrganizations.length <= 1 ? props.userOrganizations[0].key : '',
+ submitting: false
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ closeCreateOrganization = () => {
+ this.setState({ createOrganizationModal: false });
+ };
+
+ handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+
+ if (this.isValid()) {
+ const { projectKey, projectName, selectedOrganization } = this.state;
+ this.setState({ submitting: true });
+ createProject({
+ project: projectKey,
+ name: projectName,
+ organization: selectedOrganization
+ }).then(
+ ({ project }) => this.props.onProjectCreate([project]),
+ () => {
+ if (this.mounted) {
+ this.setState({ submitting: false });
+ }
+ }
+ );
+ }
+ };
+
+ handleOrganizationSelect = ({ value }: { value: string }) => {
+ this.setState({ selectedOrganization: value });
+ };
+
+ handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.setState({ projectName: event.currentTarget.value });
+ };
+
+ handleProjectKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.setState({ projectKey: event.currentTarget.value });
+ };
+
+ isValid = () => {
+ const { projectKey, projectName, selectedOrganization } = this.state;
+ return Boolean(projectKey && projectName && selectedOrganization);
+ };
+
+ onCreateOrganization = (organization: { key: string }) => {
+ this.props.fetchMyOrganizations().then(
+ () => {
+ this.handleOrganizationSelect({ value: organization.key });
+ this.closeCreateOrganization();
+ },
+ () => {
+ this.closeCreateOrganization();
+ }
+ );
+ };
+
+ showCreateOrganization = () => {
+ this.setState({ createOrganizationModal: true });
+ };
+
+ render() {
+ const { submitting } = this.state;
+ return (
+ <>
+ <form onSubmit={this.handleFormSubmit}>
+ <div className="form-field">
+ <label htmlFor="select-organization">
+ {translate('onboarding.create_project.organization')}
+ <em className="mandatory">*</em>
+ </label>
+ <Select
+ autoFocus={true}
+ className="input-super-large"
+ clearable={false}
+ id="select-organization"
+ onChange={this.handleOrganizationSelect}
+ options={sortBy(this.props.userOrganizations, o => o.name.toLowerCase()).map(
+ organization => ({
+ label: organization.name,
+ value: organization.key
+ })
+ )}
+ required={true}
+ value={this.state.selectedOrganization}
+ />
+ <Button
+ className="button-link big-spacer-left js-new-org"
+ onClick={this.showCreateOrganization}>
+ {translate('onboarding.create_project.create_new_org')}
+ </Button>
+ </div>
+ <div className="form-field">
+ <label htmlFor="project-name">
+ {translate('onboarding.create_project.project_name')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-name"
+ maxLength={400}
+ minLength={1}
+ onChange={this.handleProjectNameChange}
+ required={true}
+ type="text"
+ value={this.state.projectName}
+ />
+ </div>
+ <div className="form-field">
+ <label htmlFor="project-key">
+ {translate('onboarding.create_project.project_key')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={this.handleProjectKeyChange}
+ required={true}
+ type="text"
+ value={this.state.projectKey}
+ />
+ </div>
+ <SubmitButton disabled={!this.isValid() || submitting}>
+ {translate('onboarding.create_project.create_project')}
+ </SubmitButton>
+ <DeferredSpinner className="spacer-left" loading={submitting} />
+ </form>
+ {this.state.createOrganizationModal && (
+ <CreateOrganizationForm
+ onClose={this.closeCreateOrganization}
+ onCreate={this.onCreateOrganization}
+ />
+ )}
+ </>
+ );
+ }
+}
+
+const mapDispatchToProps = ({
+ fetchMyOrganizations
+} as any) as DispatchProps;
+
+const mapStateToProps = (state: any): StateProps => {
+ return {
+ userOrganizations: getMyOrganizations(state)
+ };
+};
+export default connect<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)(
+ ManualProjectCreate
+);
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx
new file mode 100644
index 00000000000..10ea4c7e215
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import AutoProjectCreate from '../AutoProjectCreate';
+import { getIdentityProviders } from '../../../../api/users';
+import { getRepositories } from '../../../../api/alm-integration';
+import { LoggedInUser } from '../../../../app/types';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/users', () => ({
+ getIdentityProviders: jest.fn().mockResolvedValue({
+ identityProviders: [
+ {
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ key: 'foo',
+ name: 'Foo Provider'
+ }
+ ]
+ })
+}));
+
+jest.mock('../../../../api/alm-integration', () => ({
+ getRepositories: jest.fn().mockResolvedValue({
+ installation: {
+ installationUrl: 'https://alm.foo.com/install',
+ enabled: false
+ }
+ })
+}));
+
+const user: LoggedInUser = { isLoggedIn: true, login: 'foo', name: 'Foo', externalProvider: 'foo' };
+
+beforeEach(() => {
+ (getIdentityProviders as jest.Mock<any>).mockClear();
+ (getRepositories as jest.Mock<any>).mockClear();
+});
+
+it('should display the provider app install button', async () => {
+ const wrapper = getWrapper();
+ expect(wrapper).toMatchSnapshot();
+ expect(getIdentityProviders).toHaveBeenCalled();
+ expect(getRepositories).toHaveBeenCalled();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+ return shallow(<AutoProjectCreate currentUser={user} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx
new file mode 100644
index 00000000000..f7cfa3ce8da
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { CreateProjectOnboarding } from '../CreateProjectOnboarding';
+import { LoggedInUser } from '../../../../app/types';
+import { click } from '../../../../helpers/testUtils';
+
+const user: LoggedInUser = {
+ externalProvider: 'github',
+ isLoggedIn: true,
+ login: 'foo',
+ name: 'Foo'
+};
+
+it('should render correctly', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should render with Manual creation only', () => {
+ expect(getWrapper({ currentUser: { ...user, externalProvider: 'vsts' } })).toMatchSnapshot();
+});
+
+it('should switch tabs', () => {
+ const wrapper = getWrapper();
+ click(wrapper.find('.js-manual'));
+ expect(wrapper.find('Connect(ManualProjectCreate)').exists()).toBeTruthy();
+ click(wrapper.find('.js-auto'));
+ expect(wrapper.find('AutoProjectCreate').exists()).toBeTruthy();
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <CreateProjectOnboarding
+ currentUser={user}
+ onFinishOnboarding={jest.fn()}
+ skipOnboarding={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx
new file mode 100644
index 00000000000..b79b4e4ae35
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { ManualProjectCreate } from '../ManualProjectCreate';
+import { change, click, submit, waitAndUpdate } from '../../../../helpers/testUtils';
+import { createProject } from '../../../../api/components';
+
+jest.mock('../../../../api/components', () => ({
+ createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } })
+}));
+
+beforeEach(() => {
+ (createProject as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should allow to create a new org', async () => {
+ const fetchMyOrganizations = jest.fn().mockResolvedValueOnce([]);
+ const wrapper = getWrapper({ fetchMyOrganizations });
+
+ click(wrapper.find('.js-new-org'));
+ const createForm = wrapper.find('Connect(CreateOrganizationForm)');
+ expect(createForm.exists()).toBeTruthy();
+
+ createForm.prop<Function>('onCreate')({ key: 'baz' });
+ expect(fetchMyOrganizations).toHaveBeenCalled();
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state('selectedOrganization')).toBe('baz');
+});
+
+it('should correctly create a project', async () => {
+ const onProjectCreate = jest.fn();
+ const wrapper = getWrapper({ onProjectCreate });
+ wrapper.find('Select').prop<Function>('onChange')({ value: 'foo' });
+ change(wrapper.find('#project-name'), 'Bar');
+ expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+
+ change(wrapper.find('#project-key'), 'bar');
+ expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+
+ submit(wrapper.find('form'));
+ expect(createProject).toBeCalledWith({ project: 'bar', name: 'Bar', organization: 'foo' });
+
+ await waitAndUpdate(wrapper);
+ expect(onProjectCreate).toBeCalledWith([{ key: 'bar', name: 'Bar' }]);
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <ManualProjectCreate
+ currentUser={{ isLoggedIn: true, login: 'foo', name: 'Foo' }}
+ fetchMyOrganizations={jest.fn()}
+ onProjectCreate={jest.fn()}
+ userOrganizations={[{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
new file mode 100644
index 00000000000..9320a251046
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the provider app install button 1`] = `
+<DeferredSpinner
+ timeout={100}
+/>
+`;
+
+exports[`should display the provider app install button 2`] = `
+<React.Fragment>
+ <p
+ className="alert alert-info width-60 big-spacer-bottom"
+ >
+ onboarding.create_project.beta_feature_x.Foo Provider
+ </p>
+ <div>
+ <p
+ className="spacer-bottom"
+ >
+ onboarding.create_project.install_app_x.Foo Provider
+ </p>
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "key": "foo",
+ "name": "Foo Provider",
+ }
+ }
+ small={true}
+ url="https://alm.foo.com/install"
+ >
+ onboarding.create_project.install_app_x.button.Foo Provider
+ </IdentityProviderLink>
+ </div>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap
new file mode 100644
index 00000000000..9542df43fb9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap
@@ -0,0 +1,97 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<React.Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_project.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <div
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ onboarding.create_project.header
+ </h1>
+ </div>
+ <ul
+ className="flex-tabs"
+ >
+ <li>
+ <a
+ className="js-auto selected"
+ href="#"
+ onClick={[Function]}
+ >
+ onboarding.create_project.select_repositories
+ <span
+ className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ >
+ beta
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ className="js-manual"
+ href="#"
+ onClick={[Function]}
+ >
+ onboarding.create_project.create_manually
+ </a>
+ </li>
+ </ul>
+ <AutoProjectCreate
+ currentUser={
+ Object {
+ "externalProvider": "github",
+ "isLoggedIn": true,
+ "login": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ </div>
+</React.Fragment>
+`;
+
+exports[`should render with Manual creation only 1`] = `
+<React.Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_project.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <div
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ onboarding.create_project.header
+ </h1>
+ </div>
+ <Connect(ManualProjectCreate)
+ currentUser={
+ Object {
+ "externalProvider": "vsts",
+ "isLoggedIn": true,
+ "login": "foo",
+ "name": "Foo",
+ }
+ }
+ onProjectCreate={[Function]}
+ />
+ </div>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
new file mode 100644
index 00000000000..fafb751c9bb
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
@@ -0,0 +1,125 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly create a project 1`] = `
+<SubmitButton
+ disabled={true}
+>
+ onboarding.create_project.create_project
+</SubmitButton>
+`;
+
+exports[`should correctly create a project 2`] = `
+<SubmitButton
+ disabled={false}
+>
+ onboarding.create_project.create_project
+</SubmitButton>
+`;
+
+exports[`should render correctly 1`] = `
+<React.Fragment>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="select-organization"
+ >
+ onboarding.create_project.organization
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <Select
+ autoFocus={true}
+ className="input-super-large"
+ clearable={false}
+ id="select-organization"
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "Bar",
+ "value": "bar",
+ },
+ Object {
+ "label": "Foo",
+ "value": "foo",
+ },
+ ]
+ }
+ required={true}
+ value=""
+ />
+ <Button
+ className="button-link big-spacer-left js-new-org"
+ onClick={[Function]}
+ >
+ onboarding.create_project.create_new_org
+ </Button>
+ </div>
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="project-name"
+ >
+ onboarding.create_project.project_name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-name"
+ maxLength={400}
+ minLength={1}
+ onChange={[Function]}
+ required={true}
+ type="text"
+ value=""
+ />
+ </div>
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="project-key"
+ >
+ onboarding.create_project.project_key
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={[Function]}
+ required={true}
+ type="text"
+ value=""
+ />
+ </div>
+ <SubmitButton
+ disabled={true}
+ >
+ onboarding.create_project.create_project
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ timeout={100}
+ />
+ </form>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/routes.ts b/server/sonar-web/src/main/js/apps/tutorials/routes.ts
new file mode 100644
index 00000000000..9f5b34ba428
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/routes.ts
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { lazyLoad } from '../../components/lazyLoad';
+import { isSonarCloud } from '../../helpers/system';
+
+const routes = [
+ {
+ indexRoute: {
+ component: lazyLoad(
+ () =>
+ isSonarCloud()
+ ? import('../../apps/tutorials/createProjectOnboarding/CreateProjectOnboarding')
+ : import('../../apps/tutorials/projectOnboarding/ProjectOnboardingPage')
+ )
+ }
+ }
+];
+
+export default routes;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/styles.css b/server/sonar-web/src/main/js/apps/tutorials/styles.css
index f73428e0f40..798fce21bcc 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/styles.css
+++ b/server/sonar-web/src/main/js/apps/tutorials/styles.css
@@ -58,5 +58,43 @@
.onboarding-choices {
display: flex;
justify-content: space-around;
- padding: 24px 0 44px;
+ padding-top: 44px;
+ padding-bottom: 44px;
+ background-color: var(--barBackgroundColor);
+}
+
+.onboarding-choice {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ padding: calc(2 * var(--gridSize));
+ width: 190px;
+ height: 190px;
+ background-color: #fff;
+ border: solid 1px #fff;
+ border-radius: 3px;
+ transition: all 0.2s ease;
+ box-shadow: 0 1px 1px 1px var(--barBorderColor);
+}
+
+.onboarding-choice svg {
+ color: var(--gray40);
+ margin-bottom: calc(3 * var(--gridSize));
+}
+
+.onboarding-choice span {
+ font-size: var(--mediumFontSize);
+ margin-bottom: calc(var(--gridSize) / 2);
+}
+
+.onboarding-choice .note {
+ font-weight: 400;
+}
+
+.onboarding-choice:hover,
+.onboarding-choice:focus,
+.onboarding-choice:active {
+ background-color: #fff;
+ box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.35);
+ color: var(--darkBlue);
}