diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-10-16 10:01:37 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-10-23 08:01:13 -0700 |
commit | 48fbc92a514fe94ad1ac435ddc033bd2379a5a69 (patch) | |
tree | f29a16c36a30a737282b7fa2ded566940cb0fb1f /server | |
parent | cc7c40ced053d5ba7bcc453b28f9cfcdf022a180 (diff) | |
download | sonarqube-48fbc92a514fe94ad1ac435ddc033bd2379a5a69.tar.gz sonarqube-48fbc92a514fe94ad1ac435ddc033bd2379a5a69.zip |
SONAR-9936 Add Editions pack in the marketplace
Diffstat (limited to 'server')
23 files changed, 1020 insertions, 60 deletions
diff --git a/server/sonar-web/src/main/js/api/marketplace.ts b/server/sonar-web/src/main/js/api/marketplace.ts new file mode 100644 index 00000000000..282be5bc7b3 --- /dev/null +++ b/server/sonar-web/src/main/js/api/marketplace.ts @@ -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. + */ +import { checkStatus, corsRequest, getJSON, parseJSON } from '../helpers/request'; +import throwGlobalError from '../app/utils/throwGlobalError'; + +export interface Edition { + name: string; + desc: string; + more_link: string; + request_license_link: string; + download_link: string; +} + +export interface Editions { + [key: string]: Edition; +} + +export interface EditionStatus { + currentEditionKey?: string; + nextEditionKey?: string; + installationStatus: + | 'NONE' + | 'AUTOMATIC_IN_PROGRESS' + | 'MANUAL_IN_PROGRESS' + | 'AUTOMATIC_READY' + | 'AUTOMATIC_FAILURE'; +} + +export function getEditionStatus(): Promise<EditionStatus> { + return getJSON('/api/editions/status').catch(throwGlobalError); +} + +export function getEditionsList(): Promise<Editions> { + // TODO Replace with real url + const url = + 'https://gist.githubusercontent.com/gregaubert/e34535494f8a94bec7cbc4d750ae7d06/raw/ba8670a28d4bc6fbac18f92e450ec42029cc5dcb/editions.json'; + return corsRequest(url) + .submit() + .then(checkStatus) + .then(parseJSON); +} diff --git a/server/sonar-web/src/main/js/app/components/AdminContainer.js b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx index 0794e8f79cd..fec085b91f2 100644 --- a/server/sonar-web/src/main/js/app/components/AdminContainer.js +++ b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx @@ -17,35 +17,56 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +import * as React from 'react'; +import * as PropTypes from 'prop-types'; import Helmet from 'react-helmet'; import { connect } from 'react-redux'; import SettingsNav from './nav/settings/SettingsNav'; import { getAppState } from '../../store/rootReducer'; -import { onFail } from '../../store/rootActions'; import { getSettingsNavigation } from '../../api/nav'; -import { setAdminPages } from '../../store/appState/duck'; +import { EditionStatus, getEditionStatus } from '../../api/marketplace'; +import { setAdminPages, setEditionStatus } from '../../store/appState/duck'; import { translate } from '../../helpers/l10n'; +import { Extension } from '../types'; + +interface Props { + appState: { + adminPages: Extension[]; + editionStatus?: EditionStatus; + organizationsEnabled: boolean; + }; + location: {}; + setAdminPages: (adminPages: Extension[]) => void; + setEditionStatus: (editionStatus: EditionStatus) => void; +} + +class AdminContainer extends React.PureComponent<Props> { + static contextTypes = { + canAdmin: PropTypes.bool.isRequired + }; -class AdminContainer extends React.PureComponent { componentDidMount() { - if (!this.props.appState.canAdmin) { + if (!this.context.canAdmin) { // workaround cyclic dependencies const handleRequiredAuthorization = require('../utils/handleRequiredAuthorization').default; handleRequiredAuthorization(); + } else { + this.loadData(); } - this.loadData(); } loadData() { - getSettingsNavigation().then( - r => this.props.setAdminPages(r.extensions), - onFail(this.props.dispatch) + Promise.all([getSettingsNavigation(), getEditionStatus()]).then( + ([r, editionStatus]) => { + this.props.setAdminPages(r.extensions); + this.props.setEditionStatus(editionStatus); + }, + () => {} ); } render() { - const { adminPages } = this.props.appState; + const { adminPages, editionStatus, organizationsEnabled } = this.props.appState; // Check that the adminPages are loaded if (!adminPages) { @@ -57,17 +78,22 @@ class AdminContainer extends React.PureComponent { return ( <div> <Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} /> - <SettingsNav location={this.props.location} extensions={adminPages} /> + <SettingsNav + customOrganizations={organizationsEnabled} + editionStatus={editionStatus} + extensions={adminPages} + location={this.props.location} + /> {this.props.children} </div> ); } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ appState: getAppState(state) }); -const mapDispatchToProps = { setAdminPages }; +const mapDispatchToProps = { setAdminPages, setEditionStatus }; -export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer); +export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer as any); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index ac54d018716..5f770db9ed4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import { Link } from 'react-router'; import * as classNames from 'classnames'; import * as PropTypes from 'prop-types'; -import { Branch, Component, ComponentExtension } from '../../../types'; +import { Branch, Component, Extension } from '../../../types'; import NavBarTabs from '../../../../components/nav/NavBarTabs'; import { isShortLivingBranch, @@ -419,7 +419,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { ); } - renderExtension = ({ key, name }: ComponentExtension, isAdmin: boolean) => { + renderExtension = ({ key, name }: Extension, isAdmin: boolean) => { const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`; return ( <li key={key}> diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx new file mode 100644 index 00000000000..5df886e38b2 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx @@ -0,0 +1,60 @@ +/* + * 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 * as React from 'react'; +import NavBarNotif from '../../../../components/nav/NavBarNotif'; +import { EditionStatus } from '../../../../api/marketplace'; +import { translate } from '../../../../helpers/l10n'; + +interface Props { + editionStatus: EditionStatus; +} + +export default class SettingsEditionsNotif extends React.PureComponent<Props> { + render() { + const { editionStatus } = this.props; + + if (editionStatus.installationStatus === 'AUTOMATIC_IN_PROGRESS') { + return ( + <NavBarNotif className="alert alert-info"> + <i className="spinner spacer-right text-bottom" /> + <span>{translate('marketplace.status.AUTOMATIC_IN_PROGRESS')}</span> + </NavBarNotif> + ); + } else if (editionStatus.installationStatus === 'AUTOMATIC_READY') { + return ( + <NavBarNotif className="alert alert-success"> + <span>{translate('marketplace.status.AUTOMATIC_READY')}</span> + </NavBarNotif> + ); + } else if ( + ['MANUAL_IN_PROGRESS', 'AUTOMATIC_FAILURE'].includes(editionStatus.installationStatus) + ) { + return ( + <NavBarNotif className="alert alert-danger"> + {translate('marketplace.status', editionStatus.installationStatus)} + <a className="little-spacer-left" href="https://www.sonarsource.com" target="_blank"> + {translate('marketplace.how_to_install')} + </a> + </NavBarNotif> + ); + } + return null; + } +} diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx index e307778eec4..b677146a392 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx @@ -17,23 +17,31 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; import { IndexLink, Link } from 'react-router'; -import { connect } from 'react-redux'; import ContextNavBar from '../../../../components/nav/ContextNavBar'; +import SettingsEditionsNotif from './SettingsEditionsNotif'; import NavBarTabs from '../../../../components/nav/NavBarTabs'; +import { EditionStatus } from '../../../../api/marketplace'; +import { Extension } from '../../../types'; import { translate } from '../../../../helpers/l10n'; -import { areThereCustomOrganizations } from '../../../../store/rootReducer'; -class SettingsNav extends React.PureComponent { +interface Props { + editionStatus?: EditionStatus; + extensions: Extension[]; + customOrganizations: boolean; + location: {}; +} + +export default class SettingsNav extends React.PureComponent<Props> { static defaultProps = { extensions: [] }; - isSomethingActive(urls) { + isSomethingActive(urls: string[]): boolean { const path = window.location.pathname; - return urls.some(url => path.indexOf(window.baseUrl + url) === 0); + return urls.some((url: string) => path.indexOf((window as any).baseUrl + url) === 0); } isSecurityActive() { @@ -56,7 +64,7 @@ class SettingsNav extends React.PureComponent { return this.isSomethingActive(urls); } - renderExtension = ({ key, name }) => { + renderExtension = ({ key, name }: Extension) => { return ( <li key={key}> <Link to={`/admin/extension/${key}`} activeClassName="active"> @@ -67,6 +75,7 @@ class SettingsNav extends React.PureComponent { }; render() { + const { customOrganizations, editionStatus, extensions } = this.props; const isSecurity = this.isSecurityActive(); const isProjects = this.isProjectsActive(); const isSystem = this.isSystemActive(); @@ -79,14 +88,21 @@ class SettingsNav extends React.PureComponent { active: !isSecurity && !isProjects && !isSystem && !isSupport }); - const extensionsWithoutSupport = this.props.extensions.filter( + const extensionsWithoutSupport = extensions.filter( extension => extension.key !== 'license/support' ); - const hasSupportExtension = extensionsWithoutSupport.length < this.props.extensions.length; + const hasSupportExtension = extensionsWithoutSupport.length < extensions.length; + let notifComponent; + if (editionStatus && editionStatus.installationStatus !== 'NONE') { + notifComponent = <SettingsEditionsNotif editionStatus={editionStatus} />; + } return ( - <ContextNavBar id="context-navigation" height={65}> + <ContextNavBar + id="context-navigation" + height={notifComponent ? 95 : 65} + notif={notifComponent}> <h1 className="navbar-context-header"> <strong>{translate('layout.settings')}</strong> </h1> @@ -130,21 +146,21 @@ class SettingsNav extends React.PureComponent { {translate('users.page')} </IndexLink> </li> - {!this.props.customOrganizations && ( + {!customOrganizations && ( <li> <IndexLink to="/admin/groups" activeClassName="active"> {translate('user_groups.page')} </IndexLink> </li> )} - {!this.props.customOrganizations && ( + {!customOrganizations && ( <li> <IndexLink to="/admin/permissions" activeClassName="active"> {translate('global_permissions.page')} </IndexLink> </li> )} - {!this.props.customOrganizations && ( + {!customOrganizations && ( <li> <IndexLink to="/admin/permission_templates" activeClassName="active"> {translate('permission_templates')} @@ -159,7 +175,7 @@ class SettingsNav extends React.PureComponent { {translate('sidebar.projects')} <i className="icon-dropdown" /> </a> <ul className="dropdown-menu"> - {!this.props.customOrganizations && ( + {!customOrganizations && ( <li> <IndexLink to="/admin/projects_management" activeClassName="active"> {translate('management')} @@ -210,11 +226,3 @@ class SettingsNav extends React.PureComponent { ); } } - -const mapStateToProps = state => ({ - customOrganizations: areThereCustomOrganizations(state) -}); - -export default connect(mapStateToProps)(SettingsNav); - -export const UnconnectedSettingsNav = SettingsNav; diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx new file mode 100644 index 00000000000..55612c06b34 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx @@ -0,0 +1,43 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import SettingsEditionsNotif from '../SettingsEditionsNotif'; + +it('should display an in progress notif', () => { + const wrapper = shallow( + <SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }} /> + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('should display an error notification', () => { + const wrapper = shallow( + <SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_FAILURE' }} /> + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('should display a ready notification', () => { + const wrapper = shallow( + <SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_READY' }} /> + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.js b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx index 48f8539f415..f819af90c49 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.js +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx @@ -17,12 +17,31 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +import * as React from 'react'; import { shallow } from 'enzyme'; -import { UnconnectedSettingsNav } from '../SettingsNav'; +import SettingsNav from '../SettingsNav'; it('should work with extensions', () => { const extensions = [{ key: 'foo', name: 'Foo' }]; - const wrapper = shallow(<UnconnectedSettingsNav extensions={extensions} />); + const wrapper = shallow( + <SettingsNav + customOrganizations={false} + editionStatus={{ installationStatus: 'NONE' }} + extensions={extensions} + location={{}} + /> + ); expect(wrapper).toMatchSnapshot(); }); + +it('should display an edition notification', () => { + const wrapper = shallow( + <SettingsNav + customOrganizations={false} + editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }} + extensions={[]} + location={{}} + /> + ); + expect({ ...wrapper.find('ContextNavBar').props(), children: [] }).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap new file mode 100644 index 00000000000..030ff105e6a --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display a ready notification 1`] = ` +<NavBarNotif + className="alert alert-success" +> + <span> + marketplace.status.AUTOMATIC_READY + </span> +</NavBarNotif> +`; + +exports[`should display an error notification 1`] = ` +<NavBarNotif + className="alert alert-danger" +> + marketplace.status.AUTOMATIC_FAILURE + <a + className="little-spacer-left" + href="https://www.sonarsource.com" + target="_blank" + > + marketplace.how_to_install + </a> +</NavBarNotif> +`; + +exports[`should display an in progress notif 1`] = ` +<NavBarNotif + className="alert alert-info" +> + <i + className="spinner spacer-right text-bottom" + /> + <span> + marketplace.status.AUTOMATIC_IN_PROGRESS + </span> +</NavBarNotif> +`; diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap index 62c113f24bb..9dd9e567752 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap @@ -1,5 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should display an edition notification 1`] = ` +Object { + "children": Array [], + "height": 95, + "id": "context-navigation", + "notif": <SettingsEditionsNotif + editionStatus={ + Object { + "installationStatus": "AUTOMATIC_IN_PROGRESS", + } + } +/>, +} +`; + exports[`should work with extensions 1`] = ` <ContextNavBar height={65} diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 8bf75ae6e04..73239aeb430 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -57,7 +57,7 @@ export interface ShortLivingBranch { export type Branch = MainBranch | LongLivingBranch | ShortLivingBranch; -export interface ComponentExtension { +export interface Extension { key: string; name: string; } @@ -71,7 +71,7 @@ export interface Component { }>; configuration?: ComponentConfiguration; description?: string; - extensions?: ComponentExtension[]; + extensions?: Extension[]; isFavorite?: boolean; key: string; name: string; @@ -83,7 +83,7 @@ export interface Component { } interface ComponentConfiguration { - extensions?: ComponentExtension[]; + extensions?: Extension[]; showBackgroundTasks?: boolean; showLinks?: boolean; showManualMeasures?: boolean; diff --git a/server/sonar-web/src/main/js/apps/marketplace/App.tsx b/server/sonar-web/src/main/js/apps/marketplace/App.tsx index 0cb9096adc3..6ee3d1bce8f 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/App.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx @@ -22,6 +22,7 @@ import * as PropTypes from 'prop-types'; import { sortBy, uniqBy } from 'lodash'; import Helmet from 'react-helmet'; import Header from './Header'; +import EditionBoxes from './EditionBoxes'; import Footer from './Footer'; import PendingActions from './PendingActions'; import PluginsList from './PluginsList'; @@ -34,11 +35,13 @@ import { Plugin, PluginPending } from '../../api/plugins'; +import { EditionStatus } from '../../api/marketplace'; import { RawQuery } from '../../helpers/query'; import { translate } from '../../helpers/l10n'; import { filterPlugins, parseQuery, Query, serializeQuery } from './utils'; export interface Props { + editionStatus?: EditionStatus; location: { pathname: string; query: RawQuery }; updateCenterActive: boolean; } @@ -109,7 +112,11 @@ export default class App extends React.PureComponent<Props, State> { }); } }, - () => {} + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } ); }; @@ -121,7 +128,11 @@ export default class App extends React.PureComponent<Props, State> { this.setState({ loading: false, plugins }); } }, - () => {} + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } ); }; @@ -154,6 +165,10 @@ export default class App extends React.PureComponent<Props, State> { <div className="page page-limited" id="marketplace-page"> <Helmet title={translate('marketplace.page')} /> <Header /> + <EditionBoxes + editionStatus={this.props.editionStatus} + updateCenterActive={this.props.updateCenterActive} + /> <PendingActions refreshPending={this.fetchPendingPlugins} pending={pending} /> <Search query={query} diff --git a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx index 4319d2b1890..418219fd22a 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx @@ -19,9 +19,11 @@ */ import { connect } from 'react-redux'; import App from './App'; -import { getGlobalSettingValue } from '../../store/rootReducer'; +import { getAppState, getGlobalSettingValue } from '../../store/rootReducer'; +import './style.css'; const mapStateToProps = (state: any) => ({ + editionStatus: getAppState(state).editionStatus, updateCenterActive: (getGlobalSettingValue(state, 'sonar.updatecenter.activate') || {}).value }); diff --git a/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx b/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx new file mode 100644 index 00000000000..5c74a1e8aa4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx @@ -0,0 +1,104 @@ +/* + * 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 * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import EditionBox from './components/EditionBox'; +import { Editions, EditionStatus, getEditionsList } from '../../api/marketplace'; +import { translate } from '../../helpers/l10n'; + +export interface Props { + editionStatus?: EditionStatus; + updateCenterActive: boolean; +} + +interface State { + editions: Editions; + editionsError: boolean; + loading: boolean; +} + +export default class EditionBoxes extends React.PureComponent<Props, State> { + mounted: boolean; + state: State = { editions: {}, editionsError: false, loading: true }; + + componentDidMount() { + this.mounted = true; + this.fetchEditions(); + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchEditions = () => { + this.setState({ loading: true }); + getEditionsList().then( + editions => { + if (this.mounted) { + this.setState({ + loading: false, + editions, + editionsError: false + }); + } + }, + () => { + if (this.mounted) { + this.setState({ editionsError: true, loading: false }); + } + } + ); + }; + + render() { + const { editions, loading } = this.state; + if (loading) { + return null; + } + return ( + <div className="spacer-bottom marketplace-editions"> + {this.state.editionsError ? ( + <span className="alert alert-info"> + <FormattedMessage + defaultMessage={translate('marketplace.editions_unavailable')} + id="marketplace.editions_unavailable" + values={{ + url: ( + <a href="https://www.sonarsource.com" target="_blank"> + SonarSource.com + </a> + ) + }} + /> + </span> + ) : ( + Object.keys(editions).map(key => ( + <EditionBox + edition={editions[key]} + editionKey={key} + editionStatus={this.props.editionStatus} + key={key} + /> + )) + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx new file mode 100644 index 00000000000..49ee1abdcde --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx @@ -0,0 +1,66 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import EditionBoxes from '../EditionBoxes'; +import { EditionStatus } from '../../../api/marketplace'; + +const DEFAULT_STATUS: EditionStatus = { + currentEditionKey: 'foo', + nextEditionKey: '', + installationStatus: 'NONE' +}; + +it('should display the edition boxes', () => { + const wrapper = getWrapper(); + expect(wrapper).toMatchSnapshot(); + wrapper.setState({ + editions: { + foo: { + name: 'Foo', + desc: 'Foo desc', + download_link: 'download_url', + more_link: 'more_url', + request_license_link: 'license_url' + }, + bar: { + name: 'Bar', + desc: 'Bar desc', + download_link: 'download_url', + more_link: 'more_url', + request_license_link: 'license_url' + } + }, + loading: false + }); + expect(wrapper).toMatchSnapshot(); +}); + +it('should display an error message', () => { + const wrapper = getWrapper(); + wrapper.setState({ loading: false, editionsError: true }); + expect(wrapper).toMatchSnapshot(); +}); + +function getWrapper(props = {}) { + return shallow( + <EditionBoxes editionStatus={DEFAULT_STATUS} updateCenterActive={true} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx index 9d9d15c3bf8..4ebc2b4709a 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx @@ -36,14 +36,14 @@ it('should display pending actions', () => { expect(getWrapper()).toMatchSnapshot(); }); -it('should not display nothing', () => { +it('should not display anything', () => { expect(getWrapper({ pending: { installing: [], updating: [], removing: [] } })).toMatchSnapshot(); }); it('should open the restart form', () => { const wrapper = getWrapper(); click(wrapper.find('.js-restart')); - expect(wrapper.find('RestartForm')).toHaveLength(1); + expect(wrapper.find('RestartForm').exists()).toBeTruthy(); }); it('should cancel all pending and refresh them', async () => { diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap new file mode 100644 index 00000000000..7ee4e6b73e9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display an error message 1`] = ` +<div + className="spacer-bottom marketplace-editions" +> + <span + className="alert alert-info" + > + <FormattedMessage + defaultMessage="marketplace.editions_unavailable" + id="marketplace.editions_unavailable" + values={ + Object { + "url": <a + href="https://www.sonarsource.com" + target="_blank" + > + SonarSource.com + </a>, + } + } + /> + </span> +</div> +`; + +exports[`should display the edition boxes 1`] = `null`; + +exports[`should display the edition boxes 2`] = ` +<div + className="spacer-bottom marketplace-editions" +> + <EditionBox + edition={ + Object { + "desc": "Foo desc", + "download_link": "download_url", + "more_link": "more_url", + "name": "Foo", + "request_license_link": "license_url", + } + } + editionKey="foo" + editionStatus={ + Object { + "currentEditionKey": "foo", + "installationStatus": "NONE", + "nextEditionKey": "", + } + } + /> + <EditionBox + edition={ + Object { + "desc": "Bar desc", + "download_link": "download_url", + "more_link": "more_url", + "name": "Bar", + "request_license_link": "license_url", + } + } + editionKey="bar" + editionStatus={ + Object { + "currentEditionKey": "foo", + "installationStatus": "NONE", + "nextEditionKey": "", + } + } + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap index 36604e80f98..3afbde17ba6 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap @@ -64,4 +64,4 @@ exports[`should display pending actions 1`] = ` </div> `; -exports[`should not display nothing 1`] = `null`; +exports[`should not display anything 1`] = `null`; diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx new file mode 100644 index 00000000000..3299504fdad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx @@ -0,0 +1,72 @@ +/* + * 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 * as React from 'react'; +import CheckIcon from '../../../components/icons-components/CheckIcon'; +import { Edition, EditionStatus } from '../../../api/marketplace'; +import { translate } from '../../../helpers/l10n'; + +export interface Props { + edition: Edition; + editionKey: string; + editionStatus?: EditionStatus; +} + +export default class EditionBox extends React.PureComponent<Props> { + render() { + const { edition, editionKey, editionStatus } = this.props; + const isInstalled = editionStatus && editionStatus.currentEditionKey === editionKey; + const isInstalling = editionStatus && editionStatus.nextEditionKey === editionKey; + const installInProgress = + editionStatus && editionStatus.installationStatus === 'AUTOMATIC_IN_PROGRESS'; + return ( + <div className="boxed-group boxed-group-inner marketplace-edition"> + {isInstalled && + !isInstalling && ( + <span className="marketplace-edition-badge badge badge-normal-size"> + <CheckIcon size={14} className="little-spacer-right text-text-top" /> + {translate('marketplace.installed')} + </span> + )} + {isInstalling && ( + <span className="marketplace-edition-badge badge badge-normal-size"> + {translate('marketplace.installing')} + </span> + )} + <div> + <h3 className="spacer-bottom">{edition.name}</h3> + <p>{edition.desc}</p> + </div> + <div className="marketplace-edition-action spacer-top"> + <a href={edition.more_link} target="_blank"> + {translate('marketplace.learn_more')} + </a> + {!isInstalled && ( + <button disabled={installInProgress}>{translate('marketplace.install')}</button> + )} + {isInstalled && ( + <button className="button-red" disabled={installInProgress}> + {translate('marketplace.uninstall')} + </button> + )} + </div> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx new file mode 100644 index 00000000000..ebb42f27489 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx @@ -0,0 +1,100 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import { Edition, EditionStatus } from '../../../../api/marketplace'; +import EditionBox from '../EditionBox'; + +const DEFAULT_STATUS: EditionStatus = { + currentEditionKey: '', + nextEditionKey: '', + installationStatus: 'NONE' +}; + +const DEFAULT_EDITION: Edition = { + name: 'Foo', + desc: 'Foo desc', + download_link: 'download_url', + more_link: 'more_url', + request_license_link: 'license_url' +}; + +it('should display the edition', () => { + expect(getWrapper()).toMatchSnapshot(); +}); + +it('should display installed badge', () => { + expect( + getWrapper({ + editionStatus: { + currentEditionKey: 'foo', + nextEditionKey: '', + installationStatus: 'NONE' + } + }) + ).toMatchSnapshot(); +}); + +it('should display installing badge', () => { + expect( + getWrapper({ + editionStatus: { + currentEditionKey: 'foo', + nextEditionKey: 'foo', + installationStatus: 'NONE' + } + }) + ).toMatchSnapshot(); +}); + +it('should disable install button', () => { + expect( + getWrapper({ + editionStatus: { + currentEditionKey: 'foo', + nextEditionKey: '', + installationStatus: 'AUTOMATIC_IN_PROGRESS' + } + }) + ).toMatchSnapshot(); +}); + +it('should disable uninstall button', () => { + expect( + getWrapper({ + editionStatus: { + currentEditionKey: '', + nextEditionKey: 'foo', + installationStatus: 'AUTOMATIC_IN_PROGRESS' + } + }) + ).toMatchSnapshot(); +}); + +function getWrapper(props = {}) { + return shallow( + <EditionBox + edition={DEFAULT_EDITION} + editionKey="foo" + editionStatus={DEFAULT_STATUS} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap new file mode 100644 index 00000000000..6814875f5ba --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap @@ -0,0 +1,192 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should disable install button 1`] = ` +<div + className="boxed-group boxed-group-inner marketplace-edition" +> + <span + className="marketplace-edition-badge badge badge-normal-size" + > + <CheckIcon + className="little-spacer-right text-text-top" + size={14} + /> + marketplace.installed + </span> + <div> + <h3 + className="spacer-bottom" + > + Foo + </h3> + <p> + Foo desc + </p> + </div> + <div + className="marketplace-edition-action spacer-top" + > + <a + href="more_url" + target="_blank" + > + marketplace.learn_more + </a> + <button + className="button-red" + disabled={true} + > + marketplace.uninstall + </button> + </div> +</div> +`; + +exports[`should disable uninstall button 1`] = ` +<div + className="boxed-group boxed-group-inner marketplace-edition" +> + <span + className="marketplace-edition-badge badge badge-normal-size" + > + marketplace.installing + </span> + <div> + <h3 + className="spacer-bottom" + > + Foo + </h3> + <p> + Foo desc + </p> + </div> + <div + className="marketplace-edition-action spacer-top" + > + <a + href="more_url" + target="_blank" + > + marketplace.learn_more + </a> + <button + disabled={true} + > + marketplace.install + </button> + </div> +</div> +`; + +exports[`should display installed badge 1`] = ` +<div + className="boxed-group boxed-group-inner marketplace-edition" +> + <span + className="marketplace-edition-badge badge badge-normal-size" + > + <CheckIcon + className="little-spacer-right text-text-top" + size={14} + /> + marketplace.installed + </span> + <div> + <h3 + className="spacer-bottom" + > + Foo + </h3> + <p> + Foo desc + </p> + </div> + <div + className="marketplace-edition-action spacer-top" + > + <a + href="more_url" + target="_blank" + > + marketplace.learn_more + </a> + <button + className="button-red" + disabled={false} + > + marketplace.uninstall + </button> + </div> +</div> +`; + +exports[`should display installing badge 1`] = ` +<div + className="boxed-group boxed-group-inner marketplace-edition" +> + <span + className="marketplace-edition-badge badge badge-normal-size" + > + marketplace.installing + </span> + <div> + <h3 + className="spacer-bottom" + > + Foo + </h3> + <p> + Foo desc + </p> + </div> + <div + className="marketplace-edition-action spacer-top" + > + <a + href="more_url" + target="_blank" + > + marketplace.learn_more + </a> + <button + className="button-red" + disabled={false} + > + marketplace.uninstall + </button> + </div> +</div> +`; + +exports[`should display the edition 1`] = ` +<div + className="boxed-group boxed-group-inner marketplace-edition" +> + <div> + <h3 + className="spacer-bottom" + > + Foo + </h3> + <p> + Foo desc + </p> + </div> + <div + className="marketplace-edition-action spacer-top" + > + <a + href="more_url" + target="_blank" + > + marketplace.learn_more + </a> + <button + disabled={false} + > + marketplace.install + </button> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/marketplace/style.css b/server/sonar-web/src/main/js/apps/marketplace/style.css new file mode 100644 index 00000000000..037a0c133d9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/style.css @@ -0,0 +1,32 @@ +.marketplace-editions { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-left: -8px; + margin-right: -8px; +} + +.marketplace-edition { + position: relative; + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + background-color: #f3f3f3; + margin-left: 8px; + margin-right: 8px; +} + +.marketplace-edition-badge { + position: absolute; + right: -1px; + top: 16px; + padding: 4px 8px; + border-radius: 2px 0 0 2px; +} + +.marketplace-edition-action { + display: flex; + align-items: baseline; + justify-content: space-between; +} diff --git a/server/sonar-web/src/main/js/helpers/request.ts b/server/sonar-web/src/main/js/helpers/request.ts index 1abef7a8399..6e955baeeac 100644 --- a/server/sonar-web/src/main/js/helpers/request.ts +++ b/server/sonar-web/src/main/js/helpers/request.ts @@ -78,11 +78,9 @@ class Request { constructor(private url: string, private options: { method?: string } = {}) {} - submit(): Promise<Response> { + getSubmitData(customHeaders: any = {}): { url: string; options: RequestInit } { let url = this.url; - const options: RequestInit = { ...DEFAULT_OPTIONS, ...this.options }; - const customHeaders: any = {}; if (this.data) { if (this.data instanceof FormData) { @@ -100,10 +98,13 @@ class Request { options.headers = { ...DEFAULT_HEADERS, - ...customHeaders, - ...getCSRFToken() + ...customHeaders }; + return { url, options }; + } + submit(): Promise<Response> { + const { url, options } = this.getSubmitData({ ...getCSRFToken() }); return window.fetch((window as any).baseUrl + url, options); } @@ -128,6 +129,19 @@ export function request(url: string): Request { } /** + * Make a cors request + */ +export function corsRequest(url: string, mode: RequestMode = 'cors'): Request { + const options: RequestInit = { mode }; + const request = new Request(url, options); + request.submit = function() { + const { url, options } = this.getSubmitData(); + return window.fetch(url, options); + }; + return request; +} + +/** * Check that response status is ok */ export function checkStatus(response: Response): Promise<Response> { diff --git a/server/sonar-web/src/main/js/store/appState/duck.ts b/server/sonar-web/src/main/js/store/appState/duck.ts index ed005f2888f..d783242272f 100644 --- a/server/sonar-web/src/main/js/store/appState/duck.ts +++ b/server/sonar-web/src/main/js/store/appState/duck.ts @@ -17,10 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { Extension } from '../../app/types'; +import { EditionStatus } from '../../api/marketplace'; + interface AppState { - adminPages?: any[]; + adminPages?: Extension[]; authenticationError: boolean; authorizationError: boolean; + editionStatus?: EditionStatus; organizationsEnabled: boolean; qualifiers?: string[]; } @@ -32,14 +37,23 @@ interface SetAppStateAction { interface SetAdminPagesAction { type: 'SET_ADMIN_PAGES'; - adminPages: any[]; + adminPages: Extension[]; +} + +interface SetEditionStatusAction { + type: 'SET_EDITION_STATUS'; + editionStatus: EditionStatus; } interface RequireAuthorizationAction { type: 'REQUIRE_AUTHORIZATION'; } -export type Action = SetAppStateAction | SetAdminPagesAction | RequireAuthorizationAction; +export type Action = + | SetAppStateAction + | SetAdminPagesAction + | SetEditionStatusAction + | RequireAuthorizationAction; export function setAppState(appState: AppState): SetAppStateAction { return { @@ -48,7 +62,7 @@ export function setAppState(appState: AppState): SetAppStateAction { }; } -export function setAdminPages(adminPages: any[]): SetAdminPagesAction { +export function setAdminPages(adminPages: Extension[]): SetAdminPagesAction { return { type: 'SET_ADMIN_PAGES', adminPages }; } @@ -56,6 +70,10 @@ export function requireAuthorization(): RequireAuthorizationAction { return { type: 'REQUIRE_AUTHORIZATION' }; } +export function setEditionStatus(editionStatus: EditionStatus): SetEditionStatusAction { + return { type: 'SET_EDITION_STATUS', editionStatus }; +} + const defaultValue: AppState = { authenticationError: false, authorizationError: false, @@ -71,6 +89,10 @@ export default function(state: AppState = defaultValue, action: Action): AppStat return { ...state, adminPages: action.adminPages }; } + if (action.type === 'SET_EDITION_STATUS') { + return { ...state, editionStatus: action.editionStatus }; + } + if (action.type === 'REQUIRE_AUTHORIZATION') { return { ...state, authorizationError: true }; } |