aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-10-16 10:01:37 +0200
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-10-23 08:01:13 -0700
commit48fbc92a514fe94ad1ac435ddc033bd2379a5a69 (patch)
treef29a16c36a30a737282b7fa2ded566940cb0fb1f /server
parentcc7c40ced053d5ba7bcc453b28f9cfcdf022a180 (diff)
downloadsonarqube-48fbc92a514fe94ad1ac435ddc033bd2379a5a69.tar.gz
sonarqube-48fbc92a514fe94ad1ac435ddc033bd2379a5a69.zip
SONAR-9936 Add Editions pack in the marketplace
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/marketplace.ts58
-rw-r--r--server/sonar-web/src/main/js/app/components/AdminContainer.tsx (renamed from server/sonar-web/src/main/js/app/components/AdminContainer.js)54
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx60
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js)54
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx43
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.js)25
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap39
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap (renamed from server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap)15
-rw-r--r--server/sonar-web/src/main/js/app/types.ts6
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/App.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx104
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap73
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx100
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap192
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/style.css32
-rw-r--r--server/sonar-web/src/main/js/helpers/request.ts24
-rw-r--r--server/sonar-web/src/main/js/store/appState/duck.ts30
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 };
}