From 48fbc92a514fe94ad1ac435ddc033bd2379a5a69 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Mon, 16 Oct 2017 10:01:37 +0200 Subject: [PATCH] SONAR-9936 Add Editions pack in the marketplace --- .../sonar-web/src/main/js/api/marketplace.ts | 58 ++++++ .../{AdminContainer.js => AdminContainer.tsx} | 54 +++-- .../nav/component/ComponentNavMenu.tsx | 4 +- .../nav/settings/SettingsEditionsNotif.tsx | 60 ++++++ .../{SettingsNav.js => SettingsNav.tsx} | 54 ++--- .../__tests__/SettingsEditionsNotif-test.tsx | 43 ++++ ...ttingsNav-test.js => SettingsNav-test.tsx} | 25 ++- .../SettingsEditionsNotif-test.tsx.snap | 39 ++++ ...test.js.snap => SettingsNav-test.tsx.snap} | 15 ++ server/sonar-web/src/main/js/app/types.ts | 6 +- .../src/main/js/apps/marketplace/App.tsx | 19 +- .../main/js/apps/marketplace/AppContainer.tsx | 4 +- .../main/js/apps/marketplace/EditionBoxes.tsx | 104 ++++++++++ .../__tests__/EditionBoxes-test.tsx | 66 ++++++ .../__tests__/PendingActions-test.tsx | 4 +- .../__snapshots__/EditionBoxes-test.tsx.snap | 73 +++++++ .../PendingActions-test.tsx.snap | 2 +- .../marketplace/components/EditionBox.tsx | 72 +++++++ .../components/__tests__/EditionBox-test.tsx | 100 +++++++++ .../__snapshots__/EditionBox-test.tsx.snap | 192 ++++++++++++++++++ .../src/main/js/apps/marketplace/style.css | 32 +++ .../sonar-web/src/main/js/helpers/request.ts | 24 ++- .../src/main/js/store/appState/duck.ts | 30 ++- .../resources/org/sonar/l10n/core.properties | 7 + 24 files changed, 1027 insertions(+), 60 deletions(-) create mode 100644 server/sonar-web/src/main/js/api/marketplace.ts rename server/sonar-web/src/main/js/app/components/{AdminContainer.js => AdminContainer.tsx} (56%) create mode 100644 server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx rename server/sonar-web/src/main/js/app/components/nav/settings/{SettingsNav.js => SettingsNav.tsx} (84%) create mode 100644 server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx rename server/sonar-web/src/main/js/app/components/nav/settings/__tests__/{SettingsNav-test.js => SettingsNav-test.tsx} (62%) create mode 100644 server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap rename server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/{SettingsNav-test.js.snap => SettingsNav-test.tsx.snap} (93%) create mode 100644 server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx create mode 100644 server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx create mode 100644 server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/marketplace/style.css 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 { + return getJSON('/api/editions/status').catch(throwGlobalError); +} + +export function getEditionsList(): Promise { + // 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 similarity index 56% rename from server/sonar-web/src/main/js/app/components/AdminContainer.js rename to 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 { + 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 (
- + {this.props.children}
); } } -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 { ); } - renderExtension = ({ key, name }: ComponentExtension, isAdmin: boolean) => { + renderExtension = ({ key, name }: Extension, isAdmin: boolean) => { const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`; return (
  • 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 { + render() { + const { editionStatus } = this.props; + + if (editionStatus.installationStatus === 'AUTOMATIC_IN_PROGRESS') { + return ( + + + {translate('marketplace.status.AUTOMATIC_IN_PROGRESS')} + + ); + } else if (editionStatus.installationStatus === 'AUTOMATIC_READY') { + return ( + + {translate('marketplace.status.AUTOMATIC_READY')} + + ); + } else if ( + ['MANUAL_IN_PROGRESS', 'AUTOMATIC_FAILURE'].includes(editionStatus.installationStatus) + ) { + return ( + + {translate('marketplace.status', editionStatus.installationStatus)} + + {translate('marketplace.how_to_install')} + + + ); + } + 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 similarity index 84% rename from server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js rename to 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 { 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 (
  • @@ -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 = ; + } return ( - +

    {translate('layout.settings')}

    @@ -130,21 +146,21 @@ class SettingsNav extends React.PureComponent { {translate('users.page')}
  • - {!this.props.customOrganizations && ( + {!customOrganizations && (
  • {translate('user_groups.page')}
  • )} - {!this.props.customOrganizations && ( + {!customOrganizations && (
  • {translate('global_permissions.page')}
  • )} - {!this.props.customOrganizations && ( + {!customOrganizations && (
  • {translate('permission_templates')} @@ -159,7 +175,7 @@ class SettingsNav extends React.PureComponent { {translate('sidebar.projects')}
      - {!this.props.customOrganizations && ( + {!customOrganizations && (
    • {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( + + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('should display an error notification', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('should display a ready notification', () => { + const wrapper = shallow( + + ); + 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 similarity index 62% rename from server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.js rename to 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(); + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); }); + +it('should display an edition notification', () => { + const wrapper = shallow( + + ); + 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`] = ` + + + marketplace.status.AUTOMATIC_READY + + +`; + +exports[`should display an error notification 1`] = ` + + marketplace.status.AUTOMATIC_FAILURE + + marketplace.how_to_install + + +`; + +exports[`should display an in progress notif 1`] = ` + + + + marketplace.status.AUTOMATIC_IN_PROGRESS + + +`; 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 similarity index 93% rename from server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap rename to 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": , +} +`; + exports[`should work with extensions 1`] = ` ; 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 { }); } }, - () => {} + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } ); }; @@ -121,7 +128,11 @@ export default class App extends React.PureComponent { this.setState({ loading: false, plugins }); } }, - () => {} + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } ); }; @@ -154,6 +165,10 @@ export default class App extends React.PureComponent {
      + ({ + 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 { + 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 ( +
      + {this.state.editionsError ? ( + + + SonarSource.com + + ) + }} + /> + + ) : ( + Object.keys(editions).map(key => ( + + )) + )} +
      + ); + } +} 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( + + ); +} 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`] = ` +
      + + + SonarSource.com + , + } + } + /> + +
      +`; + +exports[`should display the edition boxes 1`] = `null`; + +exports[`should display the edition boxes 2`] = ` +
      + + +
      +`; 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`] = `
      `; -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 { + 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 ( +
      + {isInstalled && + !isInstalling && ( + + + {translate('marketplace.installed')} + + )} + {isInstalling && ( + + {translate('marketplace.installing')} + + )} +
      +

      {edition.name}

      +

      {edition.desc}

      +
      +
      + + {translate('marketplace.learn_more')} + + {!isInstalled && ( + + )} + {isInstalled && ( + + )} +
      +
      + ); + } +} 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( + + ); +} 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`] = ` +
      + + + marketplace.installed + +
      +

      + Foo +

      +

      + Foo desc +

      +
      +
      + + marketplace.learn_more + + +
      +
      +`; + +exports[`should disable uninstall button 1`] = ` +
      + + marketplace.installing + +
      +

      + Foo +

      +

      + Foo desc +

      +
      +
      + + marketplace.learn_more + + +
      +
      +`; + +exports[`should display installed badge 1`] = ` +
      + + + marketplace.installed + +
      +

      + Foo +

      +

      + Foo desc +

      +
      +
      + + marketplace.learn_more + + +
      +
      +`; + +exports[`should display installing badge 1`] = ` +
      + + marketplace.installing + +
      +

      + Foo +

      +

      + Foo desc +

      +
      +
      + + marketplace.learn_more + + +
      +
      +`; + +exports[`should display the edition 1`] = ` +
      +
      +

      + Foo +

      +

      + Foo desc +

      +
      +
      + + marketplace.learn_more + + +
      +
      +`; 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 { + 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 { + const { url, options } = this.getSubmitData({ ...getCSRFToken() }); return window.fetch((window as any).baseUrl + url, options); } @@ -127,6 +128,19 @@ export function request(url: string): Request { return new Request(url); } +/** + * 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 */ 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 }; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ead056362e4..a91bb089669 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2069,6 +2069,7 @@ marketplace.revert=Revert marketplace.system_upgrades=System Upgrades marketplace.install=Install marketplace.installed=Installed +marketplace.installing=Installing... marketplace._installed=installed marketplace.available_under_commercial_license=Available under our commercial editions marketplace.learn_more=Learn more @@ -2089,6 +2090,12 @@ marketplace.update_to_x=Update to {0} marketplace.uninstall=Uninstall marketplace.i_accept_the=I accept the marketplace.terms_and_conditions=Terms and Conditions +marketplace.editions_unavailable=Explore our Editions: advanced feature packs brought to you by SonarSource on {url} +marketplace.status.AUTOMATIC_IN_PROGRESS=Updating your installation... Please wait... +marketplace.status.AUTOMATIC_READY=New installation complete. Please restart Server to benefit from it. +marketplace.status.MANUAL_IN_PROGRESS=Can't install Developer Edition because of internet access issue. Please manually install the package in your SonarQube's plugins folder. +marketplace.status.AUTOMATIC_FAILURE=Can't install Developer Edition. Please manually install the package in your SonarQube's plugins folder. +marketplace.how_to_install=How to install it? #------------------------------------------------------------------------------ # -- 2.39.5