Browse Source

SONAR-9936 Add Editions pack in the marketplace

tags/6.7-RC1
Grégoire Aubert 6 years ago
parent
commit
48fbc92a51
24 changed files with 1027 additions and 60 deletions
  1. 58
    0
      server/sonar-web/src/main/js/api/marketplace.ts
  2. 40
    14
      server/sonar-web/src/main/js/app/components/AdminContainer.tsx
  3. 2
    2
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  4. 60
    0
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx
  5. 31
    23
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
  6. 43
    0
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx
  7. 22
    3
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx
  8. 39
    0
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap
  9. 15
    0
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
  10. 3
    3
      server/sonar-web/src/main/js/app/types.ts
  11. 17
    2
      server/sonar-web/src/main/js/apps/marketplace/App.tsx
  12. 3
    1
      server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
  13. 104
    0
      server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx
  14. 66
    0
      server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx
  15. 2
    2
      server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx
  16. 73
    0
      server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap
  17. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap
  18. 72
    0
      server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
  19. 100
    0
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx
  20. 192
    0
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap
  21. 32
    0
      server/sonar-web/src/main/js/apps/marketplace/style.css
  22. 19
    5
      server/sonar-web/src/main/js/helpers/request.ts
  23. 26
    4
      server/sonar-web/src/main/js/store/appState/duck.ts
  24. 7
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 58
- 0
server/sonar-web/src/main/js/api/marketplace.ts View File

@@ -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);
}

server/sonar-web/src/main/js/app/components/AdminContainer.js → server/sonar-web/src/main/js/app/components/AdminContainer.tsx View File

@@ -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);

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

@@ -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}>

+ 60
- 0
server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx View File

@@ -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;
}
}

server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js → server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx View File

@@ -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;

+ 43
- 0
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx View File

@@ -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();
});

server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.js → server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx View File

@@ -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();
});

+ 39
- 0
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap View File

@@ -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>
`;

server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap → server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap View File

@@ -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}

+ 3
- 3
server/sonar-web/src/main/js/app/types.ts View File

@@ -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;

+ 17
- 2
server/sonar-web/src/main/js/apps/marketplace/App.tsx View File

@@ -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}

+ 3
- 1
server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx View File

@@ -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
});


+ 104
- 0
server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx View File

@@ -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>
);
}
}

+ 66
- 0
server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx View File

@@ -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} />
);
}

+ 2
- 2
server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx View File

@@ -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 () => {

+ 73
- 0
server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap View File

@@ -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>
`;

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap View File

@@ -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`;

+ 72
- 0
server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx View File

@@ -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>
);
}
}

+ 100
- 0
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx View File

@@ -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}
/>
);
}

+ 192
- 0
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap View File

@@ -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>
`;

+ 32
- 0
server/sonar-web/src/main/js/apps/marketplace/style.css View File

@@ -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;
}

+ 19
- 5
server/sonar-web/src/main/js/helpers/request.ts View File

@@ -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);
}

@@ -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
*/

+ 26
- 4
server/sonar-web/src/main/js/store/appState/duck.ts View File

@@ -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 };
}

+ 7
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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?

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save