diff options
Diffstat (limited to 'server')
81 files changed, 6094 insertions, 286 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js index 867100c4fae..e024ce947f5 100644 --- a/server/sonar-web/src/main/js/api/components.js +++ b/server/sonar-web/src/main/js/api/components.js @@ -19,6 +19,7 @@ */ // @flow import { getJSON, postJSON, post } from '../helpers/request'; +import throwGlobalError from '../app/utils/throwGlobalError'; export function getComponents(data?: Object) { const url = '/api/projects/search'; @@ -55,7 +56,7 @@ export function createProject( } ) { const url = '/api/projects/create'; - return postJSON(url, data); + return postJSON(url, data).catch(throwGlobalError); } export function searchProjectTags(data?: { ps?: number, q?: string }) { diff --git a/server/sonar-web/src/main/js/api/organizations.js b/server/sonar-web/src/main/js/api/organizations.js index 097a17ce5cb..7ca0263ba81 100644 --- a/server/sonar-web/src/main/js/api/organizations.js +++ b/server/sonar-web/src/main/js/api/organizations.js @@ -20,6 +20,7 @@ // @flow import { getJSON, post, postJSON } from '../helpers/request'; import type { Organization } from '../store/organizations/duck'; +import throwGlobalError from '../app/utils/throwGlobalError'; export const getOrganizations = (organizations?: Array<string>) => { const data = {}; @@ -44,7 +45,9 @@ type GetOrganizationNavigation = { }; export const getOrganization = (key: string): Promise<GetOrganizationType> => { - return getOrganizations([key]).then(r => r.organizations.find(o => o.key === key)); + return getOrganizations([key]) + .then(r => r.organizations.find(o => o.key === key)) + .catch(throwGlobalError); }; export const getOrganizationNavigation = (key: string): Promise<GetOrganizationNavigation> => { @@ -52,12 +55,13 @@ export const getOrganizationNavigation = (key: string): Promise<GetOrganizationN }; export const createOrganization = (fields: {}): Promise<Organization> => - postJSON('/api/organizations/create', fields).then(r => r.organization); + postJSON('/api/organizations/create', fields).then(r => r.organization, throwGlobalError); export const updateOrganization = (key: string, changes: {}) => post('/api/organizations/update', { key, ...changes }); -export const deleteOrganization = (key: string) => post('/api/organizations/delete', { key }); +export const deleteOrganization = (key: string) => + post('/api/organizations/delete', { key }).catch(throwGlobalError); export const searchMembers = ( data: { organization?: string, p?: number, ps?: number, q?: string, selected?: string } diff --git a/server/sonar-web/src/main/js/api/user-tokens.js b/server/sonar-web/src/main/js/api/user-tokens.js index 4b3b97e2bba..3947b07f1be 100644 --- a/server/sonar-web/src/main/js/api/user-tokens.js +++ b/server/sonar-web/src/main/js/api/user-tokens.js @@ -17,14 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +// @flow import { getJSON, postJSON, post } from '../helpers/request'; +import throwGlobalError from '../app/utils/throwGlobalError'; /** * List tokens for given user login * @param {string} login * @returns {Promise} */ -export function getTokens(login) { +export function getTokens(login: string) { const url = '/api/user_tokens/search'; const data = { login }; return getJSON(url, data).then(r => r.userTokens); @@ -36,10 +38,16 @@ export function getTokens(login) { * @param {string} tokenName * @returns {Promise} */ -export function generateToken(userLogin, tokenName) { +export function generateToken( + tokenName: string, + userLogin?: string +): Promise<{ name: string, token: string }> { const url = '/api/user_tokens/generate'; - const data = { login: userLogin, name: tokenName }; - return postJSON(url, data); + const data: { [string]: string } = { name: tokenName }; + if (userLogin) { + data.login = userLogin; + } + return postJSON(url, data).catch(throwGlobalError); } /** @@ -48,8 +56,11 @@ export function generateToken(userLogin, tokenName) { * @param {string} tokenName * @returns {Promise} */ -export function revokeToken(userLogin, tokenName) { +export function revokeToken(tokenName: string, userLogin?: string) { const url = '/api/user_tokens/revoke'; - const data = { login: userLogin, name: tokenName }; - return post(url, data); + const data: { [string]: string } = { name: tokenName }; + if (userLogin) { + data.login = userLogin; + } + return post(url, data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooterForSonarQubeDotCom.js b/server/sonar-web/src/main/js/app/components/GlobalFooterForSonarQubeDotCom.js index 755a34e933d..ab8ec94f97a 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooterForSonarQubeDotCom.js +++ b/server/sonar-web/src/main/js/app/components/GlobalFooterForSonarQubeDotCom.js @@ -43,7 +43,7 @@ export default function GlobalFooterForSonarQubeDotCom() { {' - '} <a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a> {' - '} - {<Link to="/about">{translate('footer.about')}</Link>} + <Link to="/about">{translate('footer.about')}</Link> </div> </div> ); diff --git a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js new file mode 100644 index 00000000000..553f5e3cc71 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js @@ -0,0 +1,118 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import classNames from 'classnames'; +import LinksHelp from './LinksHelp'; +import LinksHelpSonarCloud from './LinksHelpSonarCloud'; +import ShortcutsHelp from './ShortcutsHelp'; +import TutorialsHelp from './TutorialsHelp'; +import { translate } from '../../../helpers/l10n'; + +type Props = { + onClose: () => void, + sonarCloud?: boolean +}; + +type State = { + section: string +}; + +export default class GlobalHelp extends React.PureComponent { + props: Props; + state: State = { section: 'shortcuts' }; + + handleCloseClick = (event: Event) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleSectionClick = (event: Event & { currentTarget: HTMLElement }) => { + event.preventDefault(); + const { section } = event.currentTarget.dataset; + this.setState({ section }); + }; + + renderSection = () => { + switch (this.state.section) { + case 'shortcuts': + return <ShortcutsHelp />; + case 'links': + return this.props.sonarCloud + ? <LinksHelpSonarCloud onClose={this.props.onClose} /> + : <LinksHelp onClose={this.props.onClose} />; + case 'tutorials': + return <TutorialsHelp onClose={this.props.onClose} />; + default: + return null; + } + }; + + renderMenuItem = (section: string) => ( + <li key={section}> + <a + className={classNames({ active: section === this.state.section })} + data-section={section} + href="#" + onClick={this.handleSectionClick}> + {translate('help.section', section)} + </a> + </li> + ); + + renderMenu = () => ( + <ul className="side-tabs-menu"> + {['shortcuts', 'tutorials', 'links'].map(this.renderMenuItem)} + </ul> + ); + + render() { + return ( + <Modal + isOpen={true} + contentLabel={translate('help')} + className="modal modal-medium" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + + <div className="modal-head"> + <h2>{translate('help')}</h2> + </div> + + <div className="side-tabs-layout"> + <div className="side-tabs-side"> + {this.renderMenu()} + </div> + <div className="side-tabs-main"> + {this.renderSection()} + </div> + </div> + + <div className="modal-foot"> + <a className="js-modal-close" href="#" onClick={this.handleCloseClick}> + {translate('close')} + </a> + </div> + + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/help/LinksHelp.js b/server/sonar-web/src/main/js/app/components/help/LinksHelp.js new file mode 100644 index 00000000000..e76ef2c6bb6 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/help/LinksHelp.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { Link } from 'react-router'; +import { translate } from '../../../helpers/l10n'; + +type Props = { onClose: () => void }; + +export default function LinksHelp({ onClose }: Props) { + return ( + <div> + <h2 className="spacer-top spacer-bottom">{translate('help.section.links')}</h2> + + <a href="http://www.sonarqube.org">{translate('footer.community')}</a>{' - '} + <a href="https://redirect.sonarsource.com/doc/home.html"> + {translate('footer.documentation')} + </a> + {' - '} + <a href="https://redirect.sonarsource.com/doc/community.html"> + {translate('footer.support')} + </a> + {' - '} + <a href="https://redirect.sonarsource.com/doc/plugin-library.html"> + {translate('footer.plugins')} + </a> + {' - '} + <Link to="/web_api" onClick={onClose}>{translate('footer.web_api')}</Link> + {' - '} + <Link to="/about" onClick={onClose}>{translate('footer.about')}</Link> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js b/server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js new file mode 100644 index 00000000000..6efdac91b50 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { Link } from 'react-router'; +import { translate } from '../../../helpers/l10n'; + +type Props = { onClose: () => void }; + +export default function LinksHelpSonarCloud({ onClose }: Props) { + return ( + <div> + <h2 className="spacer-top spacer-bottom">{translate('help.section.links')}</h2> + + <a href="https://about.sonarcloud.io/news/">{translate('footer.news')}</a> + {' - '} + <a href="https://about.sonarcloud.io/terms.pdf">{translate('footer.terms')}</a> + {' - '} + <a href="https://twitter.com/sonarqube">{translate('footer.twitter')}</a> + {' - '} + <a href="https://about.sonarcloud.io/get-started/">{translate('footer.get_started')}</a> + {' - '} + <a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a> + {' - '} + <Link to="/about" onClick={onClose}>{translate('footer.about')}</Link> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js b/server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js new file mode 100644 index 00000000000..1787524f5dc --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js @@ -0,0 +1,135 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default function ShortcutsHelp() { + return ( + <div> + <h2 className="spacer-top spacer-bottom">{translate('help.section.shortcuts')}</h2> + + <div className="columns"> + <div className="column-half"> + <div className="spacer-bottom"> + <h3 className="shortcuts-section-title">{translate('shortcuts.section.global')}</h3> + <ul className="shortcuts-list"> + <li> + <span className="shortcut-button spacer-right">s</span> + {translate('shortcuts.section.global.search')} + </li> + <li> + <span className="shortcut-button spacer-right">?</span> + {translate('shortcuts.section.global.shortcuts')} + </li> + </ul> + </div> + + <h3 className="shortcuts-section-title">{translate('shortcuts.section.rules')}</h3> + <ul className="shortcuts-list"> + <li> + <span className="shortcut-button little-spacer-right">↑</span> + <span className="shortcut-button spacer-right">↓</span> + {translate('shortcuts.section.rules.navigate_between_rules')} + </li> + <li> + <span className="shortcut-button spacer-right">→</span> + {translate('shortcuts.section.rules.open_details')} + </li> + <li> + <span className="shortcut-button spacer-right">←</span> + {translate('shortcuts.section.rules.return_to_list')} + </li> + <li> + <span className="shortcut-button spacer-right">a</span> + {translate('shortcuts.section.rules.activate')} + </li> + <li> + <span className="shortcut-button spacer-right">d</span> + {translate('shortcuts.section.rules.deactivate')} + </li> + </ul> + </div> + + <div className="column-half"> + <h3 className="shortcuts-section-title">{translate('shortcuts.section.issues')}</h3> + <ul className="shortcuts-list"> + <li> + <span className="shortcut-button little-spacer-right">↑</span> + <span className="shortcut-button spacer-right">↓</span> + {translate('shortcuts.section.issues.navigate_between_issues')} + </li> + <li> + <span className="shortcut-button spacer-right">→</span> + {translate('shortcuts.section.issues.open_details')} + </li> + <li> + <span className="shortcut-button spacer-right">←</span> + {translate('shortcuts.section.issues.return_to_list')} + </li> + <li> + <span className="shortcut-button little-spacer-right">alt</span> + <span className="little-spacer-right">+</span> + <span className="shortcut-button little-spacer-right">↑</span> + <span className="shortcut-button spacer-right">↓</span> + {translate('issues.to_navigate_issue_locations')} + </li> + <li> + <span className="shortcut-button little-spacer-right">alt</span> + <span className="little-spacer-right">+</span> + <span className="shortcut-button little-spacer-right">←</span> + <span className="shortcut-button spacer-right">→</span> + {translate('issues.to_switch_flows')} + </li> + <li> + <span className="shortcut-button spacer-right">f</span> + {translate('shortcuts.section.issue.do_transition')} + </li> + <li> + <span className="shortcut-button spacer-right">a</span> + {translate('shortcuts.section.issue.assign')} + </li> + <li> + <span className="shortcut-button spacer-right">m</span> + {translate('shortcuts.section.issue.assign_to_me')} + </li> + <li> + <span className="shortcut-button spacer-right">i</span> + {translate('shortcuts.section.issue.change_severity')} + </li> + <li> + <span className="shortcut-button spacer-right">c</span> + {translate('shortcuts.section.issue.comment')} + </li> + <li> + <span className="shortcut-button little-spacer-right">ctrl</span> + <span className="shortcut-button spacer-right">enter</span> + {translate('shortcuts.section.issue.submit_comment')} + </li> + <li> + <span className="shortcut-button spacer-right">t</span> + {translate('shortcuts.section.issue.change_tags')} + </li> + </ul> + </div> + </div> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js b/server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js new file mode 100644 index 00000000000..112b6393361 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { Link } from 'react-router'; +import { translate } from '../../../helpers/l10n'; + +type Props = { onClose: () => void }; + +export default function TutorialsHelp({ onClose }: Props) { + return ( + <div> + <h2 className="spacer-top spacer-bottom">{translate('help.section.tutorials')}</h2> + <Link to="/tutorials/onboarding" onClick={onClose}>Onboarding Tutorial</Link> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js b/server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js new file mode 100644 index 00000000000..ff0a3dd6bef --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import GlobalHelp from '../GlobalHelp'; +import { click } from '../../../../helpers/testUtils'; + +it('switches between tabs', () => { + const wrapper = shallow(<GlobalHelp onClose={jest.fn()} />); + expect(wrapper.find('ShortcutsHelp')).toHaveLength(1); + clickOnSection(wrapper, 'links'); + expect(wrapper.find('LinksHelp')).toHaveLength(1); + clickOnSection(wrapper, 'tutorials'); + expect(wrapper.find('TutorialsHelp')).toHaveLength(1); + clickOnSection(wrapper, 'shortcuts'); + expect(wrapper.find('ShortcutsHelp')).toHaveLength(1); +}); + +function clickOnSection(wrapper: Object, section: string) { + click(wrapper.find(`[data-section="${section}"]`), { currentTarget: { dataset: { section } } }); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js index 2e2659d7125..8d432d498a0 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js @@ -23,8 +23,8 @@ import GlobalNavBranding from './GlobalNavBranding'; import GlobalNavMenu from './GlobalNavMenu'; import GlobalNavUserContainer from './GlobalNavUserContainer'; import Search from '../../search/Search'; -import ShortcutsHelp from './ShortcutsHelp'; -import { getCurrentUser, getAppState } from '../../../../store/rootReducer'; +import GlobalHelp from '../../help/GlobalHelp'; +import { getCurrentUser, getAppState, getSettingValue } from '../../../../store/rootReducer'; class GlobalNav extends React.PureComponent { state = { helpOpen: false }; @@ -84,15 +84,21 @@ class GlobalNav extends React.PureComponent { </ul> </div> - {this.state.helpOpen && <ShortcutsHelp onClose={this.closeHelp} />} + {this.state.helpOpen && + <GlobalHelp onClose={this.closeHelp} sonarCloud={this.props.sonarCloud} />} </nav> ); } } -const mapStateToProps = state => ({ - currentUser: getCurrentUser(state), - appState: getAppState(state) -}); +const mapStateToProps = state => { + const sonarCloudSetting = getSettingValue(state, 'sonar.lf.sonarqube.com.enabled'); + + return { + currentUser: getCurrentUser(state), + appState: getAppState(state), + sonarCloud: sonarCloudSetting != null && sonarCloudSetting.value === 'true' + }; +}; export default connect(mapStateToProps)(GlobalNav); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelp.js b/server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelp.js deleted file mode 100644 index 273b370f17a..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/global/ShortcutsHelp.js +++ /dev/null @@ -1,188 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import React from 'react'; -import Modal from 'react-modal'; -import { Link } from 'react-router'; -import { translate } from '../../../../helpers/l10n'; - -type Props = { - onClose: () => void -}; - -export default class ShortcutsHelp extends React.PureComponent { - props: Props; - - handleCloseClick = (event: Event) => { - event.preventDefault(); - this.props.onClose(); - }; - - render() { - return ( - <Modal - isOpen={true} - contentLabel="shortcuts help" - className="modal modal-large" - overlayClassName="modal-overlay" - onRequestClose={this.props.onClose}> - - <div className="modal-head"> - <h2>{translate('help')}</h2> - </div> - - <div className="modal-body modal-container"> - <div className="spacer-bottom"> - <a href="http://www.sonarqube.org">{translate('footer.community')}</a>{' - '} - <a href="https://redirect.sonarsource.com/doc/home.html"> - {translate('footer.documentation')} - </a> - {' - '} - <a href="https://redirect.sonarsource.com/doc/community.html"> - {translate('footer.support')} - </a> - {' - '} - <a href="https://redirect.sonarsource.com/doc/plugin-library.html"> - {translate('footer.plugins')} - </a> - {' - '} - <Link to="/web_api" onClick={this.props.onClose}>{translate('footer.web_api')}</Link> - {' - '} - <Link to="/about" onClick={this.props.onClose}>{translate('footer.about')}</Link> - </div> - - <h2 className="spacer-top spacer-bottom">{translate('shortcuts.modal_title')}</h2> - - <div className="columns"> - <div className="column-half"> - <div className="spacer-bottom"> - <h3 className="shortcuts-section-title">{translate('shortcuts.section.global')}</h3> - <ul className="shortcuts-list"> - <li> - <span className="shortcut-button spacer-right">s</span> - {translate('shortcuts.section.global.search')} - </li> - <li> - <span className="shortcut-button spacer-right">?</span> - {translate('shortcuts.section.global.shortcuts')} - </li> - </ul> - </div> - - <h3 className="shortcuts-section-title">{translate('shortcuts.section.rules')}</h3> - <ul className="shortcuts-list"> - <li> - <span className="shortcut-button little-spacer-right">↑</span> - <span className="shortcut-button spacer-right">↓</span> - {translate('shortcuts.section.rules.navigate_between_rules')} - </li> - <li> - <span className="shortcut-button spacer-right">→</span> - {translate('shortcuts.section.rules.open_details')} - </li> - <li> - <span className="shortcut-button spacer-right">←</span> - {translate('shortcuts.section.rules.return_to_list')} - </li> - <li> - <span className="shortcut-button spacer-right">a</span> - {translate('shortcuts.section.rules.activate')} - </li> - <li> - <span className="shortcut-button spacer-right">d</span> - {translate('shortcuts.section.rules.deactivate')} - </li> - </ul> - </div> - - <div className="column-half"> - <h3 className="shortcuts-section-title">{translate('shortcuts.section.issues')}</h3> - <ul className="shortcuts-list"> - <li> - <span className="shortcut-button little-spacer-right">↑</span> - <span className="shortcut-button spacer-right">↓</span> - {translate('shortcuts.section.issues.navigate_between_issues')} - </li> - <li> - <span className="shortcut-button spacer-right">→</span> - {translate('shortcuts.section.issues.open_details')} - </li> - <li> - <span className="shortcut-button spacer-right">←</span> - {translate('shortcuts.section.issues.return_to_list')} - </li> - <li> - <span className="shortcut-button little-spacer-right">alt</span> - <span className="little-spacer-right">+</span> - <span className="shortcut-button little-spacer-right">↑</span> - <span className="shortcut-button spacer-right">↓</span> - {translate('issues.to_navigate_issue_locations')} - </li> - <li> - <span className="shortcut-button little-spacer-right">alt</span> - <span className="little-spacer-right">+</span> - <span className="shortcut-button little-spacer-right">←</span> - <span className="shortcut-button spacer-right">→</span> - {translate('issues.to_switch_flows')} - </li> - <li> - <span className="shortcut-button spacer-right">f</span> - {translate('shortcuts.section.issue.do_transition')} - </li> - <li> - <span className="shortcut-button spacer-right">a</span> - {translate('shortcuts.section.issue.assign')} - </li> - <li> - <span className="shortcut-button spacer-right">m</span> - {translate('shortcuts.section.issue.assign_to_me')} - </li> - <li> - <span className="shortcut-button spacer-right">i</span> - {translate('shortcuts.section.issue.change_severity')} - </li> - <li> - <span className="shortcut-button spacer-right">c</span> - {translate('shortcuts.section.issue.comment')} - </li> - <li> - <span className="shortcut-button little-spacer-right">ctrl</span> - <span className="shortcut-button spacer-right">enter</span> - {translate('shortcuts.section.issue.submit_comment')} - </li> - <li> - <span className="shortcut-button spacer-right">t</span> - {translate('shortcuts.section.issue.change_tags')} - </li> - </ul> - </div> - </div> - </div> - - <div className="modal-foot"> - <a className="js-modal-close" href="#" onClick={this.handleCloseClick}> - {translate('close')} - </a> - </div> - - </Modal> - ); - } -} diff --git a/server/sonar-web/src/main/js/app/styles/boxed-group.css b/server/sonar-web/src/main/js/app/styles/boxed-group.css index 24c56850676..f41ae1927d6 100644 --- a/server/sonar-web/src/main/js/app/styles/boxed-group.css +++ b/server/sonar-web/src/main/js/app/styles/boxed-group.css @@ -34,6 +34,8 @@ } .boxed-group-actions { + position: relative; + z-index: 12; float: right; margin-top: 15px; margin-right: 20px; diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index 5b4a0d37e15..f0793da6798 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -63,6 +63,7 @@ import qualityProfilesRoutes from '../../apps/quality-profiles/routes'; import sessionsRoutes from '../../apps/sessions/routes'; import settingsRoutes from '../../apps/settings/routes'; import systemRoutes from '../../apps/system/routes'; +import tutorialRoutes from '../../apps/tutorials/routes'; import updateCenterRoutes from '../../apps/update-center/routes'; import usersRoutes from '../../apps/users/routes'; import webAPIRoutes from '../../apps/web-api/routes'; @@ -159,6 +160,7 @@ const startReactApp = () => { <Route path="quality_gates" childRoutes={qualityGatesRoutes} /> <Route path="portfolios" component={PortfoliosPage} /> <Route path="profiles" childRoutes={qualityProfilesRoutes} /> + <Route path="tutorials" childRoutes={tutorialRoutes} /> <Route path="web_api" childRoutes={webAPIRoutes} /> <Route component={ProjectContainer}> diff --git a/server/sonar-web/src/main/js/app/utils/throwGlobalError.js b/server/sonar-web/src/main/js/app/utils/throwGlobalError.js new file mode 100644 index 00000000000..75cedec4e68 --- /dev/null +++ b/server/sonar-web/src/main/js/app/utils/throwGlobalError.js @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import getStore from './getStore'; +import { onFail } from '../../store/rootActions'; + +export default function throwGlobalError(error: Object) { + const store = getStore(); + onFail(store.dispatch)(error); + return Promise.reject(); +} diff --git a/server/sonar-web/src/main/js/apps/account/tokens-view.js b/server/sonar-web/src/main/js/apps/account/tokens-view.js index 62c6f8f9143..29ef6f5f9cd 100644 --- a/server/sonar-web/src/main/js/apps/account/tokens-view.js +++ b/server/sonar-web/src/main/js/apps/account/tokens-view.js @@ -52,17 +52,13 @@ export default Marionette.ItemView.extend({ this.errors = []; this.newToken = null; const tokenName = this.$('.js-generate-token-form input').val(); - generateToken(this.model.id, tokenName) - .then(response => { + generateToken(tokenName, this.model.id).then( + response => { this.newToken = response; this.requestTokens(); - }) - .catch(error => { - error.response.json().then(response => { - this.errors = response.errors; - this.render(); - }); - }); + }, + () => {} + ); }, onRevokeTokenFormSubmit(e) { @@ -71,7 +67,7 @@ export default Marionette.ItemView.extend({ const token = this.tokens.find(token => token.name === `${tokenName}`); if (token) { if (token.deleting) { - revokeToken(this.model.id, tokenName).then(this.requestTokens.bind(this)); + revokeToken(tokenName, this.model.id).then(this.requestTokens.bind(this), () => {}); } else { token.deleting = true; this.render(); diff --git a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js b/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js index acc503123ed..0266656aa8d 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js +++ b/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js @@ -103,10 +103,9 @@ export default class CreateProjectForm extends React.PureComponent { this.props.onProjectCreated(); } }, - error => { + () => { if (this.mounted) { this.setState({ loading: false }); - this.props.onRequestFail(error); } } ); diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js index d357cff1fbc..9d45c3749e7 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/App.js +++ b/server/sonar-web/src/main/js/apps/settings/components/App.js @@ -83,15 +83,15 @@ class App extends React.PureComponent { <Helmet title={translate('settings.page')} /> <PageHeader component={this.props.component} /> - <div className="settings-layout"> - <div className="settings-side"> + <div className="side-tabs-layout settings-layout"> + <div className="side-tabs-side"> <AllCategoriesList component={this.props.component} selectedCategory={selectedCategory} defaultCategory={this.props.defaultCategory} /> </div> - <div className="settings-main"> + <div className="side-tabs-main"> <CategoryDefinitionsList component={this.props.component} category={selectedCategory} /> {selectedCategory === 'exclusions' && <WildcardsHelp />} diff --git a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js index 3ad92ee2b96..676f5511174 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js +++ b/server/sonar-web/src/main/js/apps/settings/components/CategoriesList.js @@ -70,7 +70,7 @@ export default class CategoriesList extends React.PureComponent { const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase()); return ( - <ul className="settings-menu"> + <ul className="side-tabs-menu"> {sortedCategories.map(category => ( <li key={category.key}> {this.renderLink(category)} diff --git a/server/sonar-web/src/main/js/apps/settings/styles.css b/server/sonar-web/src/main/js/apps/settings/styles.css index a529c5c5c5f..4d188e811dc 100644 --- a/server/sonar-web/src/main/js/apps/settings/styles.css +++ b/server/sonar-web/src/main/js/apps/settings/styles.css @@ -1,60 +1,7 @@ .settings-layout { - display: flex; - justify-content: space-between; - align-items: stretch; margin-bottom: 60px; } -.settings-main { - position: relative; - z-index: 2; - flex-grow: 1; - padding: 15px 20px; - border: 1px solid #e6e6e6; - box-sizing: border-box; - background-color: #fff; -} - -.settings-side { - position: relative; - z-index: 3; - width: 160px; - flex-shrink: 0; - padding: 10px 0; - box-sizing: border-box; - transform: translateX(1px); -} - -.settings-menu {} - -.settings-menu > li { - margin-bottom: 4px; -} - -.settings-menu > li > a { - display: block; - padding: 10px 10px; - line-height: 1.5; - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; - border: 1px solid #e6e6e6; - border-right: none; - overflow: hidden; - text-overflow: ellipsis; - transition: color 0.3s ease, background-color 0.3s ease; -} - -.settings-menu > li > a:hover, -.settings-menu > li > a:focus, -.settings-menu > li > a.active { - background-color: #fff; -} - -.settings-menu > li > a.active { - color: #444; - cursor: default; -} - .settings-definitions-list > li + li { margin-top: 30px; } diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js new file mode 100644 index 00000000000..78db84e6ef9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js @@ -0,0 +1,181 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Step from './Step'; +import LanguageStep from './LanguageStep'; +import type { Result } from './LanguageStep'; +import JavaMaven from './commands/JavaMaven'; +import JavaGradle from './commands/JavaGradle'; +import DotNet from './commands/DotNet'; +import Msvc from './commands/Msvc'; +import ClangGCC from './commands/ClangGCC'; +import Other from './commands/Other'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + open: boolean, + organization?: string, + sonarCloud: boolean, + stepNumber: number, + token: string +|}; + +type State = { + result?: Result +}; + +export default class AnalysisStep extends React.PureComponent { + props: Props; + state: State = {}; + + handleLanguageSelect = (result?: Result) => { + this.setState({ result }); + }; + + handleLanguageReset = () => { + this.setState({ result: undefined }); + }; + + getHost = () => window.location.origin + window.baseUrl; + + renderForm = () => { + return ( + <div className="boxed-group-inner"> + <div className="flex-columns"> + <div className="flex-column flex-column-half bordered-right"> + <LanguageStep + onDone={this.handleLanguageSelect} + onReset={this.handleLanguageReset} + sonarCloud={this.props.sonarCloud} + /> + </div> + <div className="flex-column flex-column-half"> + {this.renderCommand()} + </div> + </div> + </div> + ); + }; + + renderFormattedCommand = (...lines: Array<string>) => ( + <pre>{lines.join(' ' + '\\' + '\n' + ' ')}</pre> + ); + + renderCommand = () => { + const { result } = this.state; + + if (!result) { + return null; + } + + if (result.language === 'java') { + return result.javaBuild === 'maven' + ? this.renderCommandForMaven() + : this.renderCommandForGradle(); + } else if (result.language === 'dotnet') { + return this.renderCommandForDotNet(); + } else if (result.language === 'c-family') { + return result.cFamilyCompiler === 'msvc' + ? this.renderCommandForMSVC() + : this.renderCommandForClangGCC(); + } else { + return this.renderCommandForOther(); + } + }; + + renderCommandForMaven = () => ( + <JavaMaven + host={this.getHost()} + organization={this.props.organization} + token={this.props.token} + /> + ); + + renderCommandForGradle = () => ( + <JavaGradle + host={this.getHost()} + organization={this.props.organization} + token={this.props.token} + /> + ); + + renderCommandForDotNet = () => { + return ( + <DotNet + host={this.getHost()} + organization={this.props.organization} + // $FlowFixMe + projectKey={this.state.result.projectKey} + token={this.props.token} + /> + ); + }; + + renderCommandForMSVC = () => { + return ( + <Msvc + host={this.getHost()} + organization={this.props.organization} + // $FlowFixMe + projectKey={this.state.result.projectKey} + token={this.props.token} + /> + ); + }; + + renderCommandForClangGCC = () => ( + <ClangGCC + host={this.getHost()} + organization={this.props.organization} + // $FlowFixMe + os={this.state.result.os} + // $FlowFixMe + projectKey={this.state.result.projectKey} + token={this.props.token} + /> + ); + + renderCommandForOther = () => ( + <Other + host={this.getHost()} + organization={this.props.organization} + // $FlowFixMe + os={this.state.result.os} + // $FlowFixMe + projectKey={this.state.result.projectKey} + token={this.props.token} + /> + ); + + renderResult = () => null; + + render() { + return ( + <Step + open={this.props.open} + renderForm={this.renderForm} + renderResult={this.renderResult} + stepNumber={this.props.stepNumber} + stepTitle={translate('onboarding.analysis.header')} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/LanguageStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/LanguageStep.js new file mode 100644 index 00000000000..44bfb4ed766 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/LanguageStep.js @@ -0,0 +1,188 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import NewProjectForm from './NewProjectForm'; +import RadioToggle from '../../../components/controls/RadioToggle'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + onDone: (result: Result) => void, + onReset: () => void, + organization?: string, + sonarCloud: boolean +|}; + +type State = { + language?: string, + javaBuild?: string, + cFamilyCompiler?: string, + os?: string, + projectKey?: string +}; + +export type Result = State; + +export default class LanguageStep extends React.PureComponent { + props: Props; + + static defaultProps = { sonarCloud: false }; + + state: State = {}; + + isConfigured = () => { + const { language, javaBuild, cFamilyCompiler, os, projectKey } = this.state; + const isJavaConfigured = language === 'java' && javaBuild != null; + const isDotNetConfigured = language === 'dotnet' && projectKey != null; + const isCFamilyConfigured = + language === 'c-family' && (cFamilyCompiler === 'msvc' || os != null) && projectKey != null; + const isOtherConfigured = language === 'other' && projectKey != null; + + return isJavaConfigured || isDotNetConfigured || isCFamilyConfigured || isOtherConfigured; + }; + + handleChange = () => { + if (this.isConfigured()) { + this.props.onDone(this.state); + } else { + this.props.onReset(); + } + }; + + handleLanguageChange = (language: string) => { + this.setState({ language }, this.handleChange); + }; + + handleJavaBuildChange = (javaBuild: string) => { + this.setState({ javaBuild }, this.handleChange); + }; + + handleCFamilyCompilerChange = (cFamilyCompiler: string) => { + this.setState({ cFamilyCompiler }, this.handleChange); + }; + + handleOSChange = (os: string) => { + this.setState({ os }, this.handleChange); + }; + + handleProjectKeyDone = (projectKey: string) => { + this.setState({ projectKey }, this.handleChange); + }; + + handleProjectKeyDelete = () => { + this.setState({ projectKey: undefined }, this.handleChange); + }; + + renderJavaBuild = () => ( + <div className="big-spacer-top"> + <h4 className="spacer-bottom"> + {translate('onboarding.language.java.build_technology')} + </h4> + <RadioToggle + name="java-build" + onCheck={this.handleJavaBuildChange} + options={['maven', 'gradle'].map(build => ({ + label: translate('onboarding.language.java.build_technology', build), + value: build + }))} + value={this.state.javaBuild} + /> + </div> + ); + + renderCFamilyCompiler = () => ( + <div className="big-spacer-top"> + <h4 className="spacer-bottom"> + {translate('onboarding.language.c-family.compiler')} + </h4> + <RadioToggle + name="c-family-compiler" + onCheck={this.handleCFamilyCompilerChange} + options={['msvc', 'clang-gcc'].map(compiler => ({ + label: translate('onboarding.language.c-family.compiler', compiler), + value: compiler + }))} + value={this.state.cFamilyCompiler} + /> + </div> + ); + + renderOS = () => ( + <div className="big-spacer-top"> + <h4 className="spacer-bottom"> + {translate('onboarding.language.os')} + </h4> + <RadioToggle + name="os" + onCheck={this.handleOSChange} + options={['linux', 'win', 'mac'].map(os => ({ + label: translate('onboarding.language.os', os), + value: os + }))} + value={this.state.os} + /> + </div> + ); + + renderProjectKey = () => ( + <NewProjectForm + onDelete={this.handleProjectKeyDelete} + onDone={this.handleProjectKeyDone} + organization={this.props.organization} + projectKey={this.state.projectKey} + /> + ); + + render() { + const shouldAskProjectKey = + this.state.language === 'dotnet' || + (this.state.language === 'c-family' && + (this.state.cFamilyCompiler === 'msvc' || + (this.state.cFamilyCompiler === 'clang-gcc' && this.state.os != null))) || + (this.state.language === 'other' && this.state.os !== undefined); + + const languages = this.props.sonarCloud + ? ['java', 'dotnet', 'c-family', 'other'] + : ['java', 'dotnet', 'other']; + + return ( + <div> + <div> + <h4 className="spacer-bottom">{translate('onboarding.language')}</h4> + <RadioToggle + name="language" + onCheck={this.handleLanguageChange} + options={languages.map(language => ({ + label: translate('onboarding.language', language), + value: language + }))} + value={this.state.language} + /> + </div> + {this.state.language === 'java' && this.renderJavaBuild()} + {this.state.language === 'c-family' && this.renderCFamilyCompiler()} + {((this.state.language === 'c-family' && this.state.cFamilyCompiler === 'clang-gcc') || + this.state.language === 'other') && + this.renderOS()} + {shouldAskProjectKey && this.renderProjectKey()} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewOrganizationForm.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewOrganizationForm.js new file mode 100644 index 00000000000..16a7c54d526 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewOrganizationForm.js @@ -0,0 +1,157 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { debounce } from 'lodash'; +import { + createOrganization, + deleteOrganization, + getOrganization +} from '../../../api/organizations'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + onDelete: () => void, + onDone: (organization: string) => void, + organization?: string +|}; + +type State = { + done: boolean, + loading: boolean, + organization: string, + unique: boolean +}; + +export default class NewOrganizationForm extends React.PureComponent { + mounted: boolean; + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { + done: props.organization != null, + loading: false, + organization: props.organization || '', + unique: true + }; + this.validateOrganization = debounce(this.validateOrganization, 500); + } + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + + validateOrganization = (organization: string) => { + getOrganization(organization).then(response => { + if (this.mounted) { + this.setState({ unique: response == null }); + } + }); + }; + + sanitizeOrganization = (organization: string) => + organization.toLowerCase().replace(/[^a-z0-9-]/, '').replace(/^-/, ''); + + handleOrganizationChange = (event: { target: HTMLInputElement }) => { + const organization = this.sanitizeOrganization(event.target.value); + this.setState({ organization }); + this.validateOrganization(organization); + }; + + handleOrganizationCreate = (event: Event) => { + event.preventDefault(); + const { organization } = this.state; + if (organization) { + this.setState({ loading: true }); + createOrganization({ key: organization, name: organization }).then(() => { + if (this.mounted) { + this.setState({ done: true, loading: false }); + this.props.onDone(organization); + } + }, this.stopLoading); + } + }; + + handleOrganizationDelete = (event: Event) => { + event.preventDefault(); + const { organization } = this.state; + if (organization) { + this.setState({ loading: true }); + deleteOrganization(organization).then(() => { + if (this.mounted) { + this.setState({ done: false, loading: false, organization: '' }); + this.props.onDelete(); + } + }, this.stopLoading); + } + }; + + render() { + const { done, loading, organization, unique } = this.state; + + const valid = unique && organization.length >= 2; + + return done + ? <form onSubmit={this.handleOrganizationDelete}> + <span className="spacer-right text-middle">{organization}</span> + {loading + ? <i className="spinner" /> + : <button className="button-clean"> + <i className="icon-delete" /> + </button>} + </form> + : <form onSubmit={this.handleOrganizationCreate}> + <input + autoFocus={true} + className="input-super-large spacer-right text-middle" + onChange={this.handleOrganizationChange} + maxLength={32} + minLength={2} + placeholder={translate('onboarding.organization.placeholder')} + required={true} + type="text" + value={organization} + /> + {loading + ? <i className="spinner" /> + : <button className="text-middle" disabled={!valid}>{translate('create')}</button>} + {!unique && + <span className="big-spacer-left text-danger text-middle"> + <i className="icon-alert-error little-spacer-right text-text-top" /> + {translate('this_name_is_already_taken')} + </span>} + <div className="note spacer-top abs-width-300"> + {translate('onboarding.organization.key_requirement')} + </div> + </form>; + } +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewProjectForm.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewProjectForm.js new file mode 100644 index 00000000000..2bf64f8fe5a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/NewProjectForm.js @@ -0,0 +1,145 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { createProject, deleteProject } from '../../../api/components'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + onDelete: () => void, + onDone: (projectKey: string) => void, + organization?: string, + projectKey?: string +|}; + +type State = { + done: boolean, + loading: boolean, + projectKey: string +}; + +export default class NewProjectForm extends React.PureComponent { + mounted: boolean; + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { + done: props.projectKey != null, + loading: false, + projectKey: props.projectKey || '' + }; + } + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + + sanitizeProjectKey = (projectKey: string) => projectKey.replace(/[^a-zA-Z0-9-_\.:]/, ''); + + handleProjectKeyChange = (event: { target: HTMLInputElement }) => { + this.setState({ projectKey: this.sanitizeProjectKey(event.target.value) }); + }; + + handleProjectCreate = (event: Event) => { + event.preventDefault(); + const { projectKey } = this.state; + const data: { [string]: string } = { + name: projectKey, + project: projectKey + }; + if (this.props.organization) { + data.organization = this.props.organization; + } + this.setState({ loading: true }); + createProject(data).then(() => { + if (this.mounted) { + this.setState({ done: true, loading: false }); + this.props.onDone(projectKey); + } + }, this.stopLoading); + }; + + handleProjectDelete = (event: Event) => { + event.preventDefault(); + const { projectKey } = this.state; + this.setState({ loading: true }); + deleteProject(projectKey).then(() => { + if (this.mounted) { + this.setState({ done: false, loading: false, projectKey: '' }); + this.props.onDelete(); + } + }, this.stopLoading); + }; + + render() { + const { done, loading, projectKey } = this.state; + + const valid = projectKey.length > 0; + + const form = done + ? <form onSubmit={this.handleProjectDelete}> + <span className="spacer-right text-middle">{projectKey}</span> + {loading + ? <i className="spinner" /> + : <button className="button-clean"> + <i className="icon-delete" /> + </button>} + </form> + : <form onSubmit={this.handleProjectCreate}> + <input + autoFocus={true} + className="input-large spacer-right text-middle" + minLength={1} + maxLength={400} + onChange={this.handleProjectKeyChange} + required={true} + type="text" + value={projectKey} + /> + {loading + ? <i className="spinner" /> + : <button className="text-middle" disabled={!valid}>{translate('Done')}</button>} + <div className="note spacer-top abs-width-300"> + {translate('onboarding.project_key_requirement')} + </div> + </form>; + + return ( + <div className="big-spacer-top"> + <h4 className="spacer-bottom"> + {translate('onboarding.language.project_key')} + </h4> + {form} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js new file mode 100644 index 00000000000..0382a33e0d6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js @@ -0,0 +1,109 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import TokenStep from './TokenStep'; +import OrganizationStep from './OrganizationStep'; +import AnalysisStep from './AnalysisStep'; +import { translate } from '../../../helpers/l10n'; +import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; +import './styles.css'; + +type Props = { + currentUser: { login: string, isLoggedIn: boolean }, + organizationsEnabled: boolean, + sonarCloud: boolean +}; + +type State = { + organization?: string, + step: string, + token?: string +}; + +export default class Onboarding extends React.PureComponent { + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { step: props.organizationsEnabled ? 'organization' : 'token' }; + } + + componentDidMount() { + if (!this.props.currentUser.isLoggedIn) { + handleRequiredAuthentication(); + } + } + + handleTokenDone = (token: string) => { + this.setState({ step: 'analysis', token }); + }; + + handleOrganizationDone = (organization: string) => { + this.setState({ organization, step: 'token' }); + }; + + render() { + if (!this.props.currentUser.isLoggedIn) { + return null; + } + + const { organizationsEnabled, sonarCloud } = this.props; + const { step, token } = this.state; + + let stepNumber = 1; + + return ( + <div className="page page-limited"> + <header className="page-header"> + <h1 className="page-title"> + {translate(sonarCloud ? 'onboarding.header.sonarcloud' : 'onboarding.header')} + </h1> + <div className="page-description"> + {translate('onboarding.header.description')} + </div> + </header> + + {organizationsEnabled && + <OrganizationStep + currentUser={this.props.currentUser} + onContinue={this.handleOrganizationDone} + open={step === 'organization'} + stepNumber={stepNumber++} + />} + + <TokenStep + onContinue={this.handleTokenDone} + open={step === 'token'} + stepNumber={stepNumber++} + /> + + <AnalysisStep + organization={this.state.organization} + open={step === 'analysis'} + sonarCloud={sonarCloud} + stepNumber={stepNumber} + token={token} + /> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js new file mode 100644 index 00000000000..a7eecc2e9e4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingContainer.js @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import { connect } from 'react-redux'; +import Onboarding from './Onboarding'; +import { + getCurrentUser, + areThereCustomOrganizations, + getSettingValue +} from '../../../store/rootReducer'; + +const mapStateToProps = state => { + const sonarCloudSetting = getSettingValue(state, 'sonar.lf.sonarqube.com.enabled'); + + return { + currentUser: getCurrentUser(state), + organizationsEnabled: areThereCustomOrganizations(state), + sonarCloud: sonarCloudSetting != null && sonarCloudSetting.value === 'true' + }; +}; + +export default connect(mapStateToProps)(Onboarding); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js new file mode 100644 index 00000000000..325e8e8ac56 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js @@ -0,0 +1,240 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Select from 'react-select'; +import classNames from 'classnames'; +import { sortBy } from 'lodash'; +import Step from './Step'; +import NewOrganizationForm from './NewOrganizationForm'; +import { getMyOrganizations } from '../../../api/organizations'; +import { translate } from '../../../helpers/l10n'; + +type Props = { + currentUser: { login: string, isLoggedIn: boolean }, + open: boolean, + onContinue: (organization: string) => void +}; + +type State = { + loading: boolean, + newOrganization?: string, + existingOrganization?: string, + existingOrganizations: Array<string>, + selection: 'personal' | 'existing' | 'new' +}; + +export default class OrganizationStep extends React.PureComponent { + mounted: boolean; + props: Props; + state: State = { + loading: true, + existingOrganizations: [], + selection: 'personal' + }; + + componentDidMount() { + this.mounted = true; + this.fetchOrganizations(); + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchOrganizations = () => { + getMyOrganizations().then( + organizations => { + if (this.mounted) { + this.setState({ + loading: false, + existingOrganizations: sortBy( + organizations.filter(organization => organization !== this.props.currentUser.login) + ) + }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + getSelectedOrganization = () => { + switch (this.state.selection) { + case 'personal': + return this.props.currentUser.login; + case 'existing': + return this.state.existingOrganization; + case 'new': + return this.state.newOrganization; + default: + return null; + } + }; + + handlePersonalClick = (event: Event) => { + event.preventDefault(); + this.setState({ selection: 'personal' }); + }; + + handleExistingClick = (event: Event) => { + event.preventDefault(); + this.setState({ selection: 'existing' }); + }; + + handleNewClick = (event: Event) => { + event.preventDefault(); + this.setState({ selection: 'new' }); + }; + + handleOrganizationCreate = (newOrganization: string) => { + this.setState({ newOrganization }); + }; + + handleOrganizationDelete = () => { + this.setState({ newOrganization: undefined }); + }; + + handleExistingOrganizationSelect = ({ value }: { value: string }) => { + this.setState({ existingOrganization: value }); + }; + + handleContinueClick = (event: Event) => { + event.preventDefault(); + const organization = this.getSelectedOrganization(); + if (organization) { + this.props.onContinue(organization); + } + }; + + renderPersonalOrganizationOption = () => ( + <div> + <a className="link-base-color link-no-underline" href="#" onClick={this.handlePersonalClick}> + <i + className={classNames('icon-radio', 'spacer-right', { + 'is-checked': this.state.selection === 'personal' + })} + /> + {translate('onboarding.organization.my_personal_organization')} + <span className="note spacer-left">{this.props.currentUser.login}</span> + </a> + </div> + ); + + renderExistingOrganizationOption = () => ( + <div className="big-spacer-top"> + <a + className="js-existing link-base-color link-no-underline" + href="#" + onClick={this.handleExistingClick}> + <i + className={classNames('icon-radio', 'spacer-right', { + 'is-checked': this.state.selection === 'existing' + })} + /> + {translate('onboarding.organization.exising_organization')} + </a> + {this.state.selection === 'existing' && + <div className="big-spacer-top"> + <Select + className="input-super-large" + clearable={false} + onChange={this.handleExistingOrganizationSelect} + options={this.state.existingOrganizations.map(organization => ({ + label: organization, + value: organization + }))} + value={this.state.existingOrganization} + /> + </div>} + </div> + ); + + renderNewOrganizationOption = () => ( + <div className="big-spacer-top"> + <a + className="js-new link-base-color link-no-underline" + href="#" + onClick={this.handleNewClick}> + <i + className={classNames('icon-radio', 'spacer-right', { + 'is-checked': this.state.selection === 'new' + })} + /> + {translate('onboarding.organization.create_another_organization')} + </a> + {this.state.selection === 'new' && + <div className="big-spacer-top"> + <NewOrganizationForm + onDelete={this.handleOrganizationDelete} + onDone={this.handleOrganizationCreate} + organization={this.state.newOrganization} + /> + </div>} + </div> + ); + + renderForm = () => { + return ( + <div className="boxed-group-inner"> + <div className="big-spacer-bottom width-50"> + {translate('onboarding.organization.text')} + </div> + + {this.renderPersonalOrganizationOption()} + {this.state.existingOrganizations.length > 0 && this.renderExistingOrganizationOption()} + {this.renderNewOrganizationOption()} + + {this.getSelectedOrganization() != null && + <div className="big-spacer-top"> + <button className="js-continue" onClick={this.handleContinueClick}> + {translate('continue')} + </button> + </div>} + </div> + ); + }; + + renderResult = () => { + const result = this.getSelectedOrganization(); + + return result != null + ? <div className="boxed-group-actions"> + <i className="icon-check spacer-right" /> + <strong>{result}</strong> + </div> + : null; + }; + + render() { + return ( + <Step + open={this.props.open} + renderForm={this.renderForm} + renderResult={this.renderResult} + stepNumber={1} + stepTitle={translate('onboarding.organization.header')} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js new file mode 100644 index 00000000000..156fc34b470 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectKeyStep.js @@ -0,0 +1,145 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { createProject, deleteProject } from '../../../api/components'; +import { translate } from '../../../helpers/l10n'; + +type Props = { + onDelete: () => void, + onDone: (projectKey: string) => void, + organization?: string, + projectKey?: string +}; + +type State = { + done: boolean, + loading: boolean, + projectKey: string +}; + +export default class ProjectKeyStep extends React.PureComponent { + mounted: boolean; + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { + done: props.projectKey != null, + loading: false, + projectKey: props.projectKey || '' + }; + } + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + + sanitizeProjectKey = (projectKey: string) => projectKey.replace(/[^a-zA-Z0-9-_\.:]/, ''); + + handleProjectKeyChange = (event: { target: HTMLInputElement }) => { + this.setState({ projectKey: this.sanitizeProjectKey(event.target.value) }); + }; + + handleProjectCreate = (event: Event) => { + event.preventDefault(); + const { projectKey } = this.state; + const data: { [string]: string } = { + name: projectKey, + project: projectKey + }; + if (this.props.organization) { + data.organization = this.props.organization; + } + this.setState({ loading: true }); + createProject(data).then(() => { + if (this.mounted) { + this.setState({ done: true, loading: false }); + this.props.onDone(projectKey); + } + }, this.stopLoading); + }; + + handleProjectDelete = (event: Event) => { + event.preventDefault(); + const { projectKey } = this.state; + this.setState({ loading: true }); + deleteProject(projectKey).then(() => { + if (this.mounted) { + this.setState({ done: false, loading: false, projectKey: '' }); + this.props.onDelete(); + } + }, this.stopLoading); + }; + + render() { + const { done, loading, projectKey } = this.state; + + const valid = projectKey.length > 0; + + const form = done + ? <form onSubmit={this.handleProjectDelete}> + <span className="spacer-right text-middle">{projectKey}</span> + {loading + ? <i className="spinner" /> + : <button className="button-clean"> + <i className="icon-delete" /> + </button>} + </form> + : <form onSubmit={this.handleProjectCreate}> + <input + autoFocus={true} + className="input-large spacer-right text-middle" + minLength={1} + maxLength={400} + onChange={this.handleProjectKeyChange} + required={true} + type="text" + value={projectKey} + /> + {loading + ? <i className="spinner" /> + : <button className="text-middle" disabled={!valid}>{translate('Done')}</button>} + <div className="note spacer-top abs-width-300"> + {translate('onboarding.project_key_requirement')} + </div> + </form>; + + return ( + <div className="big-spacer-top"> + <h4 className="spacer-bottom"> + {translate('onboarding.language.project_key')} + </h4> + {form} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Step.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Step.js new file mode 100644 index 00000000000..763cef635df --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Step.js @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import classNames from 'classnames'; + +type Props = { + open: boolean, + renderForm: () => React.Element<*>, + renderResult: () => ?React.Element<*>, + stepNumber: number, + stepTitle: string +}; + +export default function Step(props: Props) { + const className = classNames('boxed-group', 'onboarding-step', { + 'onboarding-step-open': props.open + }); + + return ( + <div className={className}> + <div className="onboarding-step-number">{props.stepNumber}</div> + {!props.open && props.renderResult()} + <div className="boxed-group-header"> + <h2>{props.stepTitle}</h2> + </div> + {props.open ? props.renderForm() : <div className="boxed-group-inner" />} + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js new file mode 100644 index 00000000000..9a171f7bc77 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js @@ -0,0 +1,180 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Step from './Step'; +import { generateToken, revokeToken } from '../../../api/user-tokens'; +import { translate } from '../../../helpers/l10n'; + +type Props = { + open: boolean, + onContinue: (token: string) => void, + stepNumber: number +}; + +type State = { + loading: boolean, + tokenName?: string, + token?: string +}; + +export default class TokenStep extends React.PureComponent { + mounted: boolean; + props: Props; + + static defaultProps = { + stepNumber: 1 + }; + + state: State = { + loading: false + }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleTokenNameChange = (event: { target: HTMLInputElement }) => { + this.setState({ tokenName: event.target.value }); + }; + + handleTokenGenerate = (event: Event) => { + event.preventDefault(); + const { tokenName } = this.state; + if (tokenName) { + this.setState({ loading: true }); + generateToken(tokenName).then( + ({ token }) => { + if (this.mounted) { + this.setState({ loading: false, token }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + } + }; + + handleTokenRevoke = (event: Event) => { + event.preventDefault(); + const { tokenName } = this.state; + if (tokenName) { + this.setState({ loading: true }); + revokeToken(tokenName).then( + () => { + if (this.mounted) { + this.setState({ loading: false, token: undefined, tokenName: undefined }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + } + }; + + handleContinueClick = (event: Event) => { + event.preventDefault(); + if (this.state.token) { + this.props.onContinue(this.state.token); + } + }; + + renderForm = () => { + const { loading, token, tokenName } = this.state; + + return ( + <div className="boxed-group-inner"> + <div className="big-spacer-bottom width-50"> + {translate('onboarding.token.text')} + </div> + + {token != null + ? <form onSubmit={this.handleTokenRevoke}> + {tokenName}{': '} + <span className="monospaced spacer-right">{token}</span> + {loading + ? <i className="spinner" /> + : <button className="button-clean" onClick={this.handleTokenRevoke}> + <i className="icon-delete" /> + </button>} + </form> + : <form onSubmit={this.handleTokenGenerate}> + <input + autoFocus={true} + className="input-large spacer-right" + onChange={this.handleTokenNameChange} + placeholder={translate('onboarding.token.placeholder')} + required={true} + type="text" + value={tokenName || ''} + /> + {loading + ? <i className="spinner" /> + : <button>{translate('onboarding.token.generate')}</button>} + </form>} + + {token != null && + <div className="big-spacer-top"> + <button className="js-continue" onClick={this.handleContinueClick}> + {translate('continue')} + </button> + </div>} + </div> + ); + }; + + renderResult = () => { + const { token, tokenName } = this.state; + + if (!token) { + return null; + } + + return ( + <div className="boxed-group-actions"> + <i className="icon-check spacer-right" /> + {tokenName}{': '} + <strong className="monospaced">{token}</strong> + </div> + ); + }; + + render() { + return ( + <Step + open={this.props.open} + renderForm={this.renderForm} + renderResult={this.renderResult} + stepNumber={this.props.stepNumber} + stepTitle={translate('onboarding.token.header')} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/LanguageStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/LanguageStep-test.js new file mode 100644 index 00000000000..414b0dba7fe --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/LanguageStep-test.js @@ -0,0 +1,106 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import LanguageStep from '../LanguageStep'; + +it('selects java', () => { + const onDone = jest.fn(); + const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />); + + wrapper.find('RadioToggle').prop('onCheck')('java'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('RadioToggle').at(1).prop('onCheck')('maven'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + expect(onDone).lastCalledWith({ language: 'java', javaBuild: 'maven' }); + + wrapper.find('RadioToggle').at(1).prop('onCheck')('gradle'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + expect(onDone).lastCalledWith({ language: 'java', javaBuild: 'gradle' }); +}); + +it('selects c#', () => { + const onDone = jest.fn(); + const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />); + + wrapper.find('RadioToggle').prop('onCheck')('dotnet'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('NewProjectForm').prop('onDone')('project-foo'); + expect(onDone).lastCalledWith({ language: 'dotnet', projectKey: 'project-foo' }); +}); + +it('selects c-family', () => { + const onDone = jest.fn(); + const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} sonarCloud={true} />); + + wrapper.find('RadioToggle').prop('onCheck')('c-family'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('RadioToggle').at(1).prop('onCheck')('msvc'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('NewProjectForm').prop('onDone')('project-foo'); + expect(onDone).lastCalledWith({ + language: 'c-family', + cFamilyCompiler: 'msvc', + projectKey: 'project-foo' + }); + + wrapper.find('RadioToggle').at(1).prop('onCheck')('clang-gcc'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('RadioToggle').at(2).prop('onCheck')('linux'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('NewProjectForm').prop('onDone')('project-foo'); + expect(onDone).lastCalledWith({ + language: 'c-family', + cFamilyCompiler: 'clang-gcc', + os: 'linux', + projectKey: 'project-foo' + }); +}); + +it('selects other', () => { + const onDone = jest.fn(); + const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />); + + wrapper.find('RadioToggle').prop('onCheck')('other'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('RadioToggle').at(1).prop('onCheck')('mac'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('NewProjectForm').prop('onDone')('project-foo'); + expect(onDone).lastCalledWith({ language: 'other', os: 'mac', projectKey: 'project-foo' }); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewOrganizationForm-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewOrganizationForm-test.js new file mode 100644 index 00000000000..79a5542b3ee --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewOrganizationForm-test.js @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { mount } from 'enzyme'; +import NewOrganizationForm from '../NewOrganizationForm'; +import { change, doAsync, submit } from '../../../../helpers/testUtils'; + +jest.mock('../../../../api/organizations', () => ({ + createOrganization: () => Promise.resolve(), + deleteOrganization: () => Promise.resolve(), + getOrganization: () => Promise.resolve(null) +})); + +it('creates new organization', () => { + const onDone = jest.fn(); + const wrapper = mount(<NewOrganizationForm onDelete={jest.fn()} onDone={onDone} />); + expect(wrapper).toMatchSnapshot(); + change(wrapper.find('input'), 'foo'); + submit(wrapper.find('form')); + expect(wrapper).toMatchSnapshot(); // spinner + return doAsync(() => { + expect(wrapper).toMatchSnapshot(); + expect(onDone).toBeCalledWith('foo'); + }); +}); + +it('deletes organization', () => { + const onDelete = jest.fn(); + const wrapper = mount(<NewOrganizationForm onDelete={onDelete} onDone={jest.fn()} />); + wrapper.setState({ done: true, loading: false, organization: 'foo' }); + expect(wrapper).toMatchSnapshot(); + submit(wrapper.find('form')); + expect(wrapper).toMatchSnapshot(); // spinner + return doAsync(() => { + expect(wrapper).toMatchSnapshot(); + expect(onDelete).toBeCalled(); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewProjectForm-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewProjectForm-test.js new file mode 100644 index 00000000000..7b05724cbe7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewProjectForm-test.js @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { mount } from 'enzyme'; +import NewProjectForm from '../NewProjectForm'; +import { change, doAsync, submit } from '../../../../helpers/testUtils'; + +jest.mock('../../../../api/components', () => ({ + createProject: () => Promise.resolve(), + deleteProject: () => Promise.resolve() +})); + +it('creates new project', () => { + const onDone = jest.fn(); + const wrapper = mount(<NewProjectForm onDelete={jest.fn()} onDone={onDone} />); + expect(wrapper).toMatchSnapshot(); + change(wrapper.find('input'), 'foo'); + submit(wrapper.find('form')); + expect(wrapper).toMatchSnapshot(); // spinner + return doAsync(() => { + expect(wrapper).toMatchSnapshot(); + expect(onDone).toBeCalledWith('foo'); + }); +}); + +it('deletes project', () => { + const onDelete = jest.fn(); + const wrapper = mount(<NewProjectForm onDelete={onDelete} onDone={jest.fn()} />); + wrapper.setState({ done: true, loading: false, projectKey: 'foo' }); + expect(wrapper).toMatchSnapshot(); + submit(wrapper.find('form')); + expect(wrapper).toMatchSnapshot(); // spinner + return doAsync(() => { + expect(wrapper).toMatchSnapshot(); + expect(onDelete).toBeCalled(); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js new file mode 100644 index 00000000000..9352556f5d1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { mount } from 'enzyme'; +import OrganizationStep from '../OrganizationStep'; +import { click, doAsync } from '../../../../helpers/testUtils'; + +jest.mock('../../../../api/organizations', () => ({ + getMyOrganizations: () => Promise.resolve(['user', 'another']) +})); + +const currentUser = { isLoggedIn: true, login: 'user' }; + +it('works with personal organization', () => { + const onContinue = jest.fn(); + const wrapper = mount( + <OrganizationStep currentUser={currentUser} onContinue={onContinue} open={true} /> + ); + click(wrapper.find('.js-continue')); + expect(onContinue).toBeCalledWith('user'); +}); + +it('works with existing organization', () => { + const onContinue = jest.fn(); + const wrapper = mount( + <OrganizationStep currentUser={currentUser} onContinue={onContinue} open={true} /> + ); + return doAsync(() => { + click(wrapper.find('.js-existing')); + wrapper.find('Select').prop('onChange')({ value: 'another' }); + click(wrapper.find('.js-continue')); + expect(onContinue).toBeCalledWith('another'); + }); +}); + +it('works with new organization', () => { + const onContinue = jest.fn(); + const wrapper = mount( + <OrganizationStep currentUser={currentUser} onContinue={onContinue} open={true} /> + ); + click(wrapper.find('.js-new')); + wrapper.find('NewOrganizationForm').prop('onDone')('new'); + click(wrapper.find('.js-continue')); + expect(onContinue).toBeCalledWith('new'); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js new file mode 100644 index 00000000000..2af50f6a3b5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectKeyStep-test.js @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { mount } from 'enzyme'; +import ProjectKeyStep from '../ProjectKeyStep'; +import { change, doAsync, submit } from '../../../../helpers/testUtils'; + +jest.mock('../../../../api/components', () => ({ + createProject: () => Promise.resolve(), + deleteProject: () => Promise.resolve() +})); + +it('creates new project', () => { + const onDone = jest.fn(); + const wrapper = mount(<ProjectKeyStep onDelete={jest.fn()} onDone={onDone} />); + expect(wrapper).toMatchSnapshot(); + change(wrapper.find('input'), 'foo'); + submit(wrapper.find('form')); + expect(wrapper).toMatchSnapshot(); // spinner + return doAsync(() => { + expect(wrapper).toMatchSnapshot(); + expect(onDone).toBeCalledWith('foo'); + }); +}); + +it('deletes project', () => { + const onDelete = jest.fn(); + const wrapper = mount(<ProjectKeyStep onDelete={onDelete} onDone={jest.fn()} />); + wrapper.setState({ done: true, loading: false, projectKey: 'foo' }); + expect(wrapper).toMatchSnapshot(); + submit(wrapper.find('form')); + expect(wrapper).toMatchSnapshot(); // spinner + return doAsync(() => { + expect(wrapper).toMatchSnapshot(); + expect(onDelete).toBeCalled(); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Step-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Step-test.js new file mode 100644 index 00000000000..89d7a3b7ba4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/Step-test.js @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import Step from '../Step'; + +it('renders', () => { + const wrapper = shallow( + <Step + open={true} + renderForm={() => <div>form</div>} + renderResult={() => <div>result</div>} + stepNumber={1} + stepTitle="First Step" + /> + ); + expect(wrapper).toMatchSnapshot(); + wrapper.setProps({ open: false }); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js new file mode 100644 index 00000000000..9498fc27ada --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { mount } from 'enzyme'; +import TokenStep from '../TokenStep'; +import { change, click, doAsync, submit } from '../../../../helpers/testUtils'; + +jest.mock('../../../../api/user-tokens', () => ({ + generateToken: () => Promise.resolve({ token: 'abcd1234' }), + revokeToken: () => Promise.resolve() +})); + +it('generates token', () => { + const wrapper = mount(<TokenStep open={true} onContinue={jest.fn()} />); + expect(wrapper).toMatchSnapshot(); + change(wrapper.find('input'), 'my token'); + submit(wrapper.find('form')); + expect(wrapper).toMatchSnapshot(); // spinner + return doAsync(() => expect(wrapper).toMatchSnapshot()); +}); + +it('revokes token', () => { + const wrapper = mount(<TokenStep open={true} onContinue={jest.fn()} />); + wrapper.setState({ token: 'abcd1234', tokenName: 'my token' }); + expect(wrapper).toMatchSnapshot(); + submit(wrapper.find('form')); + expect(wrapper).toMatchSnapshot(); // spinner + return doAsync(() => expect(wrapper).toMatchSnapshot()); +}); + +it('continues', () => { + const onContinue = jest.fn(); + const wrapper = mount(<TokenStep open={true} onContinue={onContinue} />); + wrapper.setState({ token: 'abcd1234', tokenName: 'my token' }); + click(wrapper.find('.js-continue')); + expect(onContinue).toBeCalledWith('abcd1234'); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/LanguageStep-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/LanguageStep-test.js.snap new file mode 100644 index 00000000000..8e82d384b3f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/LanguageStep-test.js.snap @@ -0,0 +1,687 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`selects c# 1`] = ` +<div> + <div> + <h4 + className="spacer-bottom" + > + onboarding.language + </h4> + <RadioToggle + disabled={false} + name="language" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java", + "value": "java", + }, + Object { + "label": "onboarding.language.dotnet", + "value": "dotnet", + }, + Object { + "label": "onboarding.language.other", + "value": "other", + }, + ] + } + value="dotnet" + /> + </div> + <NewProjectForm + onDelete={[Function]} + onDone={[Function]} + /> +</div> +`; + +exports[`selects c-family 1`] = ` +<div> + <div> + <h4 + className="spacer-bottom" + > + onboarding.language + </h4> + <RadioToggle + disabled={false} + name="language" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java", + "value": "java", + }, + Object { + "label": "onboarding.language.dotnet", + "value": "dotnet", + }, + Object { + "label": "onboarding.language.c-family", + "value": "c-family", + }, + Object { + "label": "onboarding.language.other", + "value": "other", + }, + ] + } + value="c-family" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.c-family.compiler + </h4> + <RadioToggle + disabled={false} + name="c-family-compiler" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.c-family.compiler.msvc", + "value": "msvc", + }, + Object { + "label": "onboarding.language.c-family.compiler.clang-gcc", + "value": "clang-gcc", + }, + ] + } + value={null} + /> + </div> +</div> +`; + +exports[`selects c-family 2`] = ` +<div> + <div> + <h4 + className="spacer-bottom" + > + onboarding.language + </h4> + <RadioToggle + disabled={false} + name="language" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java", + "value": "java", + }, + Object { + "label": "onboarding.language.dotnet", + "value": "dotnet", + }, + Object { + "label": "onboarding.language.c-family", + "value": "c-family", + }, + Object { + "label": "onboarding.language.other", + "value": "other", + }, + ] + } + value="c-family" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.c-family.compiler + </h4> + <RadioToggle + disabled={false} + name="c-family-compiler" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.c-family.compiler.msvc", + "value": "msvc", + }, + Object { + "label": "onboarding.language.c-family.compiler.clang-gcc", + "value": "clang-gcc", + }, + ] + } + value="msvc" + /> + </div> + <NewProjectForm + onDelete={[Function]} + onDone={[Function]} + /> +</div> +`; + +exports[`selects c-family 3`] = ` +<div> + <div> + <h4 + className="spacer-bottom" + > + onboarding.language + </h4> + <RadioToggle + disabled={false} + name="language" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java", + "value": "java", + }, + Object { + "label": "onboarding.language.dotnet", + "value": "dotnet", + }, + Object { + "label": "onboarding.language.c-family", + "value": "c-family", + }, + Object { + "label": "onboarding.language.other", + "value": "other", + }, + ] + } + value="c-family" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.c-family.compiler + </h4> + <RadioToggle + disabled={false} + name="c-family-compiler" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.c-family.compiler.msvc", + "value": "msvc", + }, + Object { + "label": "onboarding.language.c-family.compiler.clang-gcc", + "value": "clang-gcc", + }, + ] + } + value="clang-gcc" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.os + </h4> + <RadioToggle + disabled={false} + name="os" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.os.linux", + "value": "linux", + }, + Object { + "label": "onboarding.language.os.win", + "value": "win", + }, + Object { + "label": "onboarding.language.os.mac", + "value": "mac", + }, + ] + } + value={null} + /> + </div> +</div> +`; + +exports[`selects c-family 4`] = ` +<div> + <div> + <h4 + className="spacer-bottom" + > + onboarding.language + </h4> + <RadioToggle + disabled={false} + name="language" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java", + "value": "java", + }, + Object { + "label": "onboarding.language.dotnet", + "value": "dotnet", + }, + Object { + "label": "onboarding.language.c-family", + "value": "c-family", + }, + Object { + "label": "onboarding.language.other", + "value": "other", + }, + ] + } + value="c-family" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.c-family.compiler + </h4> + <RadioToggle + disabled={false} + name="c-family-compiler" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.c-family.compiler.msvc", + "value": "msvc", + }, + Object { + "label": "onboarding.language.c-family.compiler.clang-gcc", + "value": "clang-gcc", + }, + ] + } + value="clang-gcc" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.os + </h4> + <RadioToggle + disabled={false} + name="os" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.os.linux", + "value": "linux", + }, + Object { + "label": "onboarding.language.os.win", + "value": "win", + }, + Object { + "label": "onboarding.language.os.mac", + "value": "mac", + }, + ] + } + value="linux" + /> + </div> + <NewProjectForm + onDelete={[Function]} + onDone={[Function]} + projectKey="project-foo" + /> +</div> +`; + +exports[`selects java 1`] = ` +<div> + <div> + <h4 + className="spacer-bottom" + > + onboarding.language + </h4> + <RadioToggle + disabled={false} + name="language" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java", + "value": "java", + }, + Object { + "label": "onboarding.language.dotnet", + "value": "dotnet", + }, + Object { + "label": "onboarding.language.other", + "value": "other", + }, + ] + } + value="java" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.java.build_technology + </h4> + <RadioToggle + disabled={false} + name="java-build" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java.build_technology.maven", + "value": "maven", + }, + Object { + "label": "onboarding.language.java.build_technology.gradle", + "value": "gradle", + }, + ] + } + value={null} + /> + </div> +</div> +`; + +exports[`selects java 2`] = ` +<div> + <div> + <h4 + className="spacer-bottom" + > + onboarding.language + </h4> + <RadioToggle + disabled={false} + name="language" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java", + "value": "java", + }, + Object { + "label": "onboarding.language.dotnet", + "value": "dotnet", + }, + Object { + "label": "onboarding.language.other", + "value": "other", + }, + ] + } + value="java" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.java.build_technology + </h4> + <RadioToggle + disabled={false} + name="java-build" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java.build_technology.maven", + "value": "maven", + }, + Object { + "label": "onboarding.language.java.build_technology.gradle", + "value": "gradle", + }, + ] + } + value="maven" + /> + </div> +</div> +`; + +exports[`selects java 3`] = ` +<div> + <div> + <h4 + className="spacer-bottom" + > + onboarding.language + </h4> + <RadioToggle + disabled={false} + name="language" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java", + "value": "java", + }, + Object { + "label": "onboarding.language.dotnet", + "value": "dotnet", + }, + Object { + "label": "onboarding.language.other", + "value": "other", + }, + ] + } + value="java" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.java.build_technology + </h4> + <RadioToggle + disabled={false} + name="java-build" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java.build_technology.maven", + "value": "maven", + }, + Object { + "label": "onboarding.language.java.build_technology.gradle", + "value": "gradle", + }, + ] + } + value="gradle" + /> + </div> +</div> +`; + +exports[`selects other 1`] = ` +<div> + <div> + <h4 + className="spacer-bottom" + > + onboarding.language + </h4> + <RadioToggle + disabled={false} + name="language" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java", + "value": "java", + }, + Object { + "label": "onboarding.language.dotnet", + "value": "dotnet", + }, + Object { + "label": "onboarding.language.other", + "value": "other", + }, + ] + } + value="other" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.os + </h4> + <RadioToggle + disabled={false} + name="os" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.os.linux", + "value": "linux", + }, + Object { + "label": "onboarding.language.os.win", + "value": "win", + }, + Object { + "label": "onboarding.language.os.mac", + "value": "mac", + }, + ] + } + value={null} + /> + </div> +</div> +`; + +exports[`selects other 2`] = ` +<div> + <div> + <h4 + className="spacer-bottom" + > + onboarding.language + </h4> + <RadioToggle + disabled={false} + name="language" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.java", + "value": "java", + }, + Object { + "label": "onboarding.language.dotnet", + "value": "dotnet", + }, + Object { + "label": "onboarding.language.other", + "value": "other", + }, + ] + } + value="other" + /> + </div> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.os + </h4> + <RadioToggle + disabled={false} + name="os" + onCheck={[Function]} + options={ + Array [ + Object { + "label": "onboarding.language.os.linux", + "value": "linux", + }, + Object { + "label": "onboarding.language.os.win", + "value": "win", + }, + Object { + "label": "onboarding.language.os.mac", + "value": "mac", + }, + ] + } + value="mac" + /> + </div> + <NewProjectForm + onDelete={[Function]} + onDone={[Function]} + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewOrganizationForm-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewOrganizationForm-test.js.snap new file mode 100644 index 00000000000..c5f05454644 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewOrganizationForm-test.js.snap @@ -0,0 +1,168 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`creates new organization 1`] = ` +<NewOrganizationForm + onDelete={[Function]} + onDone={[Function]} +> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-super-large spacer-right text-middle" + maxLength={32} + minLength={2} + onChange={[Function]} + placeholder="onboarding.organization.placeholder" + required={true} + type="text" + value="" + /> + <button + className="text-middle" + disabled={true} + > + create + </button> + <div + className="note spacer-top abs-width-300" + > + onboarding.organization.key_requirement + </div> + </form> +</NewOrganizationForm> +`; + +exports[`creates new organization 2`] = ` +<NewOrganizationForm + onDelete={[Function]} + onDone={[Function]} +> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-super-large spacer-right text-middle" + maxLength={32} + minLength={2} + onChange={[Function]} + placeholder="onboarding.organization.placeholder" + required={true} + type="text" + value="foo" + /> + <i + className="spinner" + /> + <div + className="note spacer-top abs-width-300" + > + onboarding.organization.key_requirement + </div> + </form> +</NewOrganizationForm> +`; + +exports[`creates new organization 3`] = ` +<NewOrganizationForm + onDelete={[Function]} + onDone={[Function]} +> + <form + onSubmit={[Function]} + > + <span + className="spacer-right text-middle" + > + foo + </span> + <button + className="button-clean" + > + <i + className="icon-delete" + /> + </button> + </form> +</NewOrganizationForm> +`; + +exports[`deletes organization 1`] = ` +<NewOrganizationForm + onDelete={[Function]} + onDone={[Function]} +> + <form + onSubmit={[Function]} + > + <span + className="spacer-right text-middle" + > + foo + </span> + <button + className="button-clean" + > + <i + className="icon-delete" + /> + </button> + </form> +</NewOrganizationForm> +`; + +exports[`deletes organization 2`] = ` +<NewOrganizationForm + onDelete={[Function]} + onDone={[Function]} +> + <form + onSubmit={[Function]} + > + <span + className="spacer-right text-middle" + > + foo + </span> + <i + className="spinner" + /> + </form> +</NewOrganizationForm> +`; + +exports[`deletes organization 3`] = ` +<NewOrganizationForm + onDelete={[Function]} + onDone={[Function]} +> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-super-large spacer-right text-middle" + maxLength={32} + minLength={2} + onChange={[Function]} + placeholder="onboarding.organization.placeholder" + required={true} + type="text" + value="" + /> + <button + className="text-middle" + disabled={true} + > + create + </button> + <div + className="note spacer-top abs-width-300" + > + onboarding.organization.key_requirement + </div> + </form> +</NewOrganizationForm> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewProjectForm-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewProjectForm-test.js.snap new file mode 100644 index 00000000000..50ef33521cc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/NewProjectForm-test.js.snap @@ -0,0 +1,219 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`creates new project 1`] = ` +<NewProjectForm + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right text-middle" + maxLength={400} + minLength={1} + onChange={[Function]} + required={true} + type="text" + value="" + /> + <button + className="text-middle" + disabled={true} + > + Done + </button> + <div + className="note spacer-top abs-width-300" + > + onboarding.project_key_requirement + </div> + </form> + </div> +</NewProjectForm> +`; + +exports[`creates new project 2`] = ` +<NewProjectForm + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right text-middle" + maxLength={400} + minLength={1} + onChange={[Function]} + required={true} + type="text" + value="foo" + /> + <i + className="spinner" + /> + <div + className="note spacer-top abs-width-300" + > + onboarding.project_key_requirement + </div> + </form> + </div> +</NewProjectForm> +`; + +exports[`creates new project 3`] = ` +<NewProjectForm + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <span + className="spacer-right text-middle" + > + foo + </span> + <button + className="button-clean" + > + <i + className="icon-delete" + /> + </button> + </form> + </div> +</NewProjectForm> +`; + +exports[`deletes project 1`] = ` +<NewProjectForm + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <span + className="spacer-right text-middle" + > + foo + </span> + <button + className="button-clean" + > + <i + className="icon-delete" + /> + </button> + </form> + </div> +</NewProjectForm> +`; + +exports[`deletes project 2`] = ` +<NewProjectForm + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <span + className="spacer-right text-middle" + > + foo + </span> + <i + className="spinner" + /> + </form> + </div> +</NewProjectForm> +`; + +exports[`deletes project 3`] = ` +<NewProjectForm + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right text-middle" + maxLength={400} + minLength={1} + onChange={[Function]} + required={true} + type="text" + value="" + /> + <button + className="text-middle" + disabled={true} + > + Done + </button> + <div + className="note spacer-top abs-width-300" + > + onboarding.project_key_requirement + </div> + </form> + </div> +</NewProjectForm> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap new file mode 100644 index 00000000000..cbfbf1da7ca --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectKeyStep-test.js.snap @@ -0,0 +1,219 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`creates new project 1`] = ` +<ProjectKeyStep + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right text-middle" + maxLength={400} + minLength={1} + onChange={[Function]} + required={true} + type="text" + value="" + /> + <button + className="text-middle" + disabled={true} + > + Done + </button> + <div + className="note spacer-top abs-width-300" + > + onboarding.project_key_requirement + </div> + </form> + </div> +</ProjectKeyStep> +`; + +exports[`creates new project 2`] = ` +<ProjectKeyStep + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right text-middle" + maxLength={400} + minLength={1} + onChange={[Function]} + required={true} + type="text" + value="foo" + /> + <i + className="spinner" + /> + <div + className="note spacer-top abs-width-300" + > + onboarding.project_key_requirement + </div> + </form> + </div> +</ProjectKeyStep> +`; + +exports[`creates new project 3`] = ` +<ProjectKeyStep + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <span + className="spacer-right text-middle" + > + foo + </span> + <button + className="button-clean" + > + <i + className="icon-delete" + /> + </button> + </form> + </div> +</ProjectKeyStep> +`; + +exports[`deletes project 1`] = ` +<ProjectKeyStep + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <span + className="spacer-right text-middle" + > + foo + </span> + <button + className="button-clean" + > + <i + className="icon-delete" + /> + </button> + </form> + </div> +</ProjectKeyStep> +`; + +exports[`deletes project 2`] = ` +<ProjectKeyStep + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <span + className="spacer-right text-middle" + > + foo + </span> + <i + className="spinner" + /> + </form> + </div> +</ProjectKeyStep> +`; + +exports[`deletes project 3`] = ` +<ProjectKeyStep + onDelete={[Function]} + onDone={[Function]} +> + <div + className="big-spacer-top" + > + <h4 + className="spacer-bottom" + > + onboarding.language.project_key + </h4> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right text-middle" + maxLength={400} + minLength={1} + onChange={[Function]} + required={true} + type="text" + value="" + /> + <button + className="text-middle" + disabled={true} + > + Done + </button> + <div + className="note spacer-top abs-width-300" + > + onboarding.project_key_requirement + </div> + </form> + </div> +</ProjectKeyStep> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Step-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Step-test.js.snap new file mode 100644 index 00000000000..1df068a503c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Step-test.js.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +<div + className="boxed-group onboarding-step onboarding-step-open" +> + <div + className="onboarding-step-number" + > + 1 + </div> + <div + className="boxed-group-header" + > + <h2> + First Step + </h2> + </div> + <div> + form + </div> +</div> +`; + +exports[`renders 2`] = ` +<div + className="boxed-group onboarding-step" +> + <div + className="onboarding-step-number" + > + 1 + </div> + <div> + result + </div> + <div + className="boxed-group-header" + > + <h2> + First Step + </h2> + </div> + <div + className="boxed-group-inner" + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap new file mode 100644 index 00000000000..73ae3896df4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap @@ -0,0 +1,383 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generates token 1`] = ` +<TokenStep + onContinue={[Function]} + open={true} + stepNumber={1} +> + <Step + open={true} + renderForm={[Function]} + renderResult={[Function]} + stepNumber={1} + stepTitle="onboarding.token.header" + > + <div + className="boxed-group onboarding-step onboarding-step-open" + > + <div + className="onboarding-step-number" + > + 1 + </div> + <div + className="boxed-group-header" + > + <h2> + onboarding.token.header + </h2> + </div> + <div + className="boxed-group-inner" + > + <div + className="big-spacer-bottom width-50" + > + onboarding.token.text + </div> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right" + onChange={[Function]} + placeholder="onboarding.token.placeholder" + required={true} + type="text" + value="" + /> + <button> + onboarding.token.generate + </button> + </form> + </div> + </div> + </Step> +</TokenStep> +`; + +exports[`generates token 2`] = ` +<TokenStep + onContinue={[Function]} + open={true} + stepNumber={1} +> + <Step + open={true} + renderForm={[Function]} + renderResult={[Function]} + stepNumber={1} + stepTitle="onboarding.token.header" + > + <div + className="boxed-group onboarding-step onboarding-step-open" + > + <div + className="onboarding-step-number" + > + 1 + </div> + <div + className="boxed-group-header" + > + <h2> + onboarding.token.header + </h2> + </div> + <div + className="boxed-group-inner" + > + <div + className="big-spacer-bottom width-50" + > + onboarding.token.text + </div> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right" + onChange={[Function]} + placeholder="onboarding.token.placeholder" + required={true} + type="text" + value="my token" + /> + <i + className="spinner" + /> + </form> + </div> + </div> + </Step> +</TokenStep> +`; + +exports[`generates token 3`] = ` +<TokenStep + onContinue={[Function]} + open={true} + stepNumber={1} +> + <Step + open={true} + renderForm={[Function]} + renderResult={[Function]} + stepNumber={1} + stepTitle="onboarding.token.header" + > + <div + className="boxed-group onboarding-step onboarding-step-open" + > + <div + className="onboarding-step-number" + > + 1 + </div> + <div + className="boxed-group-header" + > + <h2> + onboarding.token.header + </h2> + </div> + <div + className="boxed-group-inner" + > + <div + className="big-spacer-bottom width-50" + > + onboarding.token.text + </div> + <form + onSubmit={[Function]} + > + my token + : + <span + className="monospaced spacer-right" + > + abcd1234 + </span> + <button + className="button-clean" + onClick={[Function]} + > + <i + className="icon-delete" + /> + </button> + </form> + <div + className="big-spacer-top" + > + <button + className="js-continue" + onClick={[Function]} + > + continue + </button> + </div> + </div> + </div> + </Step> +</TokenStep> +`; + +exports[`revokes token 1`] = ` +<TokenStep + onContinue={[Function]} + open={true} + stepNumber={1} +> + <Step + open={true} + renderForm={[Function]} + renderResult={[Function]} + stepNumber={1} + stepTitle="onboarding.token.header" + > + <div + className="boxed-group onboarding-step onboarding-step-open" + > + <div + className="onboarding-step-number" + > + 1 + </div> + <div + className="boxed-group-header" + > + <h2> + onboarding.token.header + </h2> + </div> + <div + className="boxed-group-inner" + > + <div + className="big-spacer-bottom width-50" + > + onboarding.token.text + </div> + <form + onSubmit={[Function]} + > + my token + : + <span + className="monospaced spacer-right" + > + abcd1234 + </span> + <button + className="button-clean" + onClick={[Function]} + > + <i + className="icon-delete" + /> + </button> + </form> + <div + className="big-spacer-top" + > + <button + className="js-continue" + onClick={[Function]} + > + continue + </button> + </div> + </div> + </div> + </Step> +</TokenStep> +`; + +exports[`revokes token 2`] = ` +<TokenStep + onContinue={[Function]} + open={true} + stepNumber={1} +> + <Step + open={true} + renderForm={[Function]} + renderResult={[Function]} + stepNumber={1} + stepTitle="onboarding.token.header" + > + <div + className="boxed-group onboarding-step onboarding-step-open" + > + <div + className="onboarding-step-number" + > + 1 + </div> + <div + className="boxed-group-header" + > + <h2> + onboarding.token.header + </h2> + </div> + <div + className="boxed-group-inner" + > + <div + className="big-spacer-bottom width-50" + > + onboarding.token.text + </div> + <form + onSubmit={[Function]} + > + my token + : + <span + className="monospaced spacer-right" + > + abcd1234 + </span> + <i + className="spinner" + /> + </form> + <div + className="big-spacer-top" + > + <button + className="js-continue" + onClick={[Function]} + > + continue + </button> + </div> + </div> + </div> + </Step> +</TokenStep> +`; + +exports[`revokes token 3`] = ` +<TokenStep + onContinue={[Function]} + open={true} + stepNumber={1} +> + <Step + open={true} + renderForm={[Function]} + renderResult={[Function]} + stepNumber={1} + stepTitle="onboarding.token.header" + > + <div + className="boxed-group onboarding-step onboarding-step-open" + > + <div + className="onboarding-step-number" + > + 1 + </div> + <div + className="boxed-group-header" + > + <h2> + onboarding.token.header + </h2> + </div> + <div + className="boxed-group-inner" + > + <div + className="big-spacer-bottom width-50" + > + onboarding.token.text + </div> + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right" + onChange={[Function]} + placeholder="onboarding.token.placeholder" + required={true} + type="text" + value="" + /> + <button> + onboarding.token.generate + </button> + </form> + </div> + </div> + </Step> +</TokenStep> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/BuildWrapper.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/BuildWrapper.js new file mode 100644 index 00000000000..3b0624e4629 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/BuildWrapper.js @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { translate } from '../../../../helpers/l10n'; + +type Props = { + className?: string, + os: string +}; + +const filenames = { + linux: 'build-wrapper-win-x86.zip', + win: 'build-wrapper-linux-x86.zip', + mac: 'build-wrapper-macosx-x86.zip' +}; + +export default function BuildWrapper(props: Props) { + return ( + <div className={props.className}> + <h4 className="spacer-bottom"> + {translate('onboarding.analysis.build_wrapper.header', props.os)} + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={{ + __html: translate('onboarding.analysis.build_wrapper.text', props.os) + }} + /> + <p> + <a + className="button" + download={filenames[props.os]} + href={window.baseUrl + '/static/cpp/' + filenames[props.os]} + target="_blank"> + {translate('download_verb')} + </a> + </p> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/ClangGCC.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/ClangGCC.js new file mode 100644 index 00000000000..7940263931c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/ClangGCC.js @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Command from './Command'; +import SQScanner from './SQScanner'; +import BuildWrapper from './BuildWrapper'; +import { translate } from '../../../../helpers/l10n'; + +type Props = { + host: string, + os: string, + organization?: string, + projectKey: string, + token: string +}; + +const executables = { + linux: 'build-wrapper-linux-x86-64', + win: 'build-wrapper-win-x86-64.exe', + mac: 'build-wrapper-macosx-x86' +}; + +export default function ClangGCC(props: Props) { + const command1 = `${executables[props.os]} --out-dir bw-output make clean all`; + + const command2 = [ + props.os === 'win' ? 'sonar-scanner.bat' : 'sonar-scanner', + `-Dsonar.projectKey=${props.projectKey}`, + props.organization && `-Dsonar.organization=${props.organization}`, + '-Dsonar.sources=.', + '-Dsonar.cfamily.build-wrapper-output=bw-output', + `-Dsonar.host.url=${props.host}`, + `-Dsonar.login=${props.token}` + ]; + + return ( + <div> + <SQScanner os={props.os} /> + <BuildWrapper className="huge-spacer-top" os={props.os} /> + + <h4 className="huge-spacer-top spacer-bottom"> + {translate('onboarding.analysis.sq_scanner.execute')} + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={{ + __html: translate('onboarding.analysis.sq_scanner.execute.text') + }} + /> + <Command command={command1} /> + <Command command={command2} /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.sq_scanner.docs') }} + /> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Command.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Command.js new file mode 100644 index 00000000000..ec9f980083d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Command.js @@ -0,0 +1,89 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Clipboard from 'clipboard'; +import Tooltip from '../../../../components/controls/Tooltip'; +import { translate } from '../../../../helpers/l10n'; + +type Props = { + command: string | Array<?string> +}; + +type State = { + tooltipShown: boolean +}; + +const s = ' \\' + '\n '; + +export default class Command extends React.PureComponent { + clipboard: Object; + copyButton: HTMLButtonElement; + mounted: boolean; + props: Props; + state: State = { tooltipShown: false }; + + componentDidMount() { + this.mounted = true; + this.clipboard = new Clipboard(this.copyButton); + this.clipboard.on('success', this.showTooltip); + } + + componentWillUnmount() { + this.mounted = false; + this.clipboard.destroy(); + } + + showTooltip = () => { + if (this.mounted) { + this.setState({ tooltipShown: true }); + setTimeout(this.hideTooltip, 1000); + } + }; + + hideTooltip = () => { + if (this.mounted) { + this.setState({ tooltipShown: false }); + } + }; + + render() { + const { command } = this.props; + const commandArray = Array.isArray(command) ? command.filter(line => line != null) : [command]; + const finalCommand = commandArray.join(s); + + const button = ( + <button data-clipboard-text={finalCommand} ref={node => (this.copyButton = node)}> + {translate('copy')} + </button> + ); + + return ( + <div className="onboarding-command"> + <pre>{finalCommand}</pre> + {this.state.tooltipShown + ? <Tooltip defaultVisible={true} placement="top" overlay="Copied!" trigger="manual"> + {button} + </Tooltip> + : button} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/DotNet.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/DotNet.js new file mode 100644 index 00000000000..8885458953b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/DotNet.js @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Command from './Command'; +import MSBuildScanner from './MSBuildScanner'; +import { translate } from '../../../../helpers/l10n'; + +type Props = {| + host: string, + organization?: string, + projectKey: string, + token: string +|}; + +export default function DotNet(props: Props) { + const command1 = [ + 'SonarQube.Scanner.MSBuild.exe begin', + `/k:"${props.projectKey}"`, + props.organization && `/d:"sonar.organization=${props.organization}"`, + `/d:"sonar.host.url=${props.host}`, + `/d:"sonar.login=${props.token}"` + ]; + + const command2 = 'MsBuild.exe /t:Rebuild'; + + const command3 = ['SonarQube.Scanner.MSBuild.exe end', `/d:"sonar.login=${props.token}"`]; + + return ( + <div> + <MSBuildScanner /> + + <h4 className="huge-spacer-top spacer-bottom"> + {translate('onboarding.analysis.msbuild.execute')} + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={{ + __html: translate('onboarding.analysis.msbuild.execute.text') + }} + /> + <Command command={command1} /> + <Command command={command2} /> + <Command command={command3} /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.msbuild.docs') }} + /> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaGradle.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaGradle.js new file mode 100644 index 00000000000..3f89a2bb241 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaGradle.js @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Command from './Command'; +import { translate } from '../../../../helpers/l10n'; + +type Props = {| + host: string, + organization?: string, + token: string +|}; + +export default function JavaGradle(props: Props) { + const config = 'plugins {\n id "org.sonarqube" version "2.2"\n}'; + + const command = [ + './gradlew sonarqube', + props.organization && `-Dsonar.organization=${props.organization}`, + `-Dsonar.host.url=${props.host}`, + `-Dsonar.login=${props.token}` + ]; + + return ( + <div> + <h4 className="spacer-bottom">{translate('onboarding.analysis.java.gradle.header')}</h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.java.gradle.text.1') }} + /> + <Command command={config} /> + <p className="spacer-top spacer-bottom markdown"> + {translate('onboarding.analysis.java.gradle.text.2')} + </p> + <Command command={command} /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.java.gradle.docs') }} + /> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaMaven.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaMaven.js new file mode 100644 index 00000000000..6317b1bac2e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/JavaMaven.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Command from './Command'; +import { translate } from '../../../../helpers/l10n'; + +type Props = {| + host: string, + organization?: string, + token: string +|}; + +export default function JavaMaven(props: Props) { + const command = [ + 'mvn sonar:sonar', + props.organization && `-Dsonar.organization=${props.organization}`, + `-Dsonar.host.url=${props.host}`, + `-Dsonar.login=${props.token}` + ]; + + return ( + <div> + <h4 className="spacer-bottom">{translate('onboarding.analysis.java.maven.header')}</h4> + <p className="spacer-bottom markdown">{translate('onboarding.analysis.java.maven.text')}</p> + <Command command={command} /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.java.maven.docs') }} + /> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/MSBuildScanner.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/MSBuildScanner.js new file mode 100644 index 00000000000..3be38395ca9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/MSBuildScanner.js @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { translate } from '../../../../helpers/l10n'; + +type Props = { + className?: string +}; + +export default function MSBuildScanner(props: Props) { + return ( + <div className={props.className}> + <h4 className="spacer-bottom">{translate('onboarding.analysis.msbuild.header')}</h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.msbuild.text') }} + /> + <p> + <a + className="button" + href="http://redirect.sonarsource.com/doc/install-configure-scanner-msbuild.html" + target="_blank"> + {translate('download_verb')} + </a> + </p> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Msvc.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Msvc.js new file mode 100644 index 00000000000..00fba90474a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Msvc.js @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Command from './Command'; +import MSBuildScanner from './MSBuildScanner'; +import BuildWrapper from './BuildWrapper'; +import { translate } from '../../../../helpers/l10n'; + +type Props = {| + host: string, + organization?: string, + projectKey: string, + token: string +|}; + +export default function Msvc(props: Props) { + const command1 = [ + 'SonarQube.Scanner.MSBuild.exe begin', + `/k:"${props.projectKey}"`, + props.organization && `/d:"sonar.organization=${props.organization}"`, + '/d:"sonar.cfamily.build-wrapper-output=bw-output"', + `/d:"sonar.host.url=${props.host}`, + `/d:"sonar.login=${props.token}"` + ]; + + const command2 = 'build-wrapper-win-x86-64.exe --out-dir bw-output MsBuild.exe /t:Rebuild'; + + const command3 = ['SonarQube.Scanner.MSBuild.exe end', `/d:"sonar.login=${props.token}"`]; + + return ( + <div> + <MSBuildScanner /> + <BuildWrapper className="huge-spacer-top" os="win" /> + + <h4 className="huge-spacer-top spacer-bottom"> + {translate('onboarding.analysis.msbuild.execute')} + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={{ + __html: translate('onboarding.analysis.msbuild.execute.text') + }} + /> + <Command command={command1} /> + <Command command={command2} /> + <Command command={command3} /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.msbuild.docs') }} + /> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Other.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Other.js new file mode 100644 index 00000000000..534be2f8584 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/Other.js @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Command from './Command'; +import SQScanner from './SQScanner'; +import { translate } from '../../../../helpers/l10n'; + +type Props = {| + host: string, + organization?: string, + os: string, + projectKey: string, + token: string +|}; + +export default function Other(props: Props) { + const command = [ + props.os === 'win' ? 'sonar-scanner.bat' : 'sonar-scanner', + `-Dsonar.projectKey=${props.projectKey}`, + props.organization && `-Dsonar.organization=${props.organization}`, + '-Dsonar.sources=.', + `-Dsonar.host.url=${props.host}`, + `-Dsonar.login=${props.token}` + ]; + + return ( + <div> + <SQScanner os={props.os} /> + + <h4 className="huge-spacer-top spacer-bottom"> + {translate('onboarding.analysis.sq_scanner.execute')} + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={{ + __html: translate('onboarding.analysis.sq_scanner.execute.text') + }} + /> + <Command command={command} /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.sq_scanner.docs') }} + /> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/SQScanner.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/SQScanner.js new file mode 100644 index 00000000000..d36c6379259 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/SQScanner.js @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { translate } from '../../../../helpers/l10n'; + +type Props = { + className?: string, + os: string +}; + +export default function SQScanner(props: Props) { + return ( + <div className={props.className}> + <h4 className="spacer-bottom"> + {translate('onboarding.analysis.sq_scanner.header', props.os)} + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={{ + __html: translate('onboarding.analysis.sq_scanner.text', props.os) + }} + /> + <p> + <a + className="button" + href="http://redirect.sonarsource.com/doc/install-configure-scanner.html" + target="_blank"> + {translate('download_verb')} + </a> + </p> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/BuildWrapper-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/BuildWrapper-test.js new file mode 100644 index 00000000000..2135abc7606 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/BuildWrapper-test.js @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import BuildWrapper from '../BuildWrapper'; + +it('renders correctly', () => { + expect(shallow(<BuildWrapper os="win" />)).toMatchSnapshot(); + expect(shallow(<BuildWrapper os="linux" />)).toMatchSnapshot(); + expect(shallow(<BuildWrapper os="mac" />)).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/ClangGCC-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/ClangGCC-test.js new file mode 100644 index 00000000000..16c458660b8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/ClangGCC-test.js @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import ClangGCC from '../ClangGCC'; + +it('renders correctly', () => { + expect( + shallow(<ClangGCC host="host" os="win" projectKey="projectKey" token="token" />) + ).toMatchSnapshot(); + + expect( + shallow(<ClangGCC host="host" os="linux" projectKey="projectKey" token="token" />) + ).toMatchSnapshot(); + + expect( + shallow( + <ClangGCC + host="host" + os="linux" + organization="organization" + projectKey="projectKey" + token="token" + /> + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Command-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Command-test.js new file mode 100644 index 00000000000..19ee425c791 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Command-test.js @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import Command from '../Command'; + +it('renders correctly', () => { + expect(shallow(<Command command={'foo\nbar'} />)).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/DotNet-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/DotNet-test.js new file mode 100644 index 00000000000..f1e320e2b3f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/DotNet-test.js @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import DotNet from '../DotNet'; + +it('renders correctly', () => { + expect(shallow(<DotNet host="host" projectKey="projectKey" token="token" />)).toMatchSnapshot(); + expect( + shallow( + <DotNet host="host" organization="organization" projectKey="projectKey" token="token" /> + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaGradle-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaGradle-test.js new file mode 100644 index 00000000000..ef326ddc8c9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaGradle-test.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import JavaGradle from '../JavaGradle'; + +it('renders correctly', () => { + expect(shallow(<JavaGradle host="host" token="token" />)).toMatchSnapshot(); + expect( + shallow(<JavaGradle host="host" organization="organization" token="token" />) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaMaven-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaMaven-test.js new file mode 100644 index 00000000000..bb24f89e611 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/JavaMaven-test.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import JavaMaven from '../JavaMaven'; + +it('renders correctly', () => { + expect(shallow(<JavaMaven host="host" token="token" />)).toMatchSnapshot(); + expect( + shallow(<JavaMaven host="host" organization="organization" token="token" />) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/MSBuildScanner-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/MSBuildScanner-test.js new file mode 100644 index 00000000000..e2c22bc157e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/MSBuildScanner-test.js @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import MSBuildScanner from '../MSBuildScanner'; + +it('renders correctly', () => { + expect(shallow(<MSBuildScanner />)).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Msvc-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Msvc-test.js new file mode 100644 index 00000000000..28bfa947aa6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Msvc-test.js @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import Msvc from '../Msvc'; + +it('renders correctly', () => { + expect(shallow(<Msvc host="host" projectKey="projectKey" token="token" />)).toMatchSnapshot(); + expect( + shallow(<Msvc host="host" organization="organization" projectKey="projectKey" token="token" />) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Other-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Other-test.js new file mode 100644 index 00000000000..0eda78f2f94 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/Other-test.js @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import Other from '../Other'; + +it('renders correctly', () => { + expect( + shallow(<Other host="host" os="win" projectKey="projectKey" token="token" />) + ).toMatchSnapshot(); + + expect( + shallow(<Other host="host" os="linux" projectKey="projectKey" token="token" />) + ).toMatchSnapshot(); + + expect( + shallow( + <Other + host="host" + os="linux" + organization="organization" + projectKey="projectKey" + token="token" + /> + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/SQScanner-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/SQScanner-test.js new file mode 100644 index 00000000000..3b2881377ad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/SQScanner-test.js @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { shallow } from 'enzyme'; +import SQScanner from '../SQScanner'; + +it('renders correctly', () => { + expect(shallow(<SQScanner os="win" />)).toMatchSnapshot(); + expect(shallow(<SQScanner os="linux" />)).toMatchSnapshot(); + expect(shallow(<SQScanner os="mac" />)).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/BuildWrapper-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/BuildWrapper-test.js.snap new file mode 100644 index 00000000000..4a1e0c3944c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/BuildWrapper-test.js.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.build_wrapper.header.win + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.build_wrapper.text.win", + } + } + /> + <p> + <a + className="button" + download="build-wrapper-linux-x86.zip" + href="/static/cpp/build-wrapper-linux-x86.zip" + target="_blank" + > + download_verb + </a> + </p> +</div> +`; + +exports[`renders correctly 2`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.build_wrapper.header.linux + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.build_wrapper.text.linux", + } + } + /> + <p> + <a + className="button" + download="build-wrapper-win-x86.zip" + href="/static/cpp/build-wrapper-win-x86.zip" + target="_blank" + > + download_verb + </a> + </p> +</div> +`; + +exports[`renders correctly 3`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.build_wrapper.header.mac + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.build_wrapper.text.mac", + } + } + /> + <p> + <a + className="button" + download="build-wrapper-macosx-x86.zip" + href="/static/cpp/build-wrapper-macosx-x86.zip" + target="_blank" + > + download_verb + </a> + </p> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/ClangGCC-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/ClangGCC-test.js.snap new file mode 100644 index 00000000000..1ec053c4083 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/ClangGCC-test.js.snap @@ -0,0 +1,148 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div> + <SQScanner + os="win" + /> + <BuildWrapper + className="huge-spacer-top" + os="win" + /> + <h4 + className="huge-spacer-top spacer-bottom" + > + onboarding.analysis.sq_scanner.execute + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.execute.text", + } + } + /> + <Command + command="build-wrapper-win-x86-64.exe --out-dir bw-output make clean all" + /> + <Command + command={ + Array [ + "sonar-scanner.bat", + "-Dsonar.projectKey=projectKey", + undefined, + "-Dsonar.sources=.", + "-Dsonar.cfamily.build-wrapper-output=bw-output", + "-Dsonar.host.url=host", + "-Dsonar.login=token", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.docs", + } + } + /> +</div> +`; + +exports[`renders correctly 2`] = ` +<div> + <SQScanner + os="linux" + /> + <BuildWrapper + className="huge-spacer-top" + os="linux" + /> + <h4 + className="huge-spacer-top spacer-bottom" + > + onboarding.analysis.sq_scanner.execute + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.execute.text", + } + } + /> + <Command + command="build-wrapper-linux-x86-64 --out-dir bw-output make clean all" + /> + <Command + command={ + Array [ + "sonar-scanner", + "-Dsonar.projectKey=projectKey", + undefined, + "-Dsonar.sources=.", + "-Dsonar.cfamily.build-wrapper-output=bw-output", + "-Dsonar.host.url=host", + "-Dsonar.login=token", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.docs", + } + } + /> +</div> +`; + +exports[`renders correctly 3`] = ` +<div> + <SQScanner + os="linux" + /> + <BuildWrapper + className="huge-spacer-top" + os="linux" + /> + <h4 + className="huge-spacer-top spacer-bottom" + > + onboarding.analysis.sq_scanner.execute + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.execute.text", + } + } + /> + <Command + command="build-wrapper-linux-x86-64 --out-dir bw-output make clean all" + /> + <Command + command={ + Array [ + "sonar-scanner", + "-Dsonar.projectKey=projectKey", + "-Dsonar.organization=organization", + "-Dsonar.sources=.", + "-Dsonar.cfamily.build-wrapper-output=bw-output", + "-Dsonar.host.url=host", + "-Dsonar.login=token", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.docs", + } + } + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Command-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Command-test.js.snap new file mode 100644 index 00000000000..9ce1c887f5f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Command-test.js.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div + className="onboarding-command" +> + <pre> + foo + bar + </pre> + <button + data-clipboard-text="foo + bar" + > + copy + </button> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/DotNet-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/DotNet-test.js.snap new file mode 100644 index 00000000000..3672a2f4c8d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/DotNet-test.js.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div> + <MSBuildScanner /> + <h4 + className="huge-spacer-top spacer-bottom" + > + onboarding.analysis.msbuild.execute + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.msbuild.execute.text", + } + } + /> + <Command + command={ + Array [ + "SonarQube.Scanner.MSBuild.exe begin", + "/k:\\"projectKey\\"", + undefined, + "/d:\\"sonar.host.url=host", + "/d:\\"sonar.login=token\\"", + ] + } + /> + <Command + command="MsBuild.exe /t:Rebuild" + /> + <Command + command={ + Array [ + "SonarQube.Scanner.MSBuild.exe end", + "/d:\\"sonar.login=token\\"", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.msbuild.docs", + } + } + /> +</div> +`; + +exports[`renders correctly 2`] = ` +<div> + <MSBuildScanner /> + <h4 + className="huge-spacer-top spacer-bottom" + > + onboarding.analysis.msbuild.execute + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.msbuild.execute.text", + } + } + /> + <Command + command={ + Array [ + "SonarQube.Scanner.MSBuild.exe begin", + "/k:\\"projectKey\\"", + "/d:\\"sonar.organization=organization\\"", + "/d:\\"sonar.host.url=host", + "/d:\\"sonar.login=token\\"", + ] + } + /> + <Command + command="MsBuild.exe /t:Rebuild" + /> + <Command + command={ + Array [ + "SonarQube.Scanner.MSBuild.exe end", + "/d:\\"sonar.login=token\\"", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.msbuild.docs", + } + } + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaGradle-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaGradle-test.js.snap new file mode 100644 index 00000000000..df6bf9db47f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaGradle-test.js.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.java.gradle.header + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.java.gradle.text.1", + } + } + /> + <Command + command="plugins { + id \\"org.sonarqube\\" version \\"2.2\\" + }" + /> + <p + className="spacer-top spacer-bottom markdown" + > + onboarding.analysis.java.gradle.text.2 + </p> + <Command + command={ + Array [ + "./gradlew sonarqube", + undefined, + "-Dsonar.host.url=host", + "-Dsonar.login=token", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.java.gradle.docs", + } + } + /> +</div> +`; + +exports[`renders correctly 2`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.java.gradle.header + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.java.gradle.text.1", + } + } + /> + <Command + command="plugins { + id \\"org.sonarqube\\" version \\"2.2\\" + }" + /> + <p + className="spacer-top spacer-bottom markdown" + > + onboarding.analysis.java.gradle.text.2 + </p> + <Command + command={ + Array [ + "./gradlew sonarqube", + "-Dsonar.organization=organization", + "-Dsonar.host.url=host", + "-Dsonar.login=token", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.java.gradle.docs", + } + } + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaMaven-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaMaven-test.js.snap new file mode 100644 index 00000000000..3cd4796988b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/JavaMaven-test.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.java.maven.header + </h4> + <p + className="spacer-bottom markdown" + > + onboarding.analysis.java.maven.text + </p> + <Command + command={ + Array [ + "mvn sonar:sonar", + undefined, + "-Dsonar.host.url=host", + "-Dsonar.login=token", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.java.maven.docs", + } + } + /> +</div> +`; + +exports[`renders correctly 2`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.java.maven.header + </h4> + <p + className="spacer-bottom markdown" + > + onboarding.analysis.java.maven.text + </p> + <Command + command={ + Array [ + "mvn sonar:sonar", + "-Dsonar.organization=organization", + "-Dsonar.host.url=host", + "-Dsonar.login=token", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.java.maven.docs", + } + } + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.js.snap new file mode 100644 index 00000000000..c7f156bc53f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/MSBuildScanner-test.js.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.msbuild.header + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.msbuild.text", + } + } + /> + <p> + <a + className="button" + href="http://redirect.sonarsource.com/doc/install-configure-scanner-msbuild.html" + target="_blank" + > + download_verb + </a> + </p> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Msvc-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Msvc-test.js.snap new file mode 100644 index 00000000000..c2f54b40496 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Msvc-test.js.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div> + <MSBuildScanner /> + <BuildWrapper + className="huge-spacer-top" + os="win" + /> + <h4 + className="huge-spacer-top spacer-bottom" + > + onboarding.analysis.msbuild.execute + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.msbuild.execute.text", + } + } + /> + <Command + command={ + Array [ + "SonarQube.Scanner.MSBuild.exe begin", + "/k:\\"projectKey\\"", + undefined, + "/d:\\"sonar.cfamily.build-wrapper-output=bw-output\\"", + "/d:\\"sonar.host.url=host", + "/d:\\"sonar.login=token\\"", + ] + } + /> + <Command + command="build-wrapper-win-x86-64.exe --out-dir bw-output MsBuild.exe /t:Rebuild" + /> + <Command + command={ + Array [ + "SonarQube.Scanner.MSBuild.exe end", + "/d:\\"sonar.login=token\\"", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.msbuild.docs", + } + } + /> +</div> +`; + +exports[`renders correctly 2`] = ` +<div> + <MSBuildScanner /> + <BuildWrapper + className="huge-spacer-top" + os="win" + /> + <h4 + className="huge-spacer-top spacer-bottom" + > + onboarding.analysis.msbuild.execute + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.msbuild.execute.text", + } + } + /> + <Command + command={ + Array [ + "SonarQube.Scanner.MSBuild.exe begin", + "/k:\\"projectKey\\"", + "/d:\\"sonar.organization=organization\\"", + "/d:\\"sonar.cfamily.build-wrapper-output=bw-output\\"", + "/d:\\"sonar.host.url=host", + "/d:\\"sonar.login=token\\"", + ] + } + /> + <Command + command="build-wrapper-win-x86-64.exe --out-dir bw-output MsBuild.exe /t:Rebuild" + /> + <Command + command={ + Array [ + "SonarQube.Scanner.MSBuild.exe end", + "/d:\\"sonar.login=token\\"", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.msbuild.docs", + } + } + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Other-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Other-test.js.snap new file mode 100644 index 00000000000..699ff84d414 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/Other-test.js.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div> + <SQScanner + os="win" + /> + <h4 + className="huge-spacer-top spacer-bottom" + > + onboarding.analysis.sq_scanner.execute + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.execute.text", + } + } + /> + <Command + command={ + Array [ + "sonar-scanner.bat", + "-Dsonar.projectKey=projectKey", + undefined, + "-Dsonar.sources=.", + "-Dsonar.host.url=host", + "-Dsonar.login=token", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.docs", + } + } + /> +</div> +`; + +exports[`renders correctly 2`] = ` +<div> + <SQScanner + os="linux" + /> + <h4 + className="huge-spacer-top spacer-bottom" + > + onboarding.analysis.sq_scanner.execute + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.execute.text", + } + } + /> + <Command + command={ + Array [ + "sonar-scanner", + "-Dsonar.projectKey=projectKey", + undefined, + "-Dsonar.sources=.", + "-Dsonar.host.url=host", + "-Dsonar.login=token", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.docs", + } + } + /> +</div> +`; + +exports[`renders correctly 3`] = ` +<div> + <SQScanner + os="linux" + /> + <h4 + className="huge-spacer-top spacer-bottom" + > + onboarding.analysis.sq_scanner.execute + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.execute.text", + } + } + /> + <Command + command={ + Array [ + "sonar-scanner", + "-Dsonar.projectKey=projectKey", + "-Dsonar.organization=organization", + "-Dsonar.sources=.", + "-Dsonar.host.url=host", + "-Dsonar.login=token", + ] + } + /> + <p + className="big-spacer-top markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.docs", + } + } + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/SQScanner-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/SQScanner-test.js.snap new file mode 100644 index 00000000000..bcdac075ce4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/commands/__tests__/__snapshots__/SQScanner-test.js.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.sq_scanner.header.win + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.text.win", + } + } + /> + <p> + <a + className="button" + href="http://redirect.sonarsource.com/doc/install-configure-scanner.html" + target="_blank" + > + download_verb + </a> + </p> +</div> +`; + +exports[`renders correctly 2`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.sq_scanner.header.linux + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.text.linux", + } + } + /> + <p> + <a + className="button" + href="http://redirect.sonarsource.com/doc/install-configure-scanner.html" + target="_blank" + > + download_verb + </a> + </p> +</div> +`; + +exports[`renders correctly 3`] = ` +<div> + <h4 + className="spacer-bottom" + > + onboarding.analysis.sq_scanner.header.mac + </h4> + <p + className="spacer-bottom markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "onboarding.analysis.sq_scanner.text.mac", + } + } + /> + <p> + <a + className="button" + href="http://redirect.sonarsource.com/doc/install-configure-scanner.html" + target="_blank" + > + download_verb + </a> + </p> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/styles.css b/server/sonar-web/src/main/js/apps/tutorials/onboarding/styles.css new file mode 100644 index 00000000000..870da20acde --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/styles.css @@ -0,0 +1,59 @@ +.onboarding-step { + position: relative; + padding-left: 34px; +} + +.onboarding-step .boxed-group-actions { + height: 24px; + line-height: 24px; +} + +.onboarding-step-number { + position: absolute; + top: 15px; + left: 15px; + width: 24px; + height: 24px; + line-height: 24px; + border-radius: 24px; + background-color: #cdcdcd; + color: #fff; + font-size: 14px; + text-align: center; +} + +.onboarding-step-open .onboarding-step-number { + background-color: #236a97; +} + +.onboarding-command { + position: relative; + margin: 8px 0; +} + +.onboarding-command pre { + padding: 15px; + border-radius: 2px; + background: #404040; + color: #f0f0f0; + overflow: auto; +} + +.onboarding-command button { + position: absolute; + top: 15px; + right: 15px; + height: 20px; + line-height: 18px; + border: 1px solid #fff; + color: #fff; + font-size: 11px; + font-weight: normal; +} + +.onboarding-command button:hover, +.onboarding-command button:focus, +.onboarding-command button:active { + background-color: #fff; + color: #404040; +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/routes.js b/server/sonar-web/src/main/js/apps/tutorials/routes.js new file mode 100644 index 00000000000..3a7111d0ae0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/routes.js @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const routes = [ + { + path: 'onboarding', + getComponent(_, callback) { + require.ensure([], require => { + callback(null, require('./onboarding/OnboardingContainer').default); + }); + } + } +]; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/users/tokens-view.js b/server/sonar-web/src/main/js/apps/users/tokens-view.js index 01fa9a6636e..01889e1d848 100644 --- a/server/sonar-web/src/main/js/apps/users/tokens-view.js +++ b/server/sonar-web/src/main/js/apps/users/tokens-view.js @@ -54,17 +54,13 @@ export default Modal.extend({ this.errors = []; this.newToken = null; const tokenName = this.$('.js-generate-token-form input').val(); - generateToken(this.model.id, tokenName) - .then(response => { + generateToken(tokenName, this.model.id).then( + response => { this.newToken = response; this.requestTokens(); - }) - .catch(error => { - error.response.json().then(response => { - this.errors = response.errors; - this.render(); - }); - }); + }, + () => {} + ); }, onRevokeTokenFormSubmit(e) { @@ -73,7 +69,7 @@ export default Modal.extend({ const token = this.tokens.find(t => t.name === `${tokenName}`); if (token) { if (token.deleting) { - revokeToken(this.model.id, tokenName).then(this.requestTokens.bind(this)); + revokeToken(tokenName, this.model.id).then(this.requestTokens.bind(this), () => {}); } else { token.deleting = true; this.render(); diff --git a/server/sonar-web/src/main/js/helpers/testUtils.js b/server/sonar-web/src/main/js/helpers/testUtils.js index 4ae17931866..a69169ceeae 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.js +++ b/server/sonar-web/src/main/js/helpers/testUtils.js @@ -54,3 +54,11 @@ export const elementKeydown = (element, keyCode) => { preventDefault() {} }); }; + +export const doAsync = fn => + new Promise(resolve => { + setTimeout(() => { + fn(); + resolve(); + }, 0); + }); diff --git a/server/sonar-web/src/main/less/components/modals.less b/server/sonar-web/src/main/less/components/modals.less index 2920733b485..4853c080759 100644 --- a/server/sonar-web/src/main/less/components/modals.less +++ b/server/sonar-web/src/main/less/components/modals.less @@ -43,6 +43,11 @@ opacity: 1; } +.modal-medium { + width: 800px; + margin-left: -400px; +} + .modal-large { width: 90vw; margin-left: -45vw; diff --git a/server/sonar-web/src/main/less/components/side-tabs.less b/server/sonar-web/src/main/less/components/side-tabs.less new file mode 100644 index 00000000000..b7cd0da337c --- /dev/null +++ b/server/sonar-web/src/main/less/components/side-tabs.less @@ -0,0 +1,86 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@import (reference) "../mixins"; +@import (reference) "../variables"; + +.side-tabs-layout { + display: flex; + justify-content: space-between; + align-items: stretch; + + .modal & { + padding-left: 10px; + background-color: @barBackgroundColor; + } +} + +.side-tabs-main { + position: relative; + z-index: 2; + flex-grow: 1; + padding: 15px 20px; + border: 1px solid @barBorderColor; + box-sizing: border-box; + background-color: #fff; + + .modal & { + border-top: none; + border-bottom: none; + border-right: none; + } +} + +.side-tabs-side { + position: relative; + z-index: 3; + width: 160px; + flex-shrink: 0; + padding: 10px 0; + box-sizing: border-box; + transform: translateX(1px); +} + +.side-tabs-menu > li { + margin-bottom: 4px; +} + +.side-tabs-menu > li > a { + display: block; + padding: 10px 10px; + line-height: 1.5; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + border: 1px solid @barBorderColor; + border-right: none; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.3s ease, background-color 0.3s ease; +} + +.side-tabs-menu > li > a:hover, +.side-tabs-menu > li > a:focus, +.side-tabs-menu > li > a.active { + background-color: #fff; +} + +.side-tabs-menu > li > a.active { + color: #444; + cursor: default; +} diff --git a/server/sonar-web/src/main/less/init/type.less b/server/sonar-web/src/main/less/init/type.less index 75fe1839413..d26cae210ba 100644 --- a/server/sonar-web/src/main/less/init/type.less +++ b/server/sonar-web/src/main/less/init/type.less @@ -114,6 +114,7 @@ small, .text-top { vertical-align: top; } .text-middle { vertical-align: middle; } .text-bottom { vertical-align: bottom; } +.text-text-top { vertical-align: text-top !important; } // Overflow diff --git a/server/sonar-web/src/main/less/sonar.less b/server/sonar-web/src/main/less/sonar.less index 876ad49a4e2..29e5875e709 100644 --- a/server/sonar-web/src/main/less/sonar.less +++ b/server/sonar-web/src/main/less/sonar.less @@ -58,6 +58,7 @@ @import "components/search"; @import "components/pills"; @import "components/react-select"; +@import "components/side-tabs"; @import "pages/coding-rules"; @import "pages/maintenance"; |