aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/app/components/AdminContainer.tsx37
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx (renamed from server/sonar-web/src/main/js/apps/marketplace/components/EditionsStatusNotif.tsx)86
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotifContainer.tsx49
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx17
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx (renamed from server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionsStatusNotif-test.tsx)72
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap62
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/App.tsx98
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx42
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionsStatusNotif-test.tsx.snap61
-rw-r--r--server/sonar-web/src/main/js/apps/marketplace/utils.ts31
-rw-r--r--server/sonar-web/src/main/js/components/nav/NavBar.css5
-rw-r--r--server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx20
-rw-r--r--server/sonar-web/src/main/js/store/marketplace/actions.ts78
-rw-r--r--server/sonar-web/src/main/js/store/marketplace/reducer.ts55
-rw-r--r--server/sonar-web/src/main/js/store/marketplace/utils.ts49
-rw-r--r--server/sonar-web/src/main/js/store/rootReducer.js9
-rw-r--r--server/sonar-web/src/main/less/components/alerts.less5
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties2
20 files changed, 508 insertions, 274 deletions
diff --git a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx
index 12f2f691d8e..bf12d09e0a1 100644
--- a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx
+++ b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx
@@ -22,9 +22,15 @@ 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 {
+ getAppState,
+ getGlobalSettingValue,
+ getMarketplaceEditionStatus
+} from '../../store/rootReducer';
import { getSettingsNavigation } from '../../api/nav';
+import { EditionStatus, getEditionStatus } from '../../api/marketplace';
import { setAdminPages } from '../../store/appState/duck';
+import { fetchEditions, setEditionStatus } from '../../store/marketplace/actions';
import { translate } from '../../helpers/l10n';
import { Extension } from '../types';
@@ -32,9 +38,14 @@ interface Props {
appState: {
adminPages: Extension[];
organizationsEnabled: boolean;
+ version: string;
};
+ editionsUrl: string;
+ editionStatus?: EditionStatus;
+ fetchEditions: (url: string, version: string) => void;
location: {};
setAdminPages: (adminPages: Extension[]) => void;
+ setEditionStatus: (editionStatus: EditionStatus) => void;
}
class AdminContainer extends React.PureComponent<Props> {
@@ -49,18 +60,17 @@ class AdminContainer extends React.PureComponent<Props> {
handleRequredAuthorization.default()
);
} else {
- this.loadData();
+ this.fetchNavigationSettings();
+ this.props.fetchEditions(this.props.editionsUrl, this.props.appState.version);
+ this.fetchEditionStatus();
}
}
- loadData() {
- getSettingsNavigation().then(
- r => {
- this.props.setAdminPages(r.extensions);
- },
- () => {}
- );
- }
+ fetchNavigationSettings = () =>
+ getSettingsNavigation().then(r => this.props.setAdminPages(r.extensions), () => { });
+
+ fetchEditionStatus = () =>
+ getEditionStatus().then(editionStatus => this.props.setEditionStatus(editionStatus), () => { });
render() {
const { adminPages, organizationsEnabled } = this.props.appState;
@@ -77,6 +87,7 @@ class AdminContainer extends React.PureComponent<Props> {
<Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} />
<SettingsNav
customOrganizations={organizationsEnabled}
+ editionStatus={this.props.editionStatus}
extensions={adminPages}
location={this.props.location}
/>
@@ -87,9 +98,11 @@ class AdminContainer extends React.PureComponent<Props> {
}
const mapStateToProps = (state: any) => ({
- appState: getAppState(state)
+ appState: getAppState(state),
+ editionStatus: getMarketplaceEditionStatus(state),
+ editionsUrl: (getGlobalSettingValue(state, 'sonar.editions.jsonUrl') || {}).value
});
-const mapDispatchToProps = { setAdminPages };
+const mapDispatchToProps = { setAdminPages, setEditionStatus, fetchEditions };
export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer as any);
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/EditionsStatusNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx
index f21e8a30588..527b8479ada 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/components/EditionsStatusNotif.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx
@@ -18,41 +18,38 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import RestartForm from '../../../components/common/RestartForm';
-import CloseIcon from '../../../components/icons-components/CloseIcon';
-import { dismissErrorMessage, Edition, EditionStatus } from '../../../api/marketplace';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import NavBarNotif from '../../../../components/nav/NavBarNotif';
+import RestartForm from '../../../../components/common/RestartForm';
+import { dismissErrorMessage, Edition, EditionStatus } from '../../../../api/marketplace';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
interface Props {
editions?: Edition[];
editionStatus: EditionStatus;
- readOnly: boolean;
- updateEditionStatus: (editionStatus: EditionStatus) => void;
+ preventRestart: boolean;
+ setEditionStatus: (editionStatus: EditionStatus) => void;
}
interface State {
openRestart: boolean;
}
-export default class EditionsStatusNotif extends React.PureComponent<Props, State> {
+export default class SettingsEditionsNotif extends React.PureComponent<Props, State> {
state: State = { openRestart: false };
handleOpenRestart = () => this.setState({ openRestart: true });
hanleCloseRestart = () => this.setState({ openRestart: false });
- handleDismissError = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
- event.preventDefault();
+ handleDismissError = () =>
dismissErrorMessage().then(
- () =>
- this.props.updateEditionStatus({ ...this.props.editionStatus, installError: undefined }),
+ () => this.props.setEditionStatus({ ...this.props.editionStatus, installError: undefined }),
() => {}
);
- };
renderRestartMsg(edition?: Edition) {
- const { editionStatus, readOnly } = this.props;
+ const { editionStatus, preventRestart } = this.props;
return (
- <div className="alert alert-success">
+ <NavBarNotif className="alert alert-success">
<span>
{edition ? (
translateWithParameters(
@@ -63,20 +60,21 @@ export default class EditionsStatusNotif extends React.PureComponent<Props, Stat
translate('marketplace.status', editionStatus.installationStatus)
)}
</span>
- {!readOnly && (
+ {!preventRestart && (
<button className="js-restart spacer-left" onClick={this.handleOpenRestart}>
{translate('marketplace.restart')}
</button>
)}
- {!readOnly && this.state.openRestart && <RestartForm onClose={this.hanleCloseRestart} />}
- </div>
+ {!preventRestart &&
+ this.state.openRestart && <RestartForm onClose={this.hanleCloseRestart} />}
+ </NavBarNotif>
);
}
renderManualMsg(edition?: Edition) {
const { editionStatus } = this.props;
return (
- <div className="alert alert-danger">
+ <NavBarNotif className="alert alert-danger">
{edition ? (
translateWithParameters(
'marketplace.status_x.' + editionStatus.installationStatus,
@@ -85,26 +83,22 @@ export default class EditionsStatusNotif extends React.PureComponent<Props, Stat
) : (
translate('marketplace.status', editionStatus.installationStatus)
)}
- <p className="spacer-left">
- {edition && (
- <a
- className="button spacer-right"
- download={`sonarqube-${edition.name}.zip`}
- href={edition.downloadUrl}
- target="_blank">
- {translate('marketplace.download_package')}
- </a>
- )}
+ {edition && (
<a
- href="https://redirect.sonarsource.com/doc/how-to-install-an-edition.html"
+ className="button spacer-left"
+ download={`sonarqube-${edition.name}.zip`}
+ href={edition.downloadUrl}
target="_blank">
- {translate('marketplace.how_to_install')}
+ {translate('marketplace.download_package')}
</a>
- </p>
- <a className="little-spacer-left" href="https://www.sonarsource.com" target="_blank">
+ )}
+ <a
+ className="spacer-left"
+ href="https://redirect.sonarsource.com/doc/how-to-install-an-edition.html"
+ target="_blank">
{translate('marketplace.how_to_install')}
</a>
- </div>
+ </NavBarNotif>
);
}
@@ -117,10 +111,10 @@ export default class EditionsStatusNotif extends React.PureComponent<Props, Stat
switch (installationStatus) {
case 'AUTOMATIC_IN_PROGRESS':
return (
- <div className="alert alert-info">
+ <NavBarNotif className="alert alert-info">
<i className="spinner spacer-right text-bottom" />
<span>{translate('marketplace.status.AUTOMATIC_IN_PROGRESS')}</span>
- </div>
+ </NavBarNotif>
);
case 'AUTOMATIC_READY':
case 'UNINSTALL_IN_PROGRESS':
@@ -133,18 +127,14 @@ export default class EditionsStatusNotif extends React.PureComponent<Props, Stat
render() {
const { installError } = this.props.editionStatus;
- return (
- <div>
- {installError && (
- <div className="alert alert-danger alert-cancel">
- {installError}
- <a className="button-link text-danger" href="#" onClick={this.handleDismissError}>
- <CloseIcon />
- </a>
- </div>
- )}
- {this.renderStatusAlert()}
- </div>
- );
+ if (installError) {
+ return (
+ <NavBarNotif className="alert alert-danger" onCancel={this.handleDismissError}>
+ {installError}
+ </NavBarNotif>
+ );
+ }
+
+ return this.renderStatusAlert();
}
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotifContainer.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotifContainer.tsx
new file mode 100644
index 00000000000..6bac95a5897
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotifContainer.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 { connect } from 'react-redux';
+import SettingsEditionsNotif from './SettingsEditionsNotif';
+import { getAppState, getMarketplaceEditions } from '../../../../store/rootReducer';
+import { Edition, EditionStatus } from '../../../../api/marketplace';
+import { setEditionStatus } from '../../../../store/marketplace/actions';
+
+interface OwnProps {
+ editionStatus: EditionStatus;
+}
+
+interface StateToProps {
+ editions?: Edition[];
+ preventRestart: boolean;
+}
+
+interface DispatchToProps {
+ setEditionStatus: (editionStatus: EditionStatus) => void;
+}
+
+const mapStateToProps = (state: any): StateToProps => ({
+ editions: getMarketplaceEditions(state),
+ preventRestart: !getAppState(state).standalone
+});
+
+const mapDispatchToProps = { setEditionStatus };
+
+export default connect<StateToProps, DispatchToProps, OwnProps>(
+ mapStateToProps,
+ mapDispatchToProps
+)(SettingsEditionsNotif);
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
index 49bb352fb63..4a983b4feed 100644
--- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
@@ -21,11 +21,14 @@ import * as React from 'react';
import * as classNames from 'classnames';
import { IndexLink, Link } from 'react-router';
import ContextNavBar from '../../../../components/nav/ContextNavBar';
+import SettingsEditionsNotifContainer from './SettingsEditionsNotifContainer';
import NavBarTabs from '../../../../components/nav/NavBarTabs';
+import { EditionStatus } from '../../../../api/marketplace';
import { Extension } from '../../../types';
import { translate } from '../../../../helpers/l10n';
interface Props {
+ editionStatus?: EditionStatus;
extensions: Extension[];
customOrganizations: boolean;
location: {};
@@ -77,7 +80,7 @@ export default class SettingsNav extends React.PureComponent<Props> {
};
render() {
- const { customOrganizations, extensions } = this.props;
+ const { customOrganizations, editionStatus, extensions } = this.props;
const isSecurity = this.isSecurityActive();
const isProjects = this.isProjectsActive();
const isSystem = this.isSystemActive();
@@ -95,8 +98,18 @@ export default class SettingsNav extends React.PureComponent<Props> {
const hasSupportExtension = extensionsWithoutSupport.length < extensions.length;
+ let notifComponent;
+ if (
+ editionStatus &&
+ (editionStatus.installError || editionStatus.installationStatus !== 'NONE')
+ ) {
+ notifComponent = <SettingsEditionsNotifContainer 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>
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionsStatusNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx
index 4275351147c..3614b9b06fe 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionsStatusNotif-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx
@@ -18,15 +18,15 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { shallow } from 'enzyme';
-import { click } from '../../../../helpers/testUtils';
-import EditionsStatusNotif from '../EditionsStatusNotif';
+import { mount, shallow } from 'enzyme';
+import { click } from '../../../../../helpers/testUtils';
+import SettingsEditionsNotif from '../SettingsEditionsNotif';
-jest.mock('../../../../api/marketplace', () => ({
+jest.mock('../../../../../api/marketplace', () => ({
dismissErrorMessage: jest.fn(() => Promise.resolve())
}));
-const dismissMsg = require('../../../../api/marketplace').dismissErrorMessage as jest.Mock<any>;
+const dismissMsg = require('../../../../../api/marketplace').dismissErrorMessage as jest.Mock<any>;
beforeEach(() => {
dismissMsg.mockClear();
@@ -34,10 +34,10 @@ beforeEach(() => {
it('should display an in progress notif', () => {
const wrapper = shallow(
- <EditionsStatusNotif
+ <SettingsEditionsNotif
editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }}
- readOnly={false}
- updateEditionStatus={jest.fn()}
+ preventRestart={false}
+ setEditionStatus={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
@@ -45,10 +45,31 @@ it('should display an in progress notif', () => {
it('should display a ready notification', () => {
const wrapper = shallow(
- <EditionsStatusNotif
+ <SettingsEditionsNotif
editionStatus={{ installationStatus: 'AUTOMATIC_READY' }}
- readOnly={false}
- updateEditionStatus={jest.fn()}
+ preventRestart={false}
+ setEditionStatus={jest.fn()}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should display a manual installation notification', () => {
+ const wrapper = shallow(
+ <SettingsEditionsNotif
+ editionStatus={{ installationStatus: 'MANUAL_IN_PROGRESS', nextEditionKey: 'foo' }}
+ editions={[
+ {
+ key: 'foo',
+ name: 'Foo',
+ textDescription: 'Foo desc',
+ downloadUrl: 'download_url',
+ homeUrl: 'more_url',
+ requestUrl: 'license_url'
+ }
+ ]}
+ preventRestart={false}
+ setEditionStatus={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
@@ -56,29 +77,40 @@ it('should display a ready notification', () => {
it('should display install errors', () => {
const wrapper = shallow(
- <EditionsStatusNotif
+ <SettingsEditionsNotif
editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS', installError: 'Foo error' }}
- readOnly={false}
- updateEditionStatus={jest.fn()}
+ preventRestart={false}
+ setEditionStatus={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
});
it('should allow to dismiss install errors', async () => {
- const updateEditionStatus = jest.fn();
- const wrapper = shallow(
- <EditionsStatusNotif
+ const setEditionStatus = jest.fn();
+ const wrapper = mount(
+ <SettingsEditionsNotif
editionStatus={{ installationStatus: 'NONE', installError: 'Foo error' }}
- readOnly={false}
- updateEditionStatus={updateEditionStatus}
+ preventRestart={false}
+ setEditionStatus={setEditionStatus}
/>
);
click(wrapper.find('a'));
expect(dismissMsg).toHaveBeenCalled();
await new Promise(setImmediate);
- expect(updateEditionStatus).toHaveBeenCalledWith({
+ expect(setEditionStatus).toHaveBeenCalledWith({
installationStatus: 'NONE',
installError: undefined
});
});
+
+it('should not display the restart button', () => {
+ const wrapper = shallow(
+ <SettingsEditionsNotif
+ editionStatus={{ installationStatus: 'AUTOMATIC_READY' }}
+ preventRestart={true}
+ setEditionStatus={jest.fn()}
+ />
+ );
+ expect(wrapper.find('button.js-restart').exists()).toBeFalsy();
+});
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..51ab67171a5
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap
@@ -0,0 +1,62 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display a manual installation notification 1`] = `
+<NavBarNotif
+ className="alert alert-danger"
+>
+ marketplace.status_x.MANUAL_IN_PROGRESS.Foo
+ <a
+ className="button spacer-left"
+ download="sonarqube-Foo.zip"
+ href="download_url"
+ target="_blank"
+ >
+ marketplace.download_package
+ </a>
+ <a
+ className="spacer-left"
+ href="https://redirect.sonarsource.com/doc/how-to-install-an-edition.html"
+ target="_blank"
+ >
+ marketplace.how_to_install
+ </a>
+</NavBarNotif>
+`;
+
+exports[`should display a ready notification 1`] = `
+<NavBarNotif
+ className="alert alert-success"
+>
+ <span>
+ marketplace.status.AUTOMATIC_READY
+ </span>
+ <button
+ className="js-restart spacer-left"
+ onClick={[Function]}
+ >
+ marketplace.restart
+ </button>
+</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>
+`;
+
+exports[`should display install errors 1`] = `
+<NavBarNotif
+ className="alert alert-danger"
+ onCancel={[Function]}
+>
+ Foo error
+</NavBarNotif>
+`;
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 c57f3de78fd..4d6f63fd285 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/App.tsx
+++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx
@@ -22,7 +22,6 @@ import * as PropTypes from 'prop-types';
import { sortBy, uniqBy } from 'lodash';
import Helmet from 'react-helmet';
import Header from './Header';
-import EditionsStatusNotif from './components/EditionsStatusNotif';
import EditionBoxes from './EditionBoxes';
import Footer from './Footer';
import PendingActions from './PendingActions';
@@ -36,31 +35,24 @@ import {
Plugin,
PluginPending
} from '../../api/plugins';
-import { Edition, EditionStatus, getEditionsList, getEditionStatus } from '../../api/marketplace';
+import { Edition, EditionStatus } from '../../api/marketplace';
import { RawQuery } from '../../helpers/query';
import { translate } from '../../helpers/l10n';
-import {
- getEditionsForLastVersion,
- getEditionsForVersion,
- filterPlugins,
- parseQuery,
- Query,
- serializeQuery
-} from './utils';
+import { filterPlugins, parseQuery, Query, serializeQuery } from './utils';
+import './style.css';
export interface Props {
- editionsUrl: string;
+ editions?: Edition[];
+ editionsReadOnly: boolean;
+ editionStatus?: EditionStatus;
+ loadingEditions: boolean;
location: { pathname: string; query: RawQuery };
- sonarqubeVersion: string;
standaloneMode: boolean;
updateCenterActive: boolean;
+ setEditionStatus: (editionStatus: EditionStatus) => void;
}
interface State {
- editions?: Edition[];
- editionsReadOnly: boolean;
- editionStatus?: EditionStatus;
- loadingEditions: boolean;
loadingPlugins: boolean;
pending: {
installing: PluginPending[];
@@ -72,7 +64,6 @@ interface State {
export default class App extends React.PureComponent<Props, State> {
mounted: boolean;
- timer?: NodeJS.Timer;
static contextTypes = {
router: PropTypes.object.isRequired
@@ -81,8 +72,6 @@ export default class App extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
- editionsReadOnly: false,
- loadingEditions: true,
loadingPlugins: true,
pending: {
installing: [],
@@ -95,9 +84,7 @@ export default class App extends React.PureComponent<Props, State> {
componentDidMount() {
this.mounted = true;
- this.fetchEditions();
this.fetchPendingPlugins();
- this.fetchEditionStatus();
this.fetchQueryPlugins();
}
@@ -154,55 +141,6 @@ export default class App extends React.PureComponent<Props, State> {
() => {}
);
- fetchEditionStatus = () =>
- getEditionStatus().then(
- editionStatus => {
- if (this.mounted) {
- this.updateEditionStatus(editionStatus);
- }
- },
- () => {}
- );
-
- fetchEditions = () => {
- this.setState({ loadingEditions: true });
- getEditionsList(this.props.editionsUrl).then(
- editionsPerVersion => {
- if (this.mounted) {
- const newState = {
- editions: getEditionsForVersion(editionsPerVersion, this.props.sonarqubeVersion),
- editionsReadOnly: false,
- loadingEditions: false
- };
- if (!newState.editions) {
- newState.editions = getEditionsForLastVersion(editionsPerVersion);
- newState.editionsReadOnly = true;
- }
- this.setState(newState);
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loadingEditions: false });
- }
- }
- );
- };
-
- updateEditionStatus = (editionStatus: EditionStatus) => {
- this.setState({ editionStatus });
- if (this.timer) {
- global.clearTimeout(this.timer);
- this.timer = undefined;
- }
- if (editionStatus.installationStatus === 'AUTOMATIC_IN_PROGRESS') {
- this.timer = global.setTimeout(() => {
- this.fetchEditionStatus();
- this.timer = undefined;
- }, 2000);
- }
- };
-
updateQuery = (newQuery: Partial<Query>) => {
const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
this.context.router.push({ pathname: this.props.location.pathname, query });
@@ -215,8 +153,8 @@ export default class App extends React.PureComponent<Props, State> {
};
render() {
- const { standaloneMode } = this.props;
- const { editions, editionStatus, loadingPlugins, plugins, pending } = this.state;
+ const { editions, editionStatus, standaloneMode } = this.props;
+ const { loadingPlugins, plugins, pending } = this.state;
const query = parseQuery(this.props.location.query);
const filteredPlugins = query.search ? filterPlugins(plugins, query.search) : plugins;
@@ -224,14 +162,6 @@ export default class App extends React.PureComponent<Props, State> {
<div className="page page-limited" id="marketplace-page">
<Helmet title={translate('marketplace.page')} />
<div className="page-notifs">
- {editionStatus && (
- <EditionsStatusNotif
- editions={editions}
- editionStatus={editionStatus}
- readOnly={!standaloneMode}
- updateEditionStatus={this.updateEditionStatus}
- />
- )}
{standaloneMode && (
<PendingActions refreshPending={this.fetchPendingPlugins} pending={pending} />
)}
@@ -239,13 +169,11 @@ export default class App extends React.PureComponent<Props, State> {
<Header />
<EditionBoxes
editions={editions}
- loading={this.state.loadingEditions}
+ loading={this.props.loadingEditions}
editionStatus={editionStatus}
- editionsUrl={this.props.editionsUrl}
- readOnly={!standaloneMode || this.state.editionsReadOnly}
- sonarqubeVersion={this.props.sonarqubeVersion}
+ readOnly={!standaloneMode || this.props.editionsReadOnly}
updateCenterActive={this.props.updateCenterActive}
- updateEditionStatus={this.updateEditionStatus}
+ updateEditionStatus={this.props.setEditionStatus}
/>
<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 90fac954650..6d2b46b010d 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
@@ -19,15 +19,47 @@
*/
import { connect } from 'react-redux';
import App from './App';
-import { getAppState, getGlobalSettingValue } from '../../store/rootReducer';
-import './style.css';
+import {
+ getAppState,
+ getGlobalSettingValue,
+ getMarketplaceState,
+ getMarketplaceEditions,
+ getMarketplaceEditionStatus
+} from '../../store/rootReducer';
+import { Edition, EditionStatus } from '../../api/marketplace';
+import { setEditionStatus } from '../../store/marketplace/actions';
+import { RawQuery } from '../../helpers/query';
+
+interface OwnProps {
+ location: { pathname: string; query: RawQuery };
+}
+
+interface StateToProps {
+ editions?: Edition[];
+ editionsReadOnly: boolean;
+ editionStatus?: EditionStatus;
+ loadingEditions: boolean;
+ standaloneMode: boolean;
+ updateCenterActive: boolean;
+}
+
+interface DispatchToProps {
+ setEditionStatus: (editionStatus: EditionStatus) => void;
+}
const mapStateToProps = (state: any) => ({
- editionsUrl: (getGlobalSettingValue(state, 'sonar.editions.jsonUrl') || {}).value,
- sonarqubeVersion: getAppState(state).version,
+ editions: getMarketplaceEditions(state),
+ editionsReadOnly: getMarketplaceState(state).readOnly,
+ editionStatus: getMarketplaceEditionStatus(state),
+ loadingEditions: getMarketplaceState(state).loading,
standaloneMode: getAppState(state).standalone,
updateCenterActive:
(getGlobalSettingValue(state, 'sonar.updatecenter.activate') || {}).value === 'true'
});
-export default connect(mapStateToProps)(App as any);
+const mapDispatchToProps = { setEditionStatus };
+
+export default connect<StateToProps, DispatchToProps, OwnProps>(
+ mapStateToProps,
+ mapDispatchToProps
+)(App);
diff --git a/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx b/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx
index e9b7fa6f246..30370c1d3b2 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx
+++ b/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx
@@ -28,10 +28,8 @@ import { translate } from '../../helpers/l10n';
export interface Props {
editions?: Edition[];
editionStatus?: EditionStatus;
- editionsUrl: string;
loading: boolean;
readOnly: boolean;
- sonarqubeVersion: string;
updateCenterActive: boolean;
updateEditionStatus: (editionStatus: EditionStatus) => void;
}
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
index eeff50797ce..c0347bf9ad4 100644
--- 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
@@ -70,9 +70,7 @@ function getWrapper(props = {}) {
<EditionBoxes
loading={false}
editionStatus={DEFAULT_STATUS}
- editionsUrl=""
readOnly={false}
- sonarqubeVersion="6.7.5"
updateCenterActive={true}
updateEditionStatus={jest.fn()}
{...props}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionsStatusNotif-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionsStatusNotif-test.tsx.snap
deleted file mode 100644
index a76a31c61d2..00000000000
--- a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionsStatusNotif-test.tsx.snap
+++ /dev/null
@@ -1,61 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display a ready notification 1`] = `
-<div>
- <div
- className="alert alert-success"
- >
- <span>
- marketplace.status.AUTOMATIC_READY
- </span>
- <button
- className="js-restart spacer-left"
- onClick={[Function]}
- >
- marketplace.restart
- </button>
- </div>
-</div>
-`;
-
-exports[`should display an in progress notif 1`] = `
-<div>
- <div
- className="alert alert-info"
- >
- <i
- className="spinner spacer-right text-bottom"
- />
- <span>
- marketplace.status.AUTOMATIC_IN_PROGRESS
- </span>
- </div>
-</div>
-`;
-
-exports[`should display install errors 1`] = `
-<div>
- <div
- className="alert alert-danger alert-cancel"
- >
- Foo error
- <a
- className="button-link text-danger"
- href="#"
- onClick={[Function]}
- >
- <CloseIcon />
- </a>
- </div>
- <div
- className="alert alert-info"
- >
- <i
- className="spinner spacer-right text-bottom"
- />
- <span>
- marketplace.status.AUTOMATIC_IN_PROGRESS
- </span>
- </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/marketplace/utils.ts b/server/sonar-web/src/main/js/apps/marketplace/utils.ts
index 31835b2ec56..780fafb3af8 100644
--- a/server/sonar-web/src/main/js/apps/marketplace/utils.ts
+++ b/server/sonar-web/src/main/js/apps/marketplace/utils.ts
@@ -17,9 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { memoize, sortBy } from 'lodash';
+import { memoize } from 'lodash';
import { Plugin, PluginAvailable, PluginInstalled, PluginPending } from '../../api/plugins';
-import { Edition, EditionsPerVersion } from '../../api/marketplace';
import { cleanQuery, parseAsString, RawQuery, serializeString } from '../../helpers/query';
export interface Query {
@@ -52,34 +51,6 @@ export function filterPlugins(plugins: Plugin[], search: string): Plugin[] {
});
}
-export function getEditionsForLastVersion(editions: EditionsPerVersion): Edition[] {
- const sortedVersion = sortBy(Object.keys(editions), [
- (version: string) => -Number(version.split('.')[0]),
- (version: string) => -Number(version.split('.')[1] || 0),
- (version: string) => -Number(version.split('.')[2] || 0)
- ]);
- return editions[sortedVersion[0]];
-}
-
-export function getEditionsForVersion(
- editions: EditionsPerVersion,
- version: string
-): Edition[] | undefined {
- const minorVersion = version.match(/\d+\.\d+.\d+/);
- if (minorVersion) {
- if (editions[minorVersion[0]]) {
- return editions[minorVersion[0]];
- }
- }
- const majorVersion = version.match(/\d+\.\d+/);
- if (majorVersion) {
- if (editions[majorVersion[0]]) {
- return editions[majorVersion[0]];
- }
- }
- return undefined;
-}
-
export const parseQuery = memoize((urlQuery: RawQuery): Query => ({
filter: parseAsString(urlQuery['filter']) || DEFAULT_FILTER,
search: parseAsString(urlQuery['search'])
diff --git a/server/sonar-web/src/main/js/components/nav/NavBar.css b/server/sonar-web/src/main/js/components/nav/NavBar.css
index fb9393dd02e..0a24b4232ad 100644
--- a/server/sonar-web/src/main/js/components/nav/NavBar.css
+++ b/server/sonar-web/src/main/js/components/nav/NavBar.css
@@ -28,3 +28,8 @@
border-right: none;
padding: 6px 0;
}
+
+.navbar-notif-cancelable {
+ display: flex;
+ justify-content: space-between;
+}
diff --git a/server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx b/server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx
index 9004c1b7fe9..c276e20f62f 100644
--- a/server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx
+++ b/server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx
@@ -19,20 +19,38 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
+import CloseIcon from '../icons-components/CloseIcon';
interface Props {
children?: React.ReactNode;
className?: string;
+ onCancel?: () => {};
}
export default class NavBarNotif extends React.PureComponent<Props> {
+ handleCancel = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ if (this.props.onCancel) {
+ this.props.onCancel();
+ }
+ };
+
render() {
if (!this.props.children) {
return null;
}
return (
<div className={classNames('navbar-notif', this.props.className)}>
- <div className="navbar-limited clearfix">{this.props.children}</div>
+ <div className="navbar-limited clearfix">
+ <div className={classNames({ 'navbar-notif-cancelable': !!this.props.onCancel })}>
+ {this.props.children}
+ {this.props.onCancel && (
+ <a className="button-link text-danger" href="#" onClick={this.handleCancel}>
+ <CloseIcon />
+ </a>
+ )}
+ </div>
+ </div>
</div>
);
}
diff --git a/server/sonar-web/src/main/js/store/marketplace/actions.ts b/server/sonar-web/src/main/js/store/marketplace/actions.ts
new file mode 100644
index 00000000000..92e18cd48a7
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/marketplace/actions.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { Dispatch } from 'react-redux';
+import { getEditionsForVersion, getEditionsForLastVersion } from './utils';
+import { Edition, EditionStatus, getEditionStatus, getEditionsList } from '../../api/marketplace';
+
+interface LoadEditionsAction {
+ type: 'LOAD_EDITIONS';
+ loading: boolean;
+}
+
+interface SetEditionsAction {
+ type: 'SET_EDITIONS';
+ editions: Edition[];
+ readOnly: boolean;
+}
+
+interface SetEditionStatusAction {
+ type: 'SET_EDITION_STATUS';
+ status: EditionStatus;
+}
+
+export type Action = LoadEditionsAction | SetEditionsAction | SetEditionStatusAction;
+
+export function loadEditions(loading: boolean = true): LoadEditionsAction {
+ return { type: 'LOAD_EDITIONS', loading };
+}
+
+export function setEditions(editions: Edition[], readOnly?: boolean): SetEditionsAction {
+ return { type: 'SET_EDITIONS', editions, readOnly: !!readOnly };
+}
+
+let editionTimer: number | undefined;
+export const setEditionStatus = (status: EditionStatus) => (dispatch: Dispatch<Action>) => {
+ dispatch({ type: 'SET_EDITION_STATUS', status });
+ if (editionTimer) {
+ window.clearTimeout(editionTimer);
+ editionTimer = undefined;
+ }
+ if (status.installationStatus === 'AUTOMATIC_IN_PROGRESS') {
+ editionTimer = window.setTimeout(() => {
+ getEditionStatus().then(status => setEditionStatus(status)(dispatch), () => { });
+ editionTimer = undefined;
+ }, 2000);
+ }
+};
+
+export const fetchEditions = (url: string, version: string) => (dispatch: Dispatch<Action>) => {
+ dispatch(loadEditions(true));
+ getEditionsList(url).then(
+ editionsPerVersion => {
+ const editions = getEditionsForVersion(editionsPerVersion, version);
+ if (editions) {
+ dispatch(setEditions(editions));
+ } else {
+ dispatch(setEditions(getEditionsForLastVersion(editionsPerVersion), true));
+ }
+ },
+ () => dispatch(loadEditions(false))
+ );
+};
diff --git a/server/sonar-web/src/main/js/store/marketplace/reducer.ts b/server/sonar-web/src/main/js/store/marketplace/reducer.ts
new file mode 100644
index 00000000000..2d48eba1c1e
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/marketplace/reducer.ts
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { Action } from './actions';
+import { Edition, EditionStatus } from '../../api/marketplace';
+
+interface State {
+ editions?: Edition[];
+ loading: boolean;
+ status?: EditionStatus;
+ readOnly: boolean;
+}
+
+const defaultState: State = {
+ loading: true,
+ readOnly: false
+};
+
+export default function(state: State = defaultState, action: Action): State {
+ if (action.type === 'SET_EDITIONS') {
+ return { ...state, editions: action.editions, readOnly: action.readOnly, loading: false };
+ }
+ if (action.type === 'LOAD_EDITIONS') {
+ return { ...state, loading: action.loading };
+ }
+ if (action.type === 'SET_EDITION_STATUS') {
+ const hasChanged = Object.keys(action.status).some(
+ (key: keyof EditionStatus) => !state.status || state.status[key] !== action.status[key]
+ );
+ // Prevent from rerendering the whole admin if the status didn't change
+ if (hasChanged) {
+ return { ...state, status: action.status };
+ }
+ }
+ return state;
+}
+
+export const getEditions = (state: State) => state.editions;
+export const getEditionStatus = (state: State) => state.status;
diff --git a/server/sonar-web/src/main/js/store/marketplace/utils.ts b/server/sonar-web/src/main/js/store/marketplace/utils.ts
new file mode 100644
index 00000000000..51c22d707d8
--- /dev/null
+++ b/server/sonar-web/src/main/js/store/marketplace/utils.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { sortBy } from 'lodash';
+import { Edition, EditionsPerVersion } from '../../api/marketplace';
+
+export function getEditionsForLastVersion(editions: EditionsPerVersion): Edition[] {
+ const sortedVersion = sortBy(Object.keys(editions), [
+ (version: string) => -Number(version.split('.')[0]),
+ (version: string) => -Number(version.split('.')[1] || 0),
+ (version: string) => -Number(version.split('.')[2] || 0)
+ ]);
+ return editions[sortedVersion[0]];
+}
+
+export function getEditionsForVersion(
+ editions: EditionsPerVersion,
+ version: string
+): Edition[] | undefined {
+ const minorVersion = version.match(/\d+\.\d+.\d+/);
+ if (minorVersion) {
+ if (editions[minorVersion[0]]) {
+ return editions[minorVersion[0]];
+ }
+ }
+ const majorVersion = version.match(/\d+\.\d+/);
+ if (majorVersion) {
+ if (editions[majorVersion[0]]) {
+ return editions[majorVersion[0]];
+ }
+ }
+ return undefined;
+}
diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js
index 9edca6126da..9897df6974a 100644
--- a/server/sonar-web/src/main/js/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/store/rootReducer.js
@@ -19,6 +19,7 @@
*/
import { combineReducers } from 'redux';
import appState from './appState/duck';
+import marketplace, * as fromMarketplace from './marketplace/reducer';
import users, * as fromUsers from './users/reducer';
import favorites, * as fromFavorites from './favorites/duck';
import languages, * as fromLanguages from './languages/reducer';
@@ -37,6 +38,7 @@ export default combineReducers({
globalMessages,
favorites,
languages,
+ marketplace,
metrics,
notifications,
organizations,
@@ -73,6 +75,13 @@ export const getUsers = state => fromUsers.getUsers(state.users);
export const isFavorite = (state, componentKey) =>
fromFavorites.isFavorite(state.favorites, componentKey);
+export const getMarketplaceState = state => state.marketplace;
+
+export const getMarketplaceEditions = state => fromMarketplace.getEditions(state.marketplace);
+
+export const getMarketplaceEditionStatus = state =>
+ fromMarketplace.getEditionStatus(state.marketplace);
+
export const getMetrics = state => fromMetrics.getMetrics(state.metrics);
export const getMetricByKey = (state, key) => fromMetrics.getMetricByKey(state.metrics, key);
diff --git a/server/sonar-web/src/main/less/components/alerts.less b/server/sonar-web/src/main/less/components/alerts.less
index 1cc1c516fe2..3fbc9952960 100644
--- a/server/sonar-web/src/main/less/components/alerts.less
+++ b/server/sonar-web/src/main/less/components/alerts.less
@@ -66,11 +66,6 @@
.alert-emphasis-variant(#3c763d, #dff0d8, #d6e9c6);
}
-.alert-cancel {
- display: flex;
- justify-content: space-between;
-}
-
.page-notifs .alert {
padding: 8px 10px;
}
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 30f40064b78..3b7da354e46 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -2103,7 +2103,7 @@ marketplace.status.AUTOMATIC_IN_PROGRESS=Updating your installation... Please wa
marketplace.status.AUTOMATIC_READY=Commercial Edition successfully installed. Please restart the server to activate your new features.
marketplace.status.UNINSTALL_IN_PROGRESS=Commercial Edition successfully uninstalled. Please restart the server to remove the features.
marketplace.status.MANUAL_IN_PROGRESS=Can't install Commercial Edition because of internet access issue. Please manually install the package in your SonarQube's plugins folder.
-marketplace.status_x.AUTOMATIC_READY={0} successfully installed. Please resstart the server to activate your new features.
+marketplace.status_x.AUTOMATIC_READY={0} successfully installed. Please restart the server to activate your new features.
marketplace.status_X.UNINSTALL_IN_PROGRESS={0} successfully uninstalled. Please restart the server to remove the features.
marketplace.status_x.MANUAL_IN_PROGRESS=Can't install {0} because of internet access issue. Please manually install the package in your SonarQube's plugins folder.
marketplace.how_to_install=How to install it?