Browse Source

SONARCLOUD-64 New onboarding with 3 cases for SonarCloud (#383)

* SONARCLOUD-64 Move Onboarding to ProjectOnboarding
* SONARCLOUD-64 Migrate project onboarding to TS
* SONARCLOUD-64 Update ProjectOnboarding style
* SONARCLOUD-64 Add main onboarding page
tags/7.5
Grégoire Aubert 6 years ago
parent
commit
4e91bd432a
74 changed files with 1460 additions and 1194 deletions
  1. 63
    12
      server/sonar-web/src/main/js/app/components/StartupModal.tsx
  2. 5
    4
      server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx
  3. 2
    2
      server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
  4. 2
    2
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
  5. 2
    2
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
  6. 4
    4
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
  7. 1
    1
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  8. 2
    2
      server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
  9. 102
    0
      server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx
  10. 61
    0
      server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx
  11. 82
    0
      server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap
  12. 0
    236
      server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
  13. 0
    33
      server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js
  14. 0
    388
      server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap
  15. 80
    69
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/AnalysisStep.tsx
  16. 22
    29
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/LanguageStep.tsx
  17. 19
    26
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/NewOrganizationForm.tsx
  18. 23
    26
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/NewProjectForm.tsx
  19. 26
    32
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/OrganizationStep.tsx
  20. 206
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx
  21. 3
    3
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingModal.tsx
  22. 4
    4
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingPage.tsx
  23. 24
    34
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectWatcher.tsx
  24. 14
    16
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/Step.tsx
  25. 45
    57
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/TokenStep.tsx
  26. 23
    24
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/LanguageStep-test.tsx
  27. 2
    3
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/NewOrganizationForm-test.tsx
  28. 2
    3
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/NewProjectForm-test.tsx
  29. 5
    6
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/OrganizationStep-test.tsx
  30. 15
    20
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectOnboarding-test.tsx
  31. 1
    2
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectWatcher-test.tsx
  32. 1
    2
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/Step-test.tsx
  33. 2
    3
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/TokenStep-test.tsx
  34. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/LanguageStep-test.tsx.snap
  35. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/NewOrganizationForm-test.tsx.snap
  36. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/NewProjectForm-test.tsx.snap
  37. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/OrganizationStep-test.tsx.snap
  38. 349
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/ProjectOnboarding-test.tsx.snap
  39. 4
    4
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/ProjectWatcher-test.tsx.snap
  40. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/Step-test.tsx.snap
  41. 6
    6
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/TokenStep-test.tsx.snap
  42. 9
    11
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/BuildWrapper.tsx
  43. 10
    13
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/ClangGCC.tsx
  44. 8
    11
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/DotNet.tsx
  45. 7
    10
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/JavaGradle.tsx
  46. 7
    10
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/JavaMaven.tsx
  47. 7
    8
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/MSBuildScanner.tsx
  48. 8
    11
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/Msvc.tsx
  49. 9
    12
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/Other.tsx
  50. 8
    9
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/SQScanner.tsx
  51. 1
    2
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/BuildWrapper-test.tsx
  52. 3
    3
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/ClangGCC-test.tsx
  53. 2
    2
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/DotNet-test.tsx
  54. 2
    2
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/JavaGradle-test.tsx
  55. 2
    2
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/JavaMaven-test.tsx
  56. 2
    2
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/MSBuildScanner-test.tsx
  57. 2
    2
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/Msvc-test.tsx
  58. 3
    3
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/Other-test.tsx
  59. 2
    2
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/SQScanner-test.tsx
  60. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/BuildWrapper-test.tsx.snap
  61. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/ClangGCC-test.tsx.snap
  62. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/DotNet-test.tsx.snap
  63. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap
  64. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap
  65. 1
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.tsx.snap
  66. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/Msvc-test.tsx.snap
  67. 0
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/Other-test.tsx.snap
  68. 3
    0
      server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/SQScanner-test.tsx.snap
  69. 4
    11
      server/sonar-web/src/main/js/apps/tutorials/styles.css
  70. 68
    0
      server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/TeamOnboardingModal.tsx
  71. 5
    5
      server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/TeamOnboardingModal-test.tsx
  72. 61
    0
      server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/__snapshots__/TeamOnboardingModal-test.tsx.snap
  73. 2
    2
      server/sonar-web/src/main/js/helpers/testUtils.ts
  74. 22
    6
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 63
- 12
server/sonar-web/src/main/js/app/components/StartupModal.tsx View File

@@ -20,16 +20,21 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { connect } from 'react-redux';
import OnboardingModal from '../../apps/tutorials/onboarding/OnboardingModal';
import Onboarding from '../../apps/tutorials/Onboarding';
import CreateOrganizationForm from '../../apps/account/organizations/CreateOrganizationForm';
import LicensePromptModal from '../../apps/marketplace/components/LicensePromptModal';
import { CurrentUser, isLoggedIn } from '../types';
import ProjectOnboardingModal from '../../apps/tutorials/projectOnboarding/ProjectOnboardingModal';
import TeamOnboardingModal from '../../apps/tutorials/teamOnboarding/TeamOnboardingModal';
import { CurrentUser, isLoggedIn, Organization } from '../types';
import { differenceInDays, parseDate, toShortNotSoISOString } from '../../helpers/dates';
import { EditionKey } from '../../apps/marketplace/utils';
import { getCurrentUser, getAppState } from '../../store/rootReducer';
import { skipOnboarding } from '../../store/users/actions';
import { skipOnboarding as skipOnboardingAction } from '../../store/users/actions';
import { showLicense } from '../../api/marketplace';
import { hasMessage } from '../../helpers/l10n';
import { save, get } from '../../helpers/storage';
import { isSonarCloud } from '../../helpers/system';
import { skipOnboarding } from '../../api/users';

interface StateProps {
canAdmin: boolean;
@@ -38,7 +43,7 @@ interface StateProps {
}

interface DispatchProps {
skipOnboarding: () => void;
skipOnboardingAction: () => void;
}

interface OwnProps {
@@ -50,7 +55,10 @@ type Props = StateProps & DispatchProps & OwnProps;

enum ModalKey {
license,
onboarding
onboarding,
organizationOnboarding,
projectOnboarding,
teamOnboarding
}

interface State {
@@ -61,14 +69,18 @@ interface State {
const LICENSE_PROMPT = 'sonarqube.license.prompt';

export class StartupModal extends React.PureComponent<Props, State> {
static contextTypes = {
router: PropTypes.object.isRequired
};

static childContextTypes = {
openOnboardingTutorial: PropTypes.func
openProjectOnboarding: PropTypes.func
};

state: State = { automatic: false };

getChildContext() {
return { openOnboardingTutorial: this.openOnboarding };
return { openProjectOnboarding: this.openProjectOnboarding };
}

componentDidMount() {
@@ -77,8 +89,9 @@ export class StartupModal extends React.PureComponent<Props, State> {

closeOnboarding = () => {
this.setState(state => {
if (state.modal === ModalKey.onboarding) {
this.props.skipOnboarding();
if (state.modal !== ModalKey.license) {
skipOnboarding();
this.props.skipOnboardingAction();
return { automatic: false, modal: undefined };
}
return undefined;
@@ -94,10 +107,27 @@ export class StartupModal extends React.PureComponent<Props, State> {
});
};

closeOrganizationOnboarding = ({ key }: Pick<Organization, 'key'>) => {
this.closeOnboarding();
this.context.router.push(`/organizations/${key}`);
};

openOnboarding = () => {
this.setState({ modal: ModalKey.onboarding });
};

openOrganizationOnboarding = () => {
this.setState({ modal: ModalKey.organizationOnboarding });
};

openProjectOnboarding = () => {
this.setState({ modal: ModalKey.projectOnboarding });
};

openTeamOnboarding = () => {
this.setState({ modal: ModalKey.teamOnboarding });
};

tryAutoOpenLicense = () => {
const { canAdmin, currentEdition, currentUser } = this.props;
const hasLicenseManager = hasMessage('license.prompt.title');
@@ -124,7 +154,11 @@ export class StartupModal extends React.PureComponent<Props, State> {
const { currentUser, location } = this.props;
if (currentUser.showOnboardingTutorial && !location.pathname.startsWith('documentation')) {
this.setState({ automatic: true });
this.openOnboarding();
if (isSonarCloud()) {
this.openOnboarding();
} else {
this.openProjectOnboarding();
}
}
};

@@ -135,7 +169,24 @@ export class StartupModal extends React.PureComponent<Props, State> {
{this.props.children}
{modal === ModalKey.license && <LicensePromptModal onClose={this.closeLicense} />}
{modal === ModalKey.onboarding && (
<OnboardingModal automatic={automatic} onFinish={this.closeOnboarding} />
<Onboarding
onFinish={this.closeOnboarding}
onOpenOrganizationOnboarding={this.openOrganizationOnboarding}
onOpenProjectOnboarding={this.openProjectOnboarding}
onOpenTeamOnboarding={this.openTeamOnboarding}
/>
)}
{modal === ModalKey.projectOnboarding && (
<ProjectOnboardingModal automatic={automatic} onFinish={this.closeOnboarding} />
)}
{modal === ModalKey.organizationOnboarding && (
<CreateOrganizationForm
onClose={this.closeOnboarding}
onCreate={this.closeOrganizationOnboarding}
/>
)}
{modal === ModalKey.teamOnboarding && (
<TeamOnboardingModal onFinish={this.closeOnboarding} />
)}
</>
);
@@ -148,7 +199,7 @@ const mapStateToProps = (state: any): StateProps => ({
currentUser: getCurrentUser(state)
});

const mapDispatchToProps: DispatchProps = { skipOnboarding };
const mapDispatchToProps: DispatchProps = { skipOnboardingAction };

export default connect<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)(
StartupModal

+ 5
- 4
server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx View File

@@ -119,12 +119,12 @@ it('should render onboarding modal', async () => {
async function shouldNotHaveModals(wrapper: ShallowWrapper) {
await waitAndUpdate(wrapper);
expect(wrapper.find('LicensePromptModal').exists()).toBeFalsy();
expect(wrapper.find('OnboardingModal').exists()).toBeFalsy();
expect(wrapper.find('ProjectOnboardingModal').exists()).toBeFalsy();
}

async function shouldDisplayOnboarding(wrapper: ShallowWrapper) {
await waitAndUpdate(wrapper);
expect(wrapper.find('OnboardingModal').exists()).toBeTruthy();
expect(wrapper.find('ProjectOnboardingModal').exists()).toBeTruthy();
}

async function shouldDisplayLicense(wrapper: ShallowWrapper) {
@@ -139,9 +139,10 @@ function getWrapper(props = {}) {
currentEdition={EditionKey.enterprise}
currentUser={LOGGED_IN_USER}
location={{ pathname: 'foo/bar' }}
skipOnboarding={jest.fn()}
skipOnboardingAction={jest.fn()}
{...props}>
<div />
</StartupModal>
</StartupModal>,
{ context: { router: { push: jest.fn() } } }
);
}

+ 2
- 2
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx View File

@@ -36,13 +36,13 @@ interface Props {

export default class EmbedDocsPopup extends React.PureComponent<Props> {
static contextTypes = {
openOnboardingTutorial: PropTypes.func
openProjectOnboarding: PropTypes.func
};

onAnalyzeProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.context.openOnboardingTutorial();
this.context.openProjectOnboarding();
};

renderTitle(text: string) {

+ 2
- 2
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx View File

@@ -50,7 +50,7 @@ interface OwnProps {
type Props = StateProps & OwnProps;

class GlobalNav extends React.PureComponent<Props> {
static contextTypes = { openOnboardingTutorial: PropTypes.func };
static contextTypes = { openProjectOnboarding: PropTypes.func };

render() {
return (
@@ -69,7 +69,7 @@ class GlobalNav extends React.PureComponent<Props> {
<Search appState={this.props.appState} currentUser={this.props.currentUser} />
{isLoggedIn(this.props.currentUser) &&
isSonarCloud() && (
<GlobalNavPlus openOnboardingTutorial={this.context.openOnboardingTutorial} />
<GlobalNavPlus openProjectOnboarding={this.context.openProjectOnboarding} />
)}
<GlobalNavUserContainer {...this.props} />
</ul>

+ 2
- 2
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx View File

@@ -25,7 +25,7 @@ import Dropdown from '../../../../components/controls/Dropdown';
import { translate } from '../../../../helpers/l10n';

interface Props {
openOnboardingTutorial: () => void;
openProjectOnboarding: () => void;
}

interface State {
@@ -44,7 +44,7 @@ export default class GlobalNavPlus extends React.PureComponent<Props, State> {

handleNewProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.openOnboardingTutorial();
this.props.openProjectOnboarding();
};

openCreateOrganizationForm = () => this.setState({ createOrganization: true });

+ 4
- 4
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx View File

@@ -23,18 +23,18 @@ import GlobalNavPlus from '../GlobalNavPlus';
import { click } from '../../../../../helpers/testUtils';

it('render', () => {
const wrapper = shallow(<GlobalNavPlus openOnboardingTutorial={jest.fn()} />);
const wrapper = shallow(<GlobalNavPlus openProjectOnboarding={jest.fn()} />);
expect(wrapper.is('Dropdown')).toBe(true);
expect(wrapper.find('Dropdown')).toMatchSnapshot();
});

it('opens onboarding', () => {
const openOnboardingTutorial = jest.fn();
const openProjectOnboarding = jest.fn();
const wrapper = shallow(
shallow(<GlobalNavPlus openOnboardingTutorial={openOnboardingTutorial} />)
shallow(<GlobalNavPlus openProjectOnboarding={openProjectOnboarding} />)
.find('Dropdown')
.prop('overlay')
);
click(wrapper.find('.js-new-project'));
expect(openOnboardingTutorial).toBeCalled();
expect(openProjectOnboarding).toBeCalled();
});

+ 1
- 1
server/sonar-web/src/main/js/app/utils/startReactApp.js View File

@@ -172,7 +172,7 @@ const startReactApp = () => {
<Route
path="onboarding"
component={lazyLoad(() =>
import('../../apps/tutorials/onboarding/OnboardingPage')
import('../../apps/tutorials/projectOnboarding/ProjectOnboardingPage')
)}
/>
<Route path="organizations" childRoutes={organizationsRoutes} />

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx View File

@@ -36,13 +36,13 @@ interface StateProps {

export class NoFavoriteProjects extends React.PureComponent<StateProps> {
static contextTypes = {
openOnboardingTutorial: PropTypes.func
openProjectOnboarding: PropTypes.func
};

onAnalyzeProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.context.openOnboardingTutorial();
this.context.openProjectOnboarding();
};

render() {

+ 102
- 0
server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx View File

@@ -0,0 +1,102 @@
/*
* 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 { connect } from 'react-redux';
import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication';
import Modal from '../../components/controls/Modal';
import { ResetButtonLink, Button } 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;
onOpenOrganizationOnboarding: () => void;
onOpenProjectOnboarding: () => void;
onOpenTeamOnboarding: () => void;
}

interface StateProps {
currentUser: CurrentUser;
}

type Props = OwnProps & StateProps;

export class Onboarding extends React.PureComponent<Props> {
componentDidMount() {
if (!isLoggedIn(this.props.currentUser)) {
handleRequiredAuthentication();
}
}

render() {
if (!isLoggedIn(this.props.currentUser)) {
return null;
}

const header = translate('onboarding.header');
return (
<Modal
contentLabel={header}
medium={true}
onRequestClose={this.props.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>
<footer className="modal-foot">
<ResetButtonLink onClick={this.props.onFinish}>{translate('close')}</ResetButtonLink>
</footer>
</Modal>
);
}
}

const mapStateToProps = (state: any): StateProps => ({ currentUser: getCurrentUser(state) });

export default connect<StateProps, {}, OwnProps>(mapStateToProps)(Onboarding);

+ 61
- 0
server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx View File

@@ -0,0 +1,61 @@
/*
* 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 { Onboarding } from '../Onboarding';
import { click } from '../../../helpers/testUtils';

it('renders correctly', () => {
expect(
shallow(
<Onboarding
currentUser={{ isLoggedIn: true }}
onFinish={jest.fn()}
onOpenOrganizationOnboarding={jest.fn()}
onOpenProjectOnboarding={jest.fn()}
onOpenTeamOnboarding={jest.fn()}
/>
)
).toMatchSnapshot();
});

it('should correctly open the different tutorials', () => {
const onFinish = jest.fn();
const onOpenOrganizationOnboarding = jest.fn();
const onOpenProjectOnboarding = jest.fn();
const onOpenTeamOnboarding = jest.fn();
const wrapper = shallow(
<Onboarding
currentUser={{ isLoggedIn: true }}
onFinish={onFinish}
onOpenOrganizationOnboarding={onOpenOrganizationOnboarding}
onOpenProjectOnboarding={onOpenProjectOnboarding}
onOpenTeamOnboarding={onOpenTeamOnboarding}
/>
);

click(wrapper.find('ResetButtonLink'));
expect(onFinish).toHaveBeenCalled();

wrapper.find('Button').forEach(button => click(button));
expect(onOpenOrganizationOnboarding).toHaveBeenCalled();
expect(onOpenProjectOnboarding).toHaveBeenCalled();
expect(onOpenTeamOnboarding).toHaveBeenCalled();
});

+ 82
- 0
server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap View File

@@ -0,0 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly 1`] = `
<Modal
contentLabel="onboarding.header"
medium={true}
onRequestClose={[MockFunction]}
shouldCloseOnOverlayClick={false}
>
<header
className="modal-head"
>
<h2>
onboarding.header
</h2>
</header>
<div
className="modal-body"
>
<p
className="spacer-top big-spacer-bottom"
>
onboarding.header.description
</p>
<ul
className="onboarding-choices"
>
<li
className="text-center"
>
<p
className="big-spacer-bottom"
>
onboarding.analyze_public_code
</p>
<Button
onClick={[MockFunction]}
>
onboarding.analyze_public_code.button
</Button>
</li>
<li
className="text-center"
>
<p
className="big-spacer-bottom"
>
onboarding.analyze_private_code
</p>
<Button
onClick={[MockFunction]}
>
onboarding.analyze_private_code.button
</Button>
</li>
<li
className="text-center"
>
<p
className="big-spacer-bottom"
>
onboarding.contribute_existing_project
</p>
<Button
onClick={[MockFunction]}
>
onboarding.contribute_existing_project.button
</Button>
</li>
</ul>
</div>
<footer
className="modal-foot"
>
<ResetButtonLink
onClick={[MockFunction]}
>
close
</ResetButtonLink>
</footer>
</Modal>
`;

+ 0
- 236
server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js View File

@@ -1,236 +0,0 @@
/*
* 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.
*/
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import TokenStep from './TokenStep';
import OrganizationStep from './OrganizationStep';
import AnalysisStep from './AnalysisStep';
import ProjectWatcher from './ProjectWatcher';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import InstanceMessage from '../../../components/common/InstanceMessage';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
import { skipOnboarding } from '../../../api/users';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls';
import { isSonarCloud } from '../../../helpers/system';
import './styles.css';

/*::
type Props = {|
automatic?:boolean,
className?: string,
currentUser: { login: string, isLoggedIn: boolean },
onFinish: () => void,
organizationsEnabled: boolean
|};
*/

/*::
type State = {
finished: boolean,
organization?: string,
projectKey?: string,
skipping: boolean,
step: string,
token?: string
};
*/

export default class Onboarding extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
/*:: state: State; */

static contextTypes = {
router: PropTypes.object
};

constructor(props /*: Props */) {
super(props);
this.state = {
finished: false,
skipping: false,
step: props.organizationsEnabled ? 'organization' : 'token'
};
}

componentDidMount() {
this.mounted = true;

// useCapture = true to receive the event before inputs
window.addEventListener('keydown', this.onKeyDown, true);

if (!this.props.currentUser.isLoggedIn) {
handleRequiredAuthentication();
}
}

componentWillUnmount() {
window.removeEventListener('keydown', this.onKeyDown, true);
this.mounted = false;
}

onKeyDown = (event /*: KeyboardEvent */) => {
// ESC key
if (event.keyCode === 27) {
this.finishOnboarding();
}
};

finishOnboarding = () => {
this.setState({ skipping: true });
skipOnboarding().then(
() => {
if (this.mounted) {
this.props.onFinish();

if (this.state.projectKey) {
this.context.router.push(getProjectUrl(this.state.projectKey));
}
}
},
() => {
if (this.mounted) {
this.setState({ skipping: false });
}
}
);
};

handleTimeout = () => {
// unset `projectKey` to display a generic "Finish this tutorial" button
this.setState({ projectKey: undefined });
};

handleTokenDone = (token /*: string */) => {
this.setState({ step: 'analysis', token });
};

handleOrganizationDone = (organization /*: string */) => {
this.setState({ organization, step: 'token' });
};

handleTokenOpen = () => this.setState({ step: 'token' });

handleOrganizationOpen = () => this.setState({ step: 'organization' });

handleSkipClick = (event /*: Event */) => {
event.preventDefault();
this.finishOnboarding();
};

handleFinish = (projectKey /*: string | void */) => this.setState({ finished: true, projectKey });

handleReset = () => this.setState({ finished: false, projectKey: undefined });

render() {
if (!this.props.currentUser.isLoggedIn) {
return null;
}

const { automatic, organizationsEnabled } = this.props;
const { step, token } = this.state;
let stepNumber = 1;

return (
<div className={this.props.className}>
<InstanceMessage message={translate('onboarding.header')}>
{transformedMessage => <Helmet title={transformedMessage} titleTemplate="%s" />}
</InstanceMessage>

<div className="page page-limited onboarding">
<header className="page-header">
<h1 className="page-title">
<InstanceMessage message={translate('onboarding.header')} />
</h1>
<div className="page-actions">
<DeferredSpinner loading={this.state.skipping}>
<a className="js-skip text-muted" href="#" onClick={this.handleSkipClick}>
{automatic ? translate('tutorials.skip') : translate('close')}
</a>
</DeferredSpinner>

<p className="note">
{translate(
isSonarCloud()
? 'tutorials.find_it_back_in_plus'
: 'tutorials.find_it_back_in_help'
)}
</p>
</div>
<div className="page-description">
{translateWithParameters(
'onboarding.header.description',
organizationsEnabled ? 3 : 2
)}
</div>
</header>

{organizationsEnabled && (
<OrganizationStep
currentUser={this.props.currentUser}
finished={this.state.organization != null}
onContinue={this.handleOrganizationDone}
onOpen={this.handleOrganizationOpen}
open={step === 'organization'}
stepNumber={stepNumber++}
/>
)}

<TokenStep
currentUser={this.props.currentUser}
finished={this.state.token != null}
onContinue={this.handleTokenDone}
onOpen={this.handleTokenOpen}
open={step === 'token'}
stepNumber={stepNumber++}
/>

<AnalysisStep
onFinish={this.handleFinish}
onReset={this.handleReset}
open={step === 'analysis'}
organization={this.state.organization}
stepNumber={stepNumber}
token={token}
/>

{this.state.finished &&
!this.state.skipping &&
(this.state.projectKey ? (
<ProjectWatcher
onFinish={this.finishOnboarding}
onTimeout={this.handleTimeout}
projectKey={this.state.projectKey}
/>
) : (
<footer className="text-right">
<a className="button" href="#" onClick={this.handleSkipClick}>
{translate('tutorials.finish')}
</a>
</footer>
))}
</div>
</div>
);
}
}

+ 0
- 33
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js View File

@@ -1,33 +0,0 @@
/*
* 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.
*/
// @flow
import { connect } from 'react-redux';
import Onboarding from './Onboarding';
import { getCurrentUser, areThereCustomOrganizations } from '../../../store/rootReducer';

const mapStateToProps = state => {
return {
className: 'modal-container',
currentUser: getCurrentUser(state),
organizationsEnabled: areThereCustomOrganizations(state)
};
};

export default connect(mapStateToProps)(Onboarding);

+ 0
- 388
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap View File

@@ -1,388 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`guides for on-premise 1`] = `
<div
className="modal-container"
>
<InstanceMessage
message="onboarding.header"
/>
<div
className="page page-limited onboarding"
>
<header
className="page-header"
>
<h1
className="page-title"
>
<InstanceMessage
message="onboarding.header"
/>
</h1>
<div
className="page-actions"
>
<DeferredSpinner
loading={false}
timeout={100}
>
<a
className="js-skip text-muted"
href="#"
onClick={[Function]}
>
close
</a>
</DeferredSpinner>
<p
className="note"
>
tutorials.find_it_back_in_help
</p>
</div>
<div
className="page-description"
>
onboarding.header.description.2
</div>
</header>
<TokenStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={false}
onContinue={[Function]}
onOpen={[Function]}
open={true}
stepNumber={1}
/>
<AnalysisStep
onFinish={[Function]}
onReset={[Function]}
open={false}
stepNumber={2}
/>
</div>
</div>
`;

exports[`guides for on-premise 2`] = `
<div
className="modal-container"
>
<InstanceMessage
message="onboarding.header"
/>
<div
className="page page-limited onboarding"
>
<header
className="page-header"
>
<h1
className="page-title"
>
<InstanceMessage
message="onboarding.header"
/>
</h1>
<div
className="page-actions"
>
<DeferredSpinner
loading={false}
timeout={100}
>
<a
className="js-skip text-muted"
href="#"
onClick={[Function]}
>
close
</a>
</DeferredSpinner>
<p
className="note"
>
tutorials.find_it_back_in_help
</p>
</div>
<div
className="page-description"
>
onboarding.header.description.2
</div>
</header>
<TokenStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={true}
onContinue={[Function]}
onOpen={[Function]}
open={false}
stepNumber={1}
/>
<AnalysisStep
onFinish={[Function]}
onReset={[Function]}
open={true}
stepNumber={2}
token="abcd1234"
/>
</div>
</div>
`;

exports[`guides for sonarcloud 1`] = `
<div>
<InstanceMessage
message="onboarding.header"
/>
<div
className="page page-limited onboarding"
>
<header
className="page-header"
>
<h1
className="page-title"
>
<InstanceMessage
message="onboarding.header"
/>
</h1>
<div
className="page-actions"
>
<DeferredSpinner
loading={false}
timeout={100}
>
<a
className="js-skip text-muted"
href="#"
onClick={[Function]}
>
close
</a>
</DeferredSpinner>
<p
className="note"
>
tutorials.find_it_back_in_plus
</p>
</div>
<div
className="page-description"
>
onboarding.header.description.3
</div>
</header>
<OrganizationStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={false}
onContinue={[Function]}
onOpen={[Function]}
open={true}
stepNumber={1}
/>
<TokenStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={false}
onContinue={[Function]}
onOpen={[Function]}
open={false}
stepNumber={2}
/>
<AnalysisStep
onFinish={[Function]}
onReset={[Function]}
open={false}
stepNumber={3}
/>
</div>
</div>
`;

exports[`guides for sonarcloud 2`] = `
<div>
<InstanceMessage
message="onboarding.header"
/>
<div
className="page page-limited onboarding"
>
<header
className="page-header"
>
<h1
className="page-title"
>
<InstanceMessage
message="onboarding.header"
/>
</h1>
<div
className="page-actions"
>
<DeferredSpinner
loading={false}
timeout={100}
>
<a
className="js-skip text-muted"
href="#"
onClick={[Function]}
>
close
</a>
</DeferredSpinner>
<p
className="note"
>
tutorials.find_it_back_in_plus
</p>
</div>
<div
className="page-description"
>
onboarding.header.description.3
</div>
</header>
<OrganizationStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={true}
onContinue={[Function]}
onOpen={[Function]}
open={false}
stepNumber={1}
/>
<TokenStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={false}
onContinue={[Function]}
onOpen={[Function]}
open={true}
stepNumber={2}
/>
<AnalysisStep
onFinish={[Function]}
onReset={[Function]}
open={false}
organization="my-org"
stepNumber={3}
/>
</div>
</div>
`;

exports[`guides for sonarcloud 3`] = `
<div>
<InstanceMessage
message="onboarding.header"
/>
<div
className="page page-limited onboarding"
>
<header
className="page-header"
>
<h1
className="page-title"
>
<InstanceMessage
message="onboarding.header"
/>
</h1>
<div
className="page-actions"
>
<DeferredSpinner
loading={false}
timeout={100}
>
<a
className="js-skip text-muted"
href="#"
onClick={[Function]}
>
close
</a>
</DeferredSpinner>
<p
className="note"
>
tutorials.find_it_back_in_plus
</p>
</div>
<div
className="page-description"
>
onboarding.header.description.3
</div>
</header>
<OrganizationStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={true}
onContinue={[Function]}
onOpen={[Function]}
open={false}
stepNumber={1}
/>
<TokenStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={true}
onContinue={[Function]}
onOpen={[Function]}
open={false}
stepNumber={2}
/>
<AnalysisStep
onFinish={[Function]}
onReset={[Function]}
open={true}
organization="my-org"
stepNumber={3}
token="abcd1234"
/>
</div>
</div>
`;

server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/AnalysisStep.tsx View File

@@ -17,11 +17,9 @@
* 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 * as React from 'react';
import Step from './Step';
import LanguageStep from './LanguageStep';
/*:: import type { Result } from './LanguageStep'; */
import LanguageStep, { Result } from './LanguageStep';
import JavaMaven from './commands/JavaMaven';
import JavaGradle from './commands/JavaGradle';
import DotNet from './commands/DotNet';
@@ -31,28 +29,23 @@ import Other from './commands/Other';
import { translate } from '../../../helpers/l10n';
import { getHostUrl } from '../../../helpers/urls';

/*::
type Props = {|
onFinish: (projectKey?: string) => void,
onReset: () => void,
open: boolean,
organization?: string,
stepNumber: number,
token: string
|};
*/

/*::
type State = {
result?: Result
};
*/

export default class AnalysisStep extends React.PureComponent {
/*:: props: Props; */
state /*: State */ = {};

handleLanguageSelect = (result /*: Result | void */) => {
interface Props {
onFinish: (projectKey?: string) => void;
onReset: () => void;
open: boolean;
organization?: string;
stepNumber: number;
token?: string;
}

interface State {
result?: Result;
}

export default class AnalysisStep extends React.PureComponent<Props, State> {
state: State = {};

handleLanguageSelect = (result?: Result) => {
this.setState({ result });
const projectKey = result && result.language !== 'java' ? result.projectKey : undefined;
this.props.onFinish(projectKey);
@@ -80,7 +73,7 @@ export default class AnalysisStep extends React.PureComponent {
);
};

renderFormattedCommand = (...lines /*: Array<string> */) => (
renderFormattedCommand = (...lines: Array<string>) => (
// keep this "useless" concatentation for the readability reason
// eslint-disable-next-line no-useless-concat
<pre>{lines.join(' ' + '\\' + '\n' + ' ')}</pre>
@@ -108,69 +101,87 @@ export default class AnalysisStep extends React.PureComponent {
}
};

renderCommandForMaven = () => (
<JavaMaven
host={getHostUrl()}
organization={this.props.organization}
token={this.props.token}
/>
);
renderCommandForMaven = () => {
const { token } = this.props;
if (!token) {
return null;
}
return <JavaMaven host={getHostUrl()} organization={this.props.organization} token={token} />;
};

renderCommandForGradle = () => (
<JavaGradle
host={getHostUrl()}
organization={this.props.organization}
token={this.props.token}
/>
);
renderCommandForGradle = () => {
const { token } = this.props;
if (!token) {
return null;
}
return <JavaGradle host={getHostUrl()} organization={this.props.organization} token={token} />;
};

renderCommandForDotNet = () => {
const { token } = this.props;
const { result } = this.state;
if (!result || !result.projectKey || !token) {
return null;
}
return (
<DotNet
host={getHostUrl()}
organization={this.props.organization}
// $FlowFixMe
projectKey={this.state.result.projectKey}
token={this.props.token}
projectKey={result.projectKey}
token={token}
/>
);
};

renderCommandForMSVC = () => {
const { token } = this.props;
const { result } = this.state;
if (!result || !result.projectKey || !token) {
return null;
}
return (
<Msvc
host={getHostUrl()}
organization={this.props.organization}
// $FlowFixMe
projectKey={this.state.result.projectKey}
token={this.props.token}
projectKey={result.projectKey}
token={token}
/>
);
};

renderCommandForClangGCC = () => (
<ClangGCC
host={getHostUrl()}
organization={this.props.organization}
// $FlowFixMe
os={this.state.result.os}
// $FlowFixMe
projectKey={this.state.result.projectKey}
token={this.props.token}
/>
);
renderCommandForClangGCC = () => {
const { token } = this.props;
const { result } = this.state;
if (!result || !result.projectKey || !result.os || !token) {
return null;
}
return (
<ClangGCC
host={getHostUrl()}
organization={this.props.organization}
os={result.os}
projectKey={result.projectKey}
token={token}
/>
);
};

renderCommandForOther = () => (
<Other
host={getHostUrl()}
organization={this.props.organization}
// $FlowFixMe
os={this.state.result.os}
// $FlowFixMe
projectKey={this.state.result.projectKey}
token={this.props.token}
/>
);
renderCommandForOther = () => {
const { token } = this.props;
const { result } = this.state;
if (!result || !result.projectKey || !result.os || !token) {
return null;
}
return (
<Other
host={getHostUrl()}
organization={this.props.organization}
os={result.os}
projectKey={result.projectKey}
token={token}
/>
);
};

renderResult = () => null;


server/sonar-web/src/main/js/apps/tutorials/onboarding/LanguageStep.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/LanguageStep.tsx View File

@@ -17,38 +17,31 @@
* 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 * as React from 'react';
import NewProjectForm from './NewProjectForm';
import RadioToggle from '../../../components/controls/RadioToggle';
import { translate } from '../../../helpers/l10n';
import { isSonarCloud } from '../../../helpers/system';

/*::
type Props = {|
onDone: (result: Result) => void,
onReset: () => void,
organization?: string,
|};
*/

/*::
type State = {
language?: string,
javaBuild?: string,
cFamilyCompiler?: string,
os?: string,
projectKey?: string
};
*/
export interface Result {
language?: string;
javaBuild?: string;
cFamilyCompiler?: string;
os?: string;
projectKey?: string;
}

/*::
export type Result = State; */
interface Props {
onDone: (result: Result) => void;
onReset: () => void;
organization?: string;
}

export default class LanguageStep extends React.PureComponent {
/*:: props: Props; */
type State = Result;

state /*: State */ = {};
export default class LanguageStep extends React.PureComponent<Props, State> {
state: State = {};

isConfigured = () => {
const { language, javaBuild, cFamilyCompiler, os, projectKey } = this.state;
@@ -69,23 +62,23 @@ export default class LanguageStep extends React.PureComponent {
}
};

handleLanguageChange = (language /*: string */) => {
handleLanguageChange = (language: string) => {
this.setState({ language }, this.handleChange);
};

handleJavaBuildChange = (javaBuild /*: string */) => {
handleJavaBuildChange = (javaBuild: string) => {
this.setState({ javaBuild }, this.handleChange);
};

handleCFamilyCompilerChange = (cFamilyCompiler /*: string */) => {
handleCFamilyCompilerChange = (cFamilyCompiler: string) => {
this.setState({ cFamilyCompiler }, this.handleChange);
};

handleOSChange = (os /*: string */) => {
handleOSChange = (os: string) => {
this.setState({ os }, this.handleChange);
};

handleProjectKeyDone = (projectKey /*: string */) => {
handleProjectKeyDone = (projectKey: string) => {
this.setState({ projectKey }, this.handleChange);
};


server/sonar-web/src/main/js/apps/tutorials/onboarding/NewOrganizationForm.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/NewOrganizationForm.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { debounce } from 'lodash';
import {
createOrganization,
@@ -29,29 +28,23 @@ import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon'
import { DeleteButton, SubmitButton } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';

/*::
type Props = {|
onDelete: () => void,
onDone: (organization: string) => void,
organization?: string
|};
*/
interface Props {
onDelete: () => void;
onDone: (organization: string) => void;
organization?: string;
}

/*::
type State = {
done: boolean,
loading: boolean,
organization: string,
unique: boolean
};
*/
interface State {
done: boolean;
loading: boolean;
organization: string;
unique: boolean;
}

export default class NewOrganizationForm extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
/*:: state: State; */
export default class NewOrganizationForm extends React.PureComponent<Props, State> {
mounted = false;

constructor(props /*: Props */) {
constructor(props: Props) {
super(props);
this.state = {
done: props.organization != null,
@@ -76,7 +69,7 @@ export default class NewOrganizationForm extends React.PureComponent {
}
};

validateOrganization = (organization /*: string */) => {
validateOrganization = (organization: string) => {
getOrganization(organization).then(response => {
if (this.mounted) {
this.setState({ unique: response == null });
@@ -84,19 +77,19 @@ export default class NewOrganizationForm extends React.PureComponent {
});
};

sanitizeOrganization = (organization /*: string */) =>
sanitizeOrganization = (organization: string) =>
organization
.toLowerCase()
.replace(/[^a-z0-9-]/, '')
.replace(/^-/, '');

handleOrganizationChange = (event /*: { target: HTMLInputElement } */) => {
handleOrganizationChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const organization = this.sanitizeOrganization(event.target.value);
this.setState({ organization });
this.validateOrganization(organization);
};

handleOrganizationCreate = (event /*: Event */) => {
handleOrganizationCreate = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { organization } = this.state;
if (organization) {

server/sonar-web/src/main/js/apps/tutorials/onboarding/NewProjectForm.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/NewProjectForm.tsx View File

@@ -17,35 +17,28 @@
* 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 * as React from 'react';
import { createProject, deleteProject } from '../../../api/components';
import { DeleteButton, SubmitButton } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';

/*::
type Props = {|
onDelete: () => void,
onDone: (projectKey: string) => void,
organization?: string,
projectKey?: string
|};
*/
interface Props {
onDelete: () => void;
onDone: (projectKey: string) => void;
organization?: string;
projectKey?: string;
}

/*::
type State = {
done: boolean,
loading: boolean,
projectKey: string
};
*/
interface State {
done: boolean;
loading: boolean;
projectKey: string;
}

export default class NewProjectForm extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
/*:: state: State; */
export default class NewProjectForm extends React.PureComponent<Props, State> {
mounted = false;

constructor(props /*: Props */) {
constructor(props: Props) {
super(props);
this.state = {
done: props.projectKey != null,
@@ -68,16 +61,20 @@ export default class NewProjectForm extends React.PureComponent {
}
};

sanitizeProjectKey = (projectKey /*: string */) => projectKey.replace(/[^-_a-zA-Z0-9.:]/, '');
sanitizeProjectKey = (projectKey: string) => projectKey.replace(/[^-_a-zA-Z0-9.:]/, '');

handleProjectKeyChange = (event /*: { target: HTMLInputElement } */) => {
handleProjectKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ projectKey: this.sanitizeProjectKey(event.target.value) });
};

handleProjectCreate = (event /*: Event */) => {
handleProjectCreate = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { projectKey } = this.state;
const data /*: { [string]: string } */ = {
const data: {
name: string;
project: string;
organization?: string;
} = {
name: projectKey,
project: projectKey
};

server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/OrganizationStep.tsx View File

@@ -17,9 +17,8 @@
* 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';
import * as React from 'react';
import * as classNames from 'classnames';
import { sortBy } from 'lodash';
import Step from './Step';
import NewOrganizationForm from './NewOrganizationForm';
@@ -30,32 +29,27 @@ import Select from '../../../components/controls/Select';
import { translate } from '../../../helpers/l10n';
import { Button } from '../../../components/ui/buttons';

/*::
type Props = {|
currentUser: { login: string, isLoggedIn: boolean },
finished: boolean,
onOpen: () => void,
onContinue: (organization: string) => void,
open: boolean,
stepNumber: number
|};
*/
interface Props {
currentUser: { login: string; isLoggedIn: boolean };
finished: boolean;
onOpen: () => void;
onContinue: (organization: string) => void;
open: boolean;
stepNumber: number;
}

/*::
type State = {
loading: boolean,
newOrganization?: string,
existingOrganization?: string,
existingOrganizations: Array<string>,
personalOrganization?: string,
selection: 'personal' | 'existing' | 'new'
};
*/
interface State {
loading: boolean;
newOrganization?: string;
existingOrganization?: string;
existingOrganizations: Array<string>;
personalOrganization?: string;
selection: 'personal' | 'existing' | 'new';
}

export default class OrganizationStep extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
state /*: State */ = {
export default class OrganizationStep extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
loading: true,
existingOrganizations: [],
selection: 'personal'
@@ -112,22 +106,22 @@ export default class OrganizationStep extends React.PureComponent {
}
};

handlePersonalClick = (event /*: Event */) => {
handlePersonalClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.setState({ selection: 'personal' });
};

handleExistingClick = (event /*: Event */) => {
handleExistingClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.setState({ selection: 'existing' });
};

handleNewClick = (event /*: Event */) => {
handleNewClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.setState({ selection: 'new' });
};

handleOrganizationCreate = (newOrganization /*: string */) => {
handleOrganizationCreate = (newOrganization: string) => {
this.setState({ newOrganization });
};

@@ -135,7 +129,7 @@ export default class OrganizationStep extends React.PureComponent {
this.setState({ newOrganization: undefined });
};

handleExistingOrganizationSelect = ({ value } /*: { value: string } */) => {
handleExistingOrganizationSelect = ({ value }: { value: string }) => {
this.setState({ existingOrganization: value });
};


+ 206
- 0
server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx View File

@@ -0,0 +1,206 @@
/*
* 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 PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import TokenStep from './TokenStep';
import OrganizationStep from './OrganizationStep';
import AnalysisStep from './AnalysisStep';
import ProjectWatcher from './ProjectWatcher';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
import { getCurrentUser, areThereCustomOrganizations } from '../../../store/rootReducer';
import { CurrentUser, isLoggedIn } from '../../../app/types';
import { ResetButtonLink } from '../../../components/ui/buttons';
import { getProjectUrl } from '../../../helpers/urls';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { isSonarCloud } from '../../../helpers/system';
import '../styles.css';

interface OwnProps {
automatic?: boolean;
onFinish: () => void;
}

interface StateProps {
currentUser: CurrentUser;
organizationsEnabled: boolean;
}

type Props = OwnProps & StateProps;

interface State {
finished: boolean;
organization?: string;
projectKey?: string;
step: string;
token?: string;
}

export class ProjectOnboarding extends React.PureComponent<Props, State> {
mounted = false;
static contextTypes = {
router: PropTypes.object
};

constructor(props: Props) {
super(props);
this.state = {
finished: false,
step: props.organizationsEnabled ? 'organization' : 'token'
};
}

componentDidMount() {
this.mounted = true;

// useCapture = true to receive the event before inputs
window.addEventListener('keydown', this.onKeyDown, true);

if (!isLoggedIn(this.props.currentUser)) {
handleRequiredAuthentication();
}
}

componentWillUnmount() {
window.removeEventListener('keydown', this.onKeyDown, true);
this.mounted = false;
}

onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
this.finishOnboarding();
}
};

finishOnboarding = () => {
this.props.onFinish();
if (this.state.projectKey) {
this.context.router.push(getProjectUrl(this.state.projectKey));
}
};

handleTimeout = () => {
// unset `projectKey` to display a generic "Finish this tutorial" button
this.setState({ projectKey: undefined });
};

handleTokenDone = (token: string) => {
this.setState({ step: 'analysis', token });
};

handleOrganizationDone = (organization: string) => {
this.setState({ organization, step: 'token' });
};

handleTokenOpen = () => this.setState({ step: 'token' });

handleOrganizationOpen = () => this.setState({ step: 'organization' });

handleFinish = (projectKey?: string) => this.setState({ finished: true, projectKey });

handleReset = () => this.setState({ finished: false, projectKey: undefined });

render() {
const { automatic, currentUser, organizationsEnabled } = this.props;
if (!isLoggedIn(currentUser)) {
return null;
}

const { finished, projectKey, step, token } = this.state;
const header = translate('onboarding.project.header');
let stepNumber = 1;

return (
<>
<Helmet title={header} titleTemplate="%s" />
<header className="modal-head">
<h2>{header}</h2>
</header>
<div className="modal-body modal-container">
<p className="spacer-top big-spacer-bottom">
{translateWithParameters(
'onboarding.project.header.description',
organizationsEnabled ? 3 : 2
)}
</p>
{organizationsEnabled && (
<OrganizationStep
currentUser={currentUser}
finished={this.state.organization != null}
onContinue={this.handleOrganizationDone}
onOpen={this.handleOrganizationOpen}
open={step === 'organization'}
stepNumber={stepNumber++}
/>
)}

<TokenStep
currentUser={currentUser}
finished={this.state.token != null}
onContinue={this.handleTokenDone}
onOpen={this.handleTokenOpen}
open={step === 'token'}
stepNumber={stepNumber++}
/>

<AnalysisStep
onFinish={this.handleFinish}
onReset={this.handleReset}
open={step === 'analysis'}
organization={this.state.organization}
stepNumber={stepNumber}
token={token}
/>
</div>
<footer className="modal-foot">
<ResetButtonLink className="js-skip" onClick={this.finishOnboarding}>
{(finished && translate('tutorials.finish')) ||
(automatic ? translate('tutorials.skip') : translate('close'))}
</ResetButtonLink>
{finished && projectKey ? (
<ProjectWatcher
onFinish={this.finishOnboarding}
onTimeout={this.handleTimeout}
projectKey={projectKey}
/>
) : (
<span className="pull-left note">
{translate(
isSonarCloud()
? 'tutorials.find_tutorial_back_in_plus'
: 'tutorials.find_tutorial_back_in_help'
)}
</span>
)}
</footer>
</>
);
}
}

const mapStateToProps = (state: any): StateProps => {
return {
currentUser: getCurrentUser(state),
organizationsEnabled: areThereCustomOrganizations(state)
};
};

export default connect<StateProps, {}, OwnProps>(mapStateToProps)(ProjectOnboarding);

server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingModal.tsx View File

@@ -27,12 +27,12 @@ interface Props {
onFinish: () => void;
}

const OnboardingContainer = lazyLoad(() => import('./OnboardingContainer'));
const ProjectOnboarding = lazyLoad(() => import('./ProjectOnboarding'));

export default function OnboardingModal(props: Props) {
export default function ProjectOnboardingModal(props: Props) {
return (
<Modal contentLabel={translate('tutorials.onboarding')} large={true}>
<OnboardingContainer {...props} />
<ProjectOnboarding {...props} />
</Modal>
);
}

server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboardingPage.tsx View File

@@ -20,14 +20,14 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { connect } from 'react-redux';
import OnboardingModal from './OnboardingModal';
import ProjectOnboardingModal from './ProjectOnboardingModal';
import { skipOnboarding } from '../../../store/users/actions';

interface DispatchProps {
skipOnboarding: () => void;
}

export class OnboardingPage extends React.PureComponent<DispatchProps> {
export class ProjectOnboardingPage extends React.PureComponent<DispatchProps> {
static contextTypes = {
router: PropTypes.object.isRequired
};
@@ -38,10 +38,10 @@ export class OnboardingPage extends React.PureComponent<DispatchProps> {
};

render() {
return <OnboardingModal onFinish={this.onSkipOnboardingTutorial} />;
return <ProjectOnboardingModal onFinish={this.onSkipOnboardingTutorial} />;
}
}

const mapDispatchToProps: DispatchProps = { skipOnboarding };

export default connect<{}, DispatchProps, {}>(null, mapDispatchToProps)(OnboardingPage);
export default connect<{}, DispatchProps>(null, mapDispatchToProps)(ProjectOnboardingPage);

server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectWatcher.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectWatcher.tsx View File

@@ -17,8 +17,9 @@
* 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 * as React from 'react';
import * as classNames from 'classnames';
import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon';
import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon';
import { getTasksForComponent } from '../../../api/ce';
@@ -28,35 +29,27 @@ import { translate } from '../../../helpers/l10n';
const INTERVAL = 5000;
const TIMEOUT = 10 * 60 * 1000; // 10 min

/*::
type Props = {
onFinish: () => void,
onTimeout: () => void,
projectKey: string
};
*/
interface Props {
onFinish: () => void;
onTimeout: () => void;
projectKey: string;
}

/*::
type State = {
inQueue: boolean,
status: ?string
};
*/
interface State {
inQueue: boolean;
status?: string;
}

export default class ProjectWatcher extends React.PureComponent {
/*:: interval: number; */
/*:: mounted: boolean; */
/*:: props: Props; */
/*:: timeout: number; */
state /*: State */ = {
inQueue: false,
status: null
};
export default class ProjectWatcher extends React.PureComponent<Props, State> {
interval?: number;
timeout?: number;
mounted = false;
state: State = { inQueue: false };

componentDidMount() {
this.mounted = true;
this.watch();
this.timeout = setTimeout(this.props.onTimeout, TIMEOUT);
this.timeout = window.setTimeout(this.props.onTimeout, TIMEOUT);
}

componentWillUnmount() {
@@ -65,7 +58,7 @@ export default class ProjectWatcher extends React.PureComponent {
this.mounted = false;
}

watch = () => (this.interval = setTimeout(this.checkProject, INTERVAL));
watch = () => (this.interval = window.setTimeout(this.checkProject, INTERVAL));

checkProject = () => {
const { projectKey } = this.props;
@@ -93,10 +86,11 @@ export default class ProjectWatcher extends React.PureComponent {

render() {
const { inQueue, status } = this.state;
const className = 'pull-left note';

if (status === STATUSES.SUCCESS) {
return (
<div className="big-spacer-top note text-center">
<div className={classNames(className, 'display-inline-flex-center')}>
<AlertSuccessIcon className="spacer-right" />
{translate('onboarding.project_watcher.finished')}
</div>
@@ -105,7 +99,7 @@ export default class ProjectWatcher extends React.PureComponent {

if (inQueue || status === STATUSES.PENDING || status === STATUSES.IN_PROGRESS) {
return (
<div className="big-spacer-top note text-center">
<div className={className}>
<i className="spinner spacer-right" />
{translate('onboarding.project_watcher.in_progress')}
</div>
@@ -114,17 +108,13 @@ export default class ProjectWatcher extends React.PureComponent {

if (status != null) {
return (
<div className="big-spacer-top note text-center">
<div className={classNames(className, 'display-inline-flex-center')}>
<AlertErrorIcon className="spacer-right" />
{translate('onboarding.project_watcher.failed')}
</div>
);
}

return (
<div className="big-spacer-top note text-center">
{translate('onboarding.project_watcher.not_started')}
</div>
);
return <div className={className}>{translate('onboarding.project_watcher.not_started')}</div>;
}
}

server/sonar-web/src/main/js/apps/tutorials/onboarding/Step.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/Step.tsx View File

@@ -17,23 +17,21 @@
* 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';
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/no-noninteractive-tabindex */
import * as React from 'react';
import * as classNames from 'classnames';

/*::
type Props = {|
finished: boolean,
onOpen: () => void,
open: boolean,
renderForm: () => React.Element<*>,
renderResult: () => ?React.Element<*>,
stepNumber: number,
stepTitle: React.Element<*> | string
|};
*/
interface Props {
finished: boolean;
onOpen: () => void;
open: boolean;
renderForm: () => React.ReactNode;
renderResult: () => React.ReactNode;
stepNumber: number;
stepTitle: React.ReactNode;
}

export default function Step(props /*: Props */) {
export default function Step(props: Props) {
const className = classNames('boxed-group', 'onboarding-step', {
'is-open': props.open,
'is-finished': props.finished
@@ -41,7 +39,7 @@ export default function Step(props /*: Props */) {

const clickable = !props.open && props.finished;

const handleClick = (event /*: Event */) => {
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.preventDefault();
props.onOpen();
};

server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/TokenStep.tsx View File

@@ -17,9 +17,9 @@
* 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';
import * as React from 'react';
import * as classNames from 'classnames';
import Step from './Step';
import { getTokens, generateToken, revokeToken } from '../../../api/user-tokens';
import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon';
@@ -27,32 +27,28 @@ import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessI
import { DeleteButton, SubmitButton, Button } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';

/*::
type Props = {|
currentUser: { login: string },
finished: boolean,
open: boolean,
onContinue: (token: string) => void,
onOpen: () => void,
stepNumber: number
|};
*/
interface Props {
currentUser: { login: string };
finished: boolean;
open: boolean;
onContinue: (token: string) => void;
onOpen: () => void;
stepNumber: number;
}

/*::
type State = {
canUseExisting: boolean,
existingToken: string,
loading: boolean,
selection: string,
tokenName?: string,
token?: string
};
*/
interface State {
canUseExisting: boolean;
existingToken: string;
loading: boolean;
selection: string;
tokenName?: string;
token?: string;
}

export default class TokenStep extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
state /*: State */ = {
export default class TokenStep extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
canUseExisting: false,
existingToken: '',
loading: false,
@@ -87,27 +83,20 @@ export default class TokenStep extends React.PureComponent {
);
};

handleTokenNameChange = (event /*: { target: HTMLInputElement } */) => {
handleTokenNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ tokenName: event.target.value });
};

handleTokenGenerate = (event /*: Event */) => {
handleTokenGenerate = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { tokenName } = this.state;
if (tokenName) {
this.setState({ loading: true });
generateToken({ name: tokenName }).then(
({ token }) => {
if (this.mounted) {
this.setState({ loading: false, token });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
generateToken({ name: tokenName }).then(({ token }) => {
if (this.mounted) {
this.setState({ loading: false, token });
}
);
}, this.stopLoading);
}
};

@@ -115,18 +104,11 @@ export default class TokenStep extends React.PureComponent {
const { tokenName } = this.state;
if (tokenName) {
this.setState({ loading: true });
revokeToken({ name: tokenName }).then(
() => {
if (this.mounted) {
this.setState({ loading: false, token: undefined, tokenName: undefined });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
revokeToken({ name: tokenName }).then(() => {
if (this.mounted) {
this.setState({ loading: false, token: undefined, tokenName: undefined });
}
);
}, this.stopLoading);
}
};

@@ -137,20 +119,26 @@ export default class TokenStep extends React.PureComponent {
}
};

handleGenerateClick = (event /*: Event */) => {
handleGenerateClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.setState({ selection: 'generate' });
};

handleUseExistingClick = (event /*: Event */) => {
handleUseExistingClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.setState({ selection: 'use-existing' });
};

handleExisingTokenChange = (event /*: { currentTarget: HTMLInputElement } */) => {
handleExisingTokenChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ existingToken: event.currentTarget.value });
};

stopLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};

renderGenerateOption = () => (
<div>
{this.state.canUseExisting ? (
@@ -163,10 +151,10 @@ export default class TokenStep extends React.PureComponent {
'is-checked': this.state.selection === 'generate'
})}
/>
{translate('onboading.token.generate_token')}
{translate('onboarding.token.generate_token')}
</a>
) : (
translate('onboading.token.generate_token')
translate('onboarding.token.generate_token')
)}
{this.state.selection === 'generate' && (
<div className="big-spacer-top">
@@ -175,7 +163,7 @@ export default class TokenStep extends React.PureComponent {
autoFocus={true}
className="input-large spacer-right text-middle"
onChange={this.handleTokenNameChange}
placeholder={translate('onboading.token.generate_token.placeholder')}
placeholder={translate('onboarding.token.generate_token.placeholder')}
required={true}
type="text"
value={this.state.tokenName || ''}

server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/LanguageStep-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/LanguageStep-test.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { shallow } from 'enzyme';
import LanguageStep from '../LanguageStep';
import { isSonarCloud } from '../../../../helpers/system';
@@ -26,29 +25,29 @@ import { isSonarCloud } from '../../../../helpers/system';
jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() }));

beforeEach(() => {
isSonarCloud.mockImplementation(() => false);
(isSonarCloud as jest.Mock<any>).mockImplementation(() => false);
});

it('selects java', () => {
const onDone = jest.fn();
const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />);

wrapper.find('RadioToggle').prop('onCheck')('java');
(wrapper.find('RadioToggle').prop('onCheck') as Function)('java');
wrapper.update();
expect(wrapper).toMatchSnapshot();

wrapper
(wrapper
.find('RadioToggle')
.at(1)
.prop('onCheck')('maven');
.prop('onCheck') as Function)('maven');
wrapper.update();
expect(wrapper).toMatchSnapshot();
expect(onDone).lastCalledWith({ language: 'java', javaBuild: 'maven' });

wrapper
(wrapper
.find('RadioToggle')
.at(1)
.prop('onCheck')('gradle');
.prop('onCheck') as Function)('gradle');
wrapper.update();
expect(wrapper).toMatchSnapshot();
expect(onDone).lastCalledWith({ language: 'java', javaBuild: 'gradle' });
@@ -58,52 +57,52 @@ it('selects c#', () => {
const onDone = jest.fn();
const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />);

wrapper.find('RadioToggle').prop('onCheck')('dotnet');
(wrapper.find('RadioToggle').prop('onCheck') as Function)('dotnet');
wrapper.update();
expect(wrapper).toMatchSnapshot();

wrapper.find('NewProjectForm').prop('onDone')('project-foo');
(wrapper.find('NewProjectForm').prop('onDone') as Function)('project-foo');
expect(onDone).lastCalledWith({ language: 'dotnet', projectKey: 'project-foo' });
});

it('selects c-family', () => {
isSonarCloud.mockImplementation(() => true);
(isSonarCloud as jest.Mock<any>).mockImplementation(() => true);
const onDone = jest.fn();
const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />);

wrapper.find('RadioToggle').prop('onCheck')('c-family');
(wrapper.find('RadioToggle').prop('onCheck') as Function)('c-family');
wrapper.update();
expect(wrapper).toMatchSnapshot();

wrapper
(wrapper
.find('RadioToggle')
.at(1)
.prop('onCheck')('msvc');
.prop('onCheck') as Function)('msvc');
wrapper.update();
expect(wrapper).toMatchSnapshot();

wrapper.find('NewProjectForm').prop('onDone')('project-foo');
(wrapper.find('NewProjectForm').prop('onDone') as Function)('project-foo');
expect(onDone).lastCalledWith({
language: 'c-family',
cFamilyCompiler: 'msvc',
projectKey: 'project-foo'
});

wrapper
(wrapper
.find('RadioToggle')
.at(1)
.prop('onCheck')('clang-gcc');
.prop('onCheck') as Function)('clang-gcc');
wrapper.update();
expect(wrapper).toMatchSnapshot();

wrapper
(wrapper
.find('RadioToggle')
.at(2)
.prop('onCheck')('linux');
.prop('onCheck') as Function)('linux');
wrapper.update();
expect(wrapper).toMatchSnapshot();

wrapper.find('NewProjectForm').prop('onDone')('project-foo');
(wrapper.find('NewProjectForm').prop('onDone') as Function)('project-foo');
expect(onDone).lastCalledWith({
language: 'c-family',
cFamilyCompiler: 'clang-gcc',
@@ -116,17 +115,17 @@ it('selects other', () => {
const onDone = jest.fn();
const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />);

wrapper.find('RadioToggle').prop('onCheck')('other');
(wrapper.find('RadioToggle').prop('onCheck') as Function)('other');
wrapper.update();
expect(wrapper).toMatchSnapshot();

wrapper
(wrapper
.find('RadioToggle')
.at(1)
.prop('onCheck')('mac');
.prop('onCheck') as Function)('mac');
wrapper.update();
expect(wrapper).toMatchSnapshot();

wrapper.find('NewProjectForm').prop('onDone')('project-foo');
(wrapper.find('NewProjectForm').prop('onDone') as Function)('project-foo');
expect(onDone).lastCalledWith({ language: 'other', os: 'mac', projectKey: 'project-foo' });
});

server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewOrganizationForm-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/NewOrganizationForm-test.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { mount } from 'enzyme';
import NewOrganizationForm from '../NewOrganizationForm';
import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils';
@@ -48,7 +47,7 @@ it('deletes organization', async () => {
const wrapper = mount(<NewOrganizationForm onDelete={onDelete} onDone={jest.fn()} />);
wrapper.setState({ done: true, loading: false, organization: 'foo' });
expect(wrapper).toMatchSnapshot();
wrapper.find('DeleteButton').prop('onClick')();
(wrapper.find('DeleteButton').prop('onClick') as Function)();
wrapper.update();
expect(wrapper).toMatchSnapshot(); // spinner
await waitAndUpdate(wrapper);

server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewProjectForm-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/NewProjectForm-test.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { mount } from 'enzyme';
import NewProjectForm from '../NewProjectForm';
import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils';
@@ -47,7 +46,7 @@ it('deletes project', async () => {
const wrapper = mount(<NewProjectForm onDelete={onDelete} onDone={jest.fn()} />);
wrapper.setState({ done: true, loading: false, projectKey: 'foo' });
expect(wrapper).toMatchSnapshot();
wrapper.find('DeleteButton').prop('onClick')();
(wrapper.find('DeleteButton').prop('onClick') as Function)();
wrapper.update();
expect(wrapper).toMatchSnapshot(); // spinner
await waitAndUpdate(wrapper);

server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/OrganizationStep-test.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { mount } from 'enzyme';
import OrganizationStep from '../OrganizationStep';
import { click, waitAndUpdate } from '../../../../helpers/testUtils';
@@ -35,7 +34,7 @@ jest.mock('../../../../api/organizations', () => ({
const currentUser = { isLoggedIn: true, login: 'user' };

beforeEach(() => {
getOrganizations.mockClear();
(getOrganizations as jest.Mock<any>).mockClear();
});

// FIXME
@@ -73,10 +72,10 @@ it('works with existing organization', async () => {
await waitAndUpdate(wrapper);
click(wrapper.find('.js-existing'));
expect(wrapper).toMatchSnapshot();
wrapper
(wrapper
.find('Select')
.first()
.prop('onChange')({ value: 'another' });
.prop('onChange') as Function)({ value: 'another' });
wrapper.update();
click(wrapper.find('[className="js-continue"]'));
expect(onContinue).toBeCalledWith('another');
@@ -96,7 +95,7 @@ it('works with new organization', async () => {
);
await waitAndUpdate(wrapper);
click(wrapper.find('.js-new'));
wrapper.find('NewOrganizationForm').prop('onDone')('new');
(wrapper.find('NewOrganizationForm').prop('onDone') as Function)('new');
wrapper.update();
click(wrapper.find('[className="js-continue"]'));
expect(onContinue).toBeCalledWith('new');

server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Onboarding-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectOnboarding-test.tsx View File

@@ -17,10 +17,9 @@
* 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 * as React from 'react';
import { shallow, mount } from 'enzyme';
import Onboarding from '../Onboarding';
import { ProjectOnboarding } from '../ProjectOnboarding';
import { click, doAsync } from '../../../../helpers/testUtils';
import { getInstance, isSonarCloud } from '../../../../helpers/system';

@@ -36,11 +35,10 @@ jest.mock('../../../../helpers/system', () => ({
const currentUser = { login: 'admin', isLoggedIn: true };

it('guides for on-premise', () => {
getInstance.mockImplementation(() => 'SonarQube');
isSonarCloud.mockImplementation(() => false);
(getInstance as jest.Mock<any>).mockImplementation(() => 'SonarQube');
(isSonarCloud as jest.Mock<any>).mockImplementation(() => false);
const wrapper = shallow(
<Onboarding
className="modal-container"
<ProjectOnboarding
currentUser={currentUser}
onFinish={jest.fn()}
organizationsEnabled={false}
@@ -48,39 +46,36 @@ it('guides for on-premise', () => {
);
expect(wrapper).toMatchSnapshot();

// $FlowFixMe
wrapper.instance().handleTokenDone('abcd1234');
(wrapper.instance() as ProjectOnboarding).handleTokenDone('abcd1234');
wrapper.update();
expect(wrapper).toMatchSnapshot();
});

it('guides for sonarcloud', () => {
getInstance.mockImplementation(() => 'SonarCloud');
isSonarCloud.mockImplementation(() => true);
(getInstance as jest.Mock<any>).mockImplementation(() => 'SonarCloud');
(isSonarCloud as jest.Mock<any>).mockImplementation(() => true);
const wrapper = shallow(
<Onboarding currentUser={currentUser} onFinish={jest.fn()} organizationsEnabled={true} />
<ProjectOnboarding currentUser={currentUser} onFinish={jest.fn()} organizationsEnabled={true} />
);
expect(wrapper).toMatchSnapshot();

// $FlowFixMe
wrapper.instance().handleOrganizationDone('my-org');
(wrapper.instance() as ProjectOnboarding).handleOrganizationDone('my-org');
wrapper.update();
expect(wrapper).toMatchSnapshot();

// $FlowFixMe
wrapper.instance().handleTokenDone('abcd1234');
(wrapper.instance() as ProjectOnboarding).handleTokenDone('abcd1234');
wrapper.update();
expect(wrapper).toMatchSnapshot();
});

it('finishes', () => {
getInstance.mockImplementation(() => 'SonarQube');
isSonarCloud.mockImplementation(() => false);
(getInstance as jest.Mock<any>).mockImplementation(() => 'SonarQube');
(isSonarCloud as jest.Mock<any>).mockImplementation(() => false);
const onFinish = jest.fn();
const wrapper = mount(
<Onboarding currentUser={currentUser} onFinish={onFinish} organizationsEnabled={false} />
<ProjectOnboarding currentUser={currentUser} onFinish={onFinish} organizationsEnabled={false} />
);
click(wrapper.find('.js-skip'));
click(wrapper.find('ResetButtonLink'));
return doAsync(() => {
expect(onFinish).toBeCalled();
});

server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectWatcher-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/ProjectWatcher-test.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { shallow, mount } from 'enzyme';
import ProjectWatcher from '../ProjectWatcher';


server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Step-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/Step-test.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { shallow } from 'enzyme';
import Step from '../Step';
import { click } from '../../../../helpers/testUtils';

server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/TokenStep-test.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { mount } from 'enzyme';
import TokenStep from '../TokenStep';
import { change, click, submit, waitAndUpdate } from '../../../../helpers/testUtils';
@@ -67,7 +66,7 @@ it('revokes token', async () => {
await new Promise(setImmediate);
wrapper.setState({ token: 'abcd1234', tokenName: 'my token' });
expect(wrapper).toMatchSnapshot();
wrapper.find('DeleteButton').prop('onClick')();
(wrapper.find('DeleteButton').prop('onClick') as Function)();
wrapper.update();
expect(wrapper).toMatchSnapshot(); // spinner
await waitAndUpdate(wrapper);

server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/LanguageStep-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/LanguageStep-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewOrganizationForm-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/NewOrganizationForm-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewProjectForm-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/NewProjectForm-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationStep-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/OrganizationStep-test.tsx.snap View File


+ 349
- 0
server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/ProjectOnboarding-test.tsx.snap View File

@@ -0,0 +1,349 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`guides for on-premise 1`] = `
<React.Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="onboarding.project.header"
titleTemplate="%s"
/>
<header
className="modal-head"
>
<h2>
onboarding.project.header
</h2>
</header>
<div
className="modal-body modal-container"
>
<p
className="spacer-top big-spacer-bottom"
>
onboarding.project.header.description.2
</p>
<TokenStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={false}
onContinue={[Function]}
onOpen={[Function]}
open={true}
stepNumber={1}
/>
<AnalysisStep
onFinish={[Function]}
onReset={[Function]}
open={false}
stepNumber={2}
/>
</div>
<footer
className="modal-foot"
>
<ResetButtonLink
className="js-skip"
onClick={[Function]}
>
close
</ResetButtonLink>
<span
className="pull-left note"
>
tutorials.find_tutorial_back_in_help
</span>
</footer>
</React.Fragment>
`;

exports[`guides for on-premise 2`] = `
<React.Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="onboarding.project.header"
titleTemplate="%s"
/>
<header
className="modal-head"
>
<h2>
onboarding.project.header
</h2>
</header>
<div
className="modal-body modal-container"
>
<p
className="spacer-top big-spacer-bottom"
>
onboarding.project.header.description.2
</p>
<TokenStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={true}
onContinue={[Function]}
onOpen={[Function]}
open={false}
stepNumber={1}
/>
<AnalysisStep
onFinish={[Function]}
onReset={[Function]}
open={true}
stepNumber={2}
token="abcd1234"
/>
</div>
<footer
className="modal-foot"
>
<ResetButtonLink
className="js-skip"
onClick={[Function]}
>
close
</ResetButtonLink>
<span
className="pull-left note"
>
tutorials.find_tutorial_back_in_help
</span>
</footer>
</React.Fragment>
`;

exports[`guides for sonarcloud 1`] = `
<React.Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="onboarding.project.header"
titleTemplate="%s"
/>
<header
className="modal-head"
>
<h2>
onboarding.project.header
</h2>
</header>
<div
className="modal-body modal-container"
>
<p
className="spacer-top big-spacer-bottom"
>
onboarding.project.header.description.3
</p>
<OrganizationStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={false}
onContinue={[Function]}
onOpen={[Function]}
open={true}
stepNumber={1}
/>
<TokenStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={false}
onContinue={[Function]}
onOpen={[Function]}
open={false}
stepNumber={2}
/>
<AnalysisStep
onFinish={[Function]}
onReset={[Function]}
open={false}
stepNumber={3}
/>
</div>
<footer
className="modal-foot"
>
<ResetButtonLink
className="js-skip"
onClick={[Function]}
>
close
</ResetButtonLink>
<span
className="pull-left note"
>
tutorials.find_tutorial_back_in_plus
</span>
</footer>
</React.Fragment>
`;

exports[`guides for sonarcloud 2`] = `
<React.Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="onboarding.project.header"
titleTemplate="%s"
/>
<header
className="modal-head"
>
<h2>
onboarding.project.header
</h2>
</header>
<div
className="modal-body modal-container"
>
<p
className="spacer-top big-spacer-bottom"
>
onboarding.project.header.description.3
</p>
<OrganizationStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={true}
onContinue={[Function]}
onOpen={[Function]}
open={false}
stepNumber={1}
/>
<TokenStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={false}
onContinue={[Function]}
onOpen={[Function]}
open={true}
stepNumber={2}
/>
<AnalysisStep
onFinish={[Function]}
onReset={[Function]}
open={false}
organization="my-org"
stepNumber={3}
/>
</div>
<footer
className="modal-foot"
>
<ResetButtonLink
className="js-skip"
onClick={[Function]}
>
close
</ResetButtonLink>
<span
className="pull-left note"
>
tutorials.find_tutorial_back_in_plus
</span>
</footer>
</React.Fragment>
`;

exports[`guides for sonarcloud 3`] = `
<React.Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="onboarding.project.header"
titleTemplate="%s"
/>
<header
className="modal-head"
>
<h2>
onboarding.project.header
</h2>
</header>
<div
className="modal-body modal-container"
>
<p
className="spacer-top big-spacer-bottom"
>
onboarding.project.header.description.3
</p>
<OrganizationStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={true}
onContinue={[Function]}
onOpen={[Function]}
open={false}
stepNumber={1}
/>
<TokenStep
currentUser={
Object {
"isLoggedIn": true,
"login": "admin",
}
}
finished={true}
onContinue={[Function]}
onOpen={[Function]}
open={false}
stepNumber={2}
/>
<AnalysisStep
onFinish={[Function]}
onReset={[Function]}
open={true}
organization="my-org"
stepNumber={3}
token="abcd1234"
/>
</div>
<footer
className="modal-foot"
>
<ResetButtonLink
className="js-skip"
onClick={[Function]}
>
close
</ResetButtonLink>
<span
className="pull-left note"
>
tutorials.find_tutorial_back_in_plus
</span>
</footer>
</React.Fragment>
`;

server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectWatcher-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/ProjectWatcher-test.tsx.snap View File

@@ -2,7 +2,7 @@

exports[`renders 1`] = `
<div
className="big-spacer-top note text-center"
className="pull-left note"
>
onboarding.project_watcher.not_started
</div>
@@ -10,7 +10,7 @@ exports[`renders 1`] = `

exports[`renders 2`] = `
<div
className="big-spacer-top note text-center"
className="pull-left note"
>
<i
className="spinner spacer-right"
@@ -21,7 +21,7 @@ exports[`renders 2`] = `

exports[`renders 3`] = `
<div
className="big-spacer-top note text-center"
className="pull-left note display-inline-flex-center"
>
<AlertSuccessIcon
className="spacer-right"
@@ -32,7 +32,7 @@ exports[`renders 3`] = `

exports[`renders 4`] = `
<div
className="big-spacer-top note text-center"
className="pull-left note"
>
<i
className="spinner spacer-right"

server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Step-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/Step-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/__tests__/__snapshots__/TokenStep-test.tsx.snap View File

@@ -50,7 +50,7 @@ exports[`generates token 1`] = `
<i
className="icon-radio spacer-right is-checked"
/>
onboading.token.generate_token
onboarding.token.generate_token
</a>
<div
className="big-spacer-top"
@@ -62,7 +62,7 @@ exports[`generates token 1`] = `
autoFocus={true}
className="input-large spacer-right text-middle"
onChange={[Function]}
placeholder="onboading.token.generate_token.placeholder"
placeholder="onboarding.token.generate_token.placeholder"
required={true}
type="text"
value=""
@@ -166,7 +166,7 @@ exports[`generates token 2`] = `
<i
className="icon-radio spacer-right is-checked"
/>
onboading.token.generate_token
onboarding.token.generate_token
</a>
<div
className="big-spacer-top"
@@ -178,7 +178,7 @@ exports[`generates token 2`] = `
autoFocus={true}
className="input-large spacer-right text-middle"
onChange={[Function]}
placeholder="onboading.token.generate_token.placeholder"
placeholder="onboarding.token.generate_token.placeholder"
required={true}
type="text"
value="my token"
@@ -582,7 +582,7 @@ exports[`revokes token 3`] = `
<i
className="icon-radio spacer-right is-checked"
/>
onboading.token.generate_token
onboarding.token.generate_token
</a>
<div
className="big-spacer-top"
@@ -594,7 +594,7 @@ exports[`revokes token 3`] = `
autoFocus={true}
className="input-large spacer-right text-middle"
onChange={[Function]}
placeholder="onboading.token.generate_token.placeholder"
placeholder="onboarding.token.generate_token.placeholder"
required={true}
type="text"
value=""

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/BuildWrapper.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/BuildWrapper.tsx View File

@@ -17,24 +17,22 @@
* 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 * as React from 'react';
import { translate } from '../../../../helpers/l10n';
import { getBaseUrl } from '../../../../helpers/urls';

/*::
type Props = {
className?: string,
os: string
};
*/
interface Props {
className?: string;
os: string;
}

const filenames = {
const filenames: { [key: string]: string } = {
win: 'build-wrapper-win-x86.zip',
linux: 'build-wrapper-linux-x86.zip',
mac: 'build-wrapper-macosx-x86.zip'
};

export default function BuildWrapper(props /*: Props */) {
export default function BuildWrapper(props: Props) {
return (
<div className={props.className}>
<h4 className="spacer-bottom">
@@ -50,7 +48,7 @@ export default function BuildWrapper(props /*: Props */) {
<a
className="button"
download={filenames[props.os]}
href={window.baseUrl + '/static/cpp/' + filenames[props.os]}
href={`${getBaseUrl()}/static/cpp/${filenames[props.os]}`}
target="_blank">
{translate('download_verb')}
</a>

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/ClangGCC.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/ClangGCC.tsx View File

@@ -17,31 +17,28 @@
* 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 * as React from 'react';
import SQScanner from './SQScanner';
import BuildWrapper from './BuildWrapper';
import CodeSnippet from '../../../../components/common/CodeSnippet';
import InstanceMessage from '../../../../components/common/InstanceMessage';
import { translate } from '../../../../helpers/l10n';

/*::
type Props = {
host: string,
os: string,
organization?: string,
projectKey: string,
token: string
};
*/
interface Props {
host: string;
os: string;
organization?: string;
projectKey: string;
token: string;
}

const executables = {
const executables: { [key: string]: string } = {
linux: 'build-wrapper-linux-x86-64',
win: 'build-wrapper-win-x86-64.exe',
mac: 'build-wrapper-macosx-x86'
};

export default function ClangGCC(props /*: Props */) {
export default function ClangGCC(props: Props) {
const command1 = `${executables[props.os]} --out-dir bw-output make clean all`;

const command2 = [

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/DotNet.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/DotNet.tsx View File

@@ -17,23 +17,20 @@
* 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 * as React from 'react';
import MSBuildScanner from './MSBuildScanner';
import CodeSnippet from '../../../../components/common/CodeSnippet';
import InstanceMessage from '../../../../components/common/InstanceMessage';
import { translate } from '../../../../helpers/l10n';

/*::
type Props = {|
host: string,
organization?: string,
projectKey: string,
token: string
|};
*/
interface Props {
host: string;
organization?: string;
projectKey: string;
token: string;
}

export default function DotNet(props /*: Props */) {
export default function DotNet(props: Props) {
const command1 = [
'SonarQube.Scanner.MSBuild.exe begin',
`/k:"${props.projectKey}"`,

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaGradle.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/JavaGradle.tsx View File

@@ -17,21 +17,18 @@
* 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 * as React from 'react';
import CodeSnippet from '../../../../components/common/CodeSnippet';
import InstanceMessage from '../../../../components/common/InstanceMessage';
import { translate } from '../../../../helpers/l10n';

/*::
type Props = {|
host: string,
organization?: string,
token: string
|};
*/
interface Props {
host: string;
organization?: string;
token: string;
}

export default function JavaGradle(props /*: Props */) {
export default function JavaGradle(props: Props) {
const config = 'plugins {\n id "org.sonarqube" version "2.6"\n}';

const command = [

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaMaven.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/JavaMaven.tsx View File

@@ -17,21 +17,18 @@
* 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 * as React from 'react';
import CodeSnippet from '../../../../components/common/CodeSnippet';
import InstanceMessage from '../../../../components/common/InstanceMessage';
import { translate } from '../../../../helpers/l10n';

/*::
type Props = {|
host: string,
organization?: string,
token: string
|};
*/
interface Props {
host: string;
organization?: string;
token: string;
}

export default function JavaMaven(props /*: Props */) {
export default function JavaMaven(props: Props) {
const command = [
'mvn sonar:sonar',
props.organization && `-Dsonar.organization=${props.organization}`,

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/MSBuildScanner.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/MSBuildScanner.tsx View File

@@ -17,17 +17,15 @@
* 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 * as React from 'react';
import { translate } from '../../../../helpers/l10n';

/*::
type Props = {
className?: string
};
*/
interface Props {
className?: string;
}

export default function MSBuildScanner(props /*: Props */) {
export default function MSBuildScanner(props: Props) {
return (
<div className={props.className}>
<h4 className="spacer-bottom">{translate('onboarding.analysis.msbuild.header')}</h4>
@@ -39,6 +37,7 @@ export default function MSBuildScanner(props /*: Props */) {
<a
className="button"
href="http://redirect.sonarsource.com/doc/install-configure-scanner-msbuild.html"
rel="noopener noreferrer"
target="_blank">
{translate('download_verb')}
</a>

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Msvc.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/Msvc.tsx View File

@@ -17,24 +17,21 @@
* 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 * as React from 'react';
import MSBuildScanner from './MSBuildScanner';
import BuildWrapper from './BuildWrapper';
import CodeSnippet from '../../../../components/common/CodeSnippet';
import InstanceMessage from '../../../../components/common/InstanceMessage';
import { translate } from '../../../../helpers/l10n';

/*::
type Props = {|
host: string,
organization?: string,
projectKey: string,
token: string
|};
*/
interface Props {
host: string;
organization?: string;
projectKey: string;
token: string;
}

export default function Msvc(props /*: Props */) {
export default function Msvc(props: Props) {
const command1 = [
'SonarQube.Scanner.MSBuild.exe begin',
`/k:"${props.projectKey}"`,

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Other.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/Other.tsx View File

@@ -17,24 +17,21 @@
* 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 * as React from 'react';
import SQScanner from './SQScanner';
import CodeSnippet from '../../../../components/common/CodeSnippet';
import InstanceMessage from '../../../../components/common/InstanceMessage';
import { translate } from '../../../../helpers/l10n';

/*::
type Props = {|
host: string,
organization?: string,
os: string,
projectKey: string,
token: string
|};
*/
interface Props {
host: string;
organization?: string;
os: string;
projectKey: string;
token: string;
}

export default function Other(props /*: Props */) {
export default function Other(props: Props) {
const command = [
props.os === 'win' ? 'sonar-scanner.bat' : 'sonar-scanner',
`-Dsonar.projectKey=${props.projectKey}`,

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/SQScanner.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/SQScanner.tsx View File

@@ -17,18 +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 React from 'react';
import * as React from 'react';
import { translate } from '../../../../helpers/l10n';

/*::
type Props = {
className?: string,
os: string
};
*/
interface Props {
className?: string;
os: string;
}

export default function SQScanner(props /*: Props */) {
export default function SQScanner(props: Props) {
return (
<div className={props.className}>
<h4 className="spacer-bottom">
@@ -44,6 +42,7 @@ export default function SQScanner(props /*: Props */) {
<a
className="button"
href="http://redirect.sonarsource.com/doc/install-configure-scanner.html"
rel="noopener noreferrer"
target="_blank">
{translate('download_verb')}
</a>

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/BuildWrapper-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/BuildWrapper-test.tsx View File

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { shallow } from 'enzyme';
import BuildWrapper from '../BuildWrapper';


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/ClangGCC-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/ClangGCC-test.tsx View File

@@ -17,8 +17,8 @@
* 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 * as React from 'react';
import { shallow } from 'enzyme';
import ClangGCC from '../ClangGCC';

@@ -35,8 +35,8 @@ it('renders correctly', () => {
shallow(
<ClangGCC
host="host"
os="linux"
organization="organization"
os="linux"
projectKey="projectKey"
token="token"
/>

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/DotNet-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/DotNet-test.tsx View File

@@ -17,8 +17,8 @@
* 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 * as React from 'react';
import { shallow } from 'enzyme';
import DotNet from '../DotNet';


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaGradle-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/JavaGradle-test.tsx View File

@@ -17,8 +17,8 @@
* 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 * as React from 'react';
import { shallow } from 'enzyme';
import JavaGradle from '../JavaGradle';


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaMaven-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/JavaMaven-test.tsx View File

@@ -17,8 +17,8 @@
* 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 * as React from 'react';
import { shallow } from 'enzyme';
import JavaMaven from '../JavaMaven';


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/MSBuildScanner-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/MSBuildScanner-test.tsx View File

@@ -17,8 +17,8 @@
* 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 * as React from 'react';
import { shallow } from 'enzyme';
import MSBuildScanner from '../MSBuildScanner';


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Msvc-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/Msvc-test.tsx View File

@@ -17,8 +17,8 @@
* 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 * as React from 'react';
import { shallow } from 'enzyme';
import Msvc from '../Msvc';


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Other-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/Other-test.tsx View File

@@ -17,8 +17,8 @@
* 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 * as React from 'react';
import { shallow } from 'enzyme';
import Other from '../Other';

@@ -35,8 +35,8 @@ it('renders correctly', () => {
shallow(
<Other
host="host"
os="linux"
organization="organization"
os="linux"
projectKey="projectKey"
token="token"
/>

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/SQScanner-test.js → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/SQScanner-test.tsx View File

@@ -17,8 +17,8 @@
* 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 * as React from 'react';
import { shallow } from 'enzyme';
import SQScanner from '../SQScanner';


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/BuildWrapper-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/BuildWrapper-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/ClangGCC-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/ClangGCC-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/DotNet-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/DotNet-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaGradle-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/JavaGradle-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaMaven-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/JavaMaven-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.tsx.snap View File

@@ -19,6 +19,7 @@ exports[`renders correctly 1`] = `
<a
className="button"
href="http://redirect.sonarsource.com/doc/install-configure-scanner-msbuild.html"
rel="noopener noreferrer"
target="_blank"
>
download_verb

server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Msvc-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/Msvc-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Other-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/Other-test.tsx.snap View File


server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/SQScanner-test.js.snap → server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/commands/__tests__/__snapshots__/SQScanner-test.tsx.snap View File

@@ -19,6 +19,7 @@ exports[`renders correctly 1`] = `
<a
className="button"
href="http://redirect.sonarsource.com/doc/install-configure-scanner.html"
rel="noopener noreferrer"
target="_blank"
>
download_verb
@@ -46,6 +47,7 @@ exports[`renders correctly 2`] = `
<a
className="button"
href="http://redirect.sonarsource.com/doc/install-configure-scanner.html"
rel="noopener noreferrer"
target="_blank"
>
download_verb
@@ -73,6 +75,7 @@ exports[`renders correctly 3`] = `
<a
className="button"
href="http://redirect.sonarsource.com/doc/install-configure-scanner.html"
rel="noopener noreferrer"
target="_blank"
>
download_verb

server/sonar-web/src/main/js/apps/tutorials/onboarding/styles.css → server/sonar-web/src/main/js/apps/tutorials/styles.css View File

@@ -17,9 +17,6 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.onboarding {
min-height: calc(70vh - 60px);
}

.onboarding-step {
position: relative;
@@ -58,12 +55,8 @@
outline: none;
}

.onboarding .page-actions {
text-align: right;
margin-bottom: 0;
}

.onboarding .page-actions p {
line-height: 16px;
margin-top: 6px;
.onboarding-choices {
display: flex;
justify-content: space-around;
padding: 24px 0 44px;
}

+ 68
- 0
server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/TeamOnboardingModal.tsx View File

@@ -0,0 +1,68 @@
/*
* 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 { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import Modal from '../../../components/controls/Modal';
import { translate } from '../../../helpers/l10n';
import { ResetButtonLink } from '../../../components/ui/buttons';

interface Props {
onFinish: () => void;
}

export default class TeamOnboardingModal extends React.PureComponent<Props> {
render() {
const header = translate('onboarding.team.header');
return (
<Modal
contentLabel={header}
medium={true}
onRequestClose={this.props.onFinish}
shouldCloseOnOverlayClick={false}>
<header className="modal-head">
<h2>{header}</h2>
</header>
<div className="modal-body">
<div className="alert alert-info modal-alert">
{translate('onboarding.team.work_in_progress')}
</div>
<p className="spacer-top big-spacer-bottom">{translate('onboarding.team.first_step')}</p>
<p className="spacer-top big-spacer-bottom">
<FormattedMessage
defaultMessage={translate('onboarding.team.how_to_join')}
id="onboarding.team.how_to_join"
values={{
link: (
<Link onClick={this.props.onFinish} to="/documentation/organizations/manage-team">
{translate('as_explained_here')}
</Link>
)
}}
/>
</p>
</div>
<footer className="modal-foot">
<ResetButtonLink onClick={this.props.onFinish}>{translate('close')}</ResetButtonLink>
</footer>
</Modal>
);
}
}

server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts → server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/TeamOnboardingModal-test.tsx View File

@@ -18,9 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import TeamOnboardingModal from '../TeamOnboardingModal';

export interface Props {
onFinish: () => void;
}

export default class OnboardingModal extends React.PureComponent<Props> {}
it('renders correctly', () => {
expect(shallow(<TeamOnboardingModal onFinish={jest.fn()} />)).toMatchSnapshot();
});

+ 61
- 0
server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/__snapshots__/TeamOnboardingModal-test.tsx.snap View File

@@ -0,0 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly 1`] = `
<Modal
contentLabel="onboarding.team.header"
medium={true}
onRequestClose={[MockFunction]}
shouldCloseOnOverlayClick={false}
>
<header
className="modal-head"
>
<h2>
onboarding.team.header
</h2>
</header>
<div
className="modal-body"
>
<div
className="alert alert-info modal-alert"
>
onboarding.team.work_in_progress
</div>
<p
className="spacer-top big-spacer-bottom"
>
onboarding.team.first_step
</p>
<p
className="spacer-top big-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.team.how_to_join"
id="onboarding.team.how_to_join"
values={
Object {
"link": <Link
onClick={[MockFunction]}
onlyActiveOnIndex={false}
style={Object {}}
to="/documentation/organizations/manage-team"
>
as_explained_here
</Link>,
}
}
/>
</p>
</div>
<footer
className="modal-foot"
>
<ResetButtonLink
onClick={[MockFunction]}
>
close
</ResetButtonLink>
</footer>
</Modal>
`;

+ 2
- 2
server/sonar-web/src/main/js/helpers/testUtils.ts View File

@@ -44,13 +44,13 @@ export function clickOutside(event = {}): void {
window.dispatchEvent(dispatchedEvent);
}

export function submit(element: ShallowWrapper): void {
export function submit(element: ShallowWrapper | ReactWrapper): void {
element.simulate('submit', {
preventDefault() {}
});
}

export function change(element: ShallowWrapper, value: string, event = {}): void {
export function change(element: ShallowWrapper | ReactWrapper, value: string, event = {}): void {
element.simulate('change', {
target: { value },
currentTarget: { value },

+ 22
- 6
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -202,6 +202,7 @@ no=No

and_worse=and worse
are_you_sure=Are you sure?
as_explained_here=as explained here
assigned_to=Assigned to
bulk_change=Bulk Change
bulleted_point=Bulleted point
@@ -921,8 +922,8 @@ shortcuts.section.rules.deactivate=deactivate selected rule
tutorials.onboarding=Analyze a new project
tutorials.skip=Skip this tutorial
tutorials.finish=Finish this tutorial
tutorials.find_it_back_in_help=Find it back anytime in the Help section
tutorials.find_it_back_in_plus=Find it back anytime in the "+" menu
tutorials.find_tutorial_back_in_help=Find this tutorial back anytime in the Help section
tutorials.find_tutorial_back_in_plus=Find this tutorial back anytime in the "+" menu


#------------------------------------------------------------------------------
@@ -2589,15 +2590,30 @@ footer.web_api=Web API
# ONBOARDING
#
#------------------------------------------------------------------------------
onboarding.header=Welcome to {instance}!
onboarding.header.description=Want to quickly analyze a first project? Follow these {0} easy steps.
onboarding.header=Welcome to SonarCloud!
onboarding.header.description=Let us help you get started. What do you want to do?

onboarding.project.header=Analyze a project
onboarding.project.header.description=Want to quickly analyze a first project? Follow these {0} easy steps.

onboarding.team.header=Join a team
onboarding.team.first_step=Well congrats, the first step is done!
onboarding.team.how_to_join=To join a team, the only thing you need to do is to be a user registered on Sonarcloud. The administrator of the Sonarcloud organization you wish to join has to add you to his organization's members {link}. Ask him to do so!
onboarding.team.work_in_progress=We are currently working on a better way to join a team or invite people to yours.

onboarding.analyze_public_code=I want to analyze public code
onboarding.analyze_public_code.button=Analyze a project
onboarding.analyze_private_code=I want to analyze private code
onboarding.analyze_private_code.button=Setup a new organization
onboarding.contribute_existing_project=I want to contribute to an existing project
onboarding.contribute_existing_project.button=Join a team

onboarding.token.header=Provide a token
onboarding.token.text=The token is used to identify you when an analysis is performed. If it has been compromised, you can revoke it at any point of time in your user account.
onboarding.token.generate=Generate
onboarding.token.placeholder=Enter a name for your token
onboading.token.generate_token=Generate a token
onboading.token.generate_token.placeholder=Enter a name for your token
onboarding.token.generate_token=Generate a token
onboarding.token.generate_token.placeholder=Enter a name for your token
onboarding.token.use_existing_token=Use existing token
onboarding.token.use_existing_token.placeholder=Enter your existing token
onboarding.token.invalid_format=The token you have entered has invalid format.

Loading…
Cancel
Save