From b08814f7807c1443592af65cd68c2a51dfd4ee37 Mon Sep 17 00:00:00 2001 From: Grégoire Aubert Date: Fri, 20 Jul 2018 16:57:23 +0200 Subject: 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 --- .../src/main/js/apps/tutorials/Onboarding.tsx | 82 ++++---- .../apps/tutorials/__tests__/Onboarding-test.tsx | 17 +- .../__snapshots__/Onboarding-test.tsx.snap | 116 +++++------ .../createProjectOnboarding/AutoProjectCreate.tsx | 132 ++++++++++++ .../CreateProjectOnboarding.tsx | 182 +++++++++++++++++ .../ManualProjectCreate.tsx | 227 +++++++++++++++++++++ .../__tests__/AutoProjectCreate-test.tsx | 69 +++++++ .../__tests__/CreateProjectOnboarding-test.tsx | 58 ++++++ .../__tests__/ManualProjectCreate-test.tsx | 79 +++++++ .../__snapshots__/AutoProjectCreate-test.tsx.snap | 39 ++++ .../CreateProjectOnboarding-test.tsx.snap | 97 +++++++++ .../ManualProjectCreate-test.tsx.snap | 125 ++++++++++++ .../sonar-web/src/main/js/apps/tutorials/routes.ts | 36 ++++ .../src/main/js/apps/tutorials/styles.css | 40 +++- 14 files changed, 1196 insertions(+), 103 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx create mode 100644 server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx create mode 100644 server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx create mode 100644 server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/tutorials/routes.ts (limited to 'server/sonar-web/src/main/js/apps/tutorials') 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 { + 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 { -
-

{header}

-
-
-

- {translate('onboarding.header.description')} -

-
    -
  • -

    {translate('onboarding.analyze_public_code')}

    - -
  • -
  • -

    {translate('onboarding.analyze_private_code')}

    - -
  • -
  • -

    - {translate('onboarding.contribute_existing_project')} -

    - -
  • -
+
+

{translate('onboarding.header')}

+

{translate('onboarding.header.description')}

+
+
+ + + +
+
+ + {translate('not_now')} + +

{translate('onboarding.footer')}

-
- {translate('close')} -
); } 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( ) @@ -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( + />, + { 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`] = ` -
-

- onboarding.header -

-
+

+ onboarding.header +

onboarding.header.description

-
    +
    + - -
  • + + -
  • -
  • + + -
  • -
+ onboarding.contribute_existing_project.note +

+
-
- close + not_now -
+

+ onboarding.footer +

+
`; 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 { + 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 ; + } + + const { currentUser } = this.props; + const identityProvider = this.state.identityProviders.find( + identityProvider => identityProvider.key === currentUser.externalProvider + ); + + if (!identityProvider) { + return null; + } + + return ( + <> +

+ {translateWithParameters( + 'onboarding.create_project.beta_feature_x', + identityProvider.name + )} +

+ {this.state.installed ? ( + 'Repositories list' + ) : ( +
+

+ {translateWithParameters( + 'onboarding.create_project.install_app_x', + identityProvider.name + )} +

+ + {translateWithParameters( + 'onboarding.create_project.install_app_x.button', + identityProvider.name + )} + +
+ )} + + ); + } +} 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 { + 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[], 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) => { + event.preventDefault(); + this.setState({ activeTab: Tabs.AUTO }); + }; + + showManual = (event: React.MouseEvent) => { + 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 ( + <> + +
+
+

{header}

+
+ + {this.shouldDisplayTabs() && ( + + )} + + {activeTab === Tabs.AUTO ? ( + + ) : ( + + )} +
+ + ); + } +} + +const mapStateToProps = (state: any): StateProps => { + return { + currentUser: getCurrentUser(state) + }; +}; + +const mapDispatchToProps: DispatchProps = { skipOnboarding }; + +export default connect(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; +} + +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 { + 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) => { + 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) => { + this.setState({ projectName: event.currentTarget.value }); + }; + + handleProjectKeyChange = (event: React.ChangeEvent) => { + 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 ( + <> +
+
+ + +
+
+ + +
+ + {translate('onboarding.create_project.create_project')} + + + + {this.state.createOrganizationModal && ( + + )} + + ); + } +} + +const mapDispatchToProps = ({ + fetchMyOrganizations +} as any) as DispatchProps; + +const mapStateToProps = (state: any): StateProps => { + return { + userOrganizations: getMyOrganizations(state) + }; +}; +export default connect(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).mockClear(); + (getRepositories as jest.Mock).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(); +} 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( + + ); +} 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).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('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('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( + + ); +} 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`] = ` + +`; + +exports[`should display the provider app install button 2`] = ` + +

+ onboarding.create_project.beta_feature_x.Foo Provider +

+
+

+ onboarding.create_project.install_app_x.Foo Provider +

+ + onboarding.create_project.install_app_x.button.Foo Provider + +
+
+`; 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`] = ` + + + + +`; + +exports[`should render with Manual creation only 1`] = ` + + +
+
+

+ onboarding.create_project.header +

+
+ +
+
+`; 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`] = ` + + onboarding.create_project.create_project + +`; + +exports[`should correctly create a project 2`] = ` + + onboarding.create_project.create_project + +`; + +exports[`should render correctly 1`] = ` + +
+
+ + +
+
+ + +
+ + onboarding.create_project.create_project + + + +
+`; 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); } -- cgit v1.2.3