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';
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> {
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;
<Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} />
<SettingsNav
customOrganizations={organizationsEnabled}
+ editionStatus={this.props.editionStatus}
extensions={adminPages}
location={this.props.location}
/>
}
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);
--- /dev/null
+/*
+ * 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 RestartForm from '../../../../components/common/RestartForm';
+import { dismissErrorMessage, Edition, EditionStatus } from '../../../../api/marketplace';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+
+interface Props {
+ editions?: Edition[];
+ editionStatus: EditionStatus;
+ preventRestart: boolean;
+ setEditionStatus: (editionStatus: EditionStatus) => void;
+}
+
+interface State {
+ openRestart: boolean;
+}
+
+export default class SettingsEditionsNotif extends React.PureComponent<Props, State> {
+ state: State = { openRestart: false };
+
+ handleOpenRestart = () => this.setState({ openRestart: true });
+ hanleCloseRestart = () => this.setState({ openRestart: false });
+
+ handleDismissError = () =>
+ dismissErrorMessage().then(
+ () => this.props.setEditionStatus({ ...this.props.editionStatus, installError: undefined }),
+ () => {}
+ );
+
+ renderRestartMsg(edition?: Edition) {
+ const { editionStatus, preventRestart } = this.props;
+ return (
+ <NavBarNotif className="alert alert-success">
+ <span>
+ {edition ? (
+ translateWithParameters(
+ 'marketplace.status_x.' + editionStatus.installationStatus,
+ edition.name
+ )
+ ) : (
+ translate('marketplace.status', editionStatus.installationStatus)
+ )}
+ </span>
+ {!preventRestart && (
+ <button className="js-restart spacer-left" onClick={this.handleOpenRestart}>
+ {translate('marketplace.restart')}
+ </button>
+ )}
+ {!preventRestart &&
+ this.state.openRestart && <RestartForm onClose={this.hanleCloseRestart} />}
+ </NavBarNotif>
+ );
+ }
+
+ renderManualMsg(edition?: Edition) {
+ const { editionStatus } = this.props;
+ return (
+ <NavBarNotif className="alert alert-danger">
+ {edition ? (
+ translateWithParameters(
+ 'marketplace.status_x.' + editionStatus.installationStatus,
+ edition.name
+ )
+ ) : (
+ translate('marketplace.status', editionStatus.installationStatus)
+ )}
+ {edition && (
+ <a
+ className="button spacer-left"
+ download={`sonarqube-${edition.name}.zip`}
+ href={edition.downloadUrl}
+ target="_blank">
+ {translate('marketplace.download_package')}
+ </a>
+ )}
+ <a
+ className="spacer-left"
+ href="https://redirect.sonarsource.com/doc/how-to-install-an-edition.html"
+ target="_blank">
+ {translate('marketplace.how_to_install')}
+ </a>
+ </NavBarNotif>
+ );
+ }
+
+ renderStatusAlert() {
+ const { editionStatus } = this.props;
+ const { installationStatus, nextEditionKey } = editionStatus;
+ const nextEdition =
+ this.props.editions && this.props.editions.find(edition => edition.key === nextEditionKey);
+
+ switch (installationStatus) {
+ case '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>
+ );
+ case 'AUTOMATIC_READY':
+ case 'UNINSTALL_IN_PROGRESS':
+ return this.renderRestartMsg(nextEdition);
+ case 'MANUAL_IN_PROGRESS':
+ return this.renderManualMsg(nextEdition);
+ }
+ return null;
+ }
+
+ render() {
+ const { installError } = this.props.editionStatus;
+ if (installError) {
+ return (
+ <NavBarNotif className="alert alert-danger" onCancel={this.handleDismissError}>
+ {installError}
+ </NavBarNotif>
+ );
+ }
+
+ return this.renderStatusAlert();
+ }
+}
--- /dev/null
+/*
+ * 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);
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: {};
};
render() {
- const { customOrganizations, extensions } = this.props;
+ const { customOrganizations, editionStatus, extensions } = this.props;
const isSecurity = this.isSecurityActive();
const isProjects = this.isProjectsActive();
const isSystem = this.isSystemActive();
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>
--- /dev/null
+/*
+ * 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 { mount, shallow } from 'enzyme';
+import { click } from '../../../../../helpers/testUtils';
+import SettingsEditionsNotif from '../SettingsEditionsNotif';
+
+jest.mock('../../../../../api/marketplace', () => ({
+ dismissErrorMessage: jest.fn(() => Promise.resolve())
+}));
+
+const dismissMsg = require('../../../../../api/marketplace').dismissErrorMessage as jest.Mock<any>;
+
+beforeEach(() => {
+ dismissMsg.mockClear();
+});
+
+it('should display an in progress notif', () => {
+ const wrapper = shallow(
+ <SettingsEditionsNotif
+ editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }}
+ preventRestart={false}
+ setEditionStatus={jest.fn()}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should display a ready notification', () => {
+ const wrapper = shallow(
+ <SettingsEditionsNotif
+ editionStatus={{ installationStatus: 'AUTOMATIC_READY' }}
+ 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();
+});
+
+it('should display install errors', () => {
+ const wrapper = shallow(
+ <SettingsEditionsNotif
+ editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS', installError: 'Foo error' }}
+ preventRestart={false}
+ setEditionStatus={jest.fn()}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should allow to dismiss install errors', async () => {
+ const setEditionStatus = jest.fn();
+ const wrapper = mount(
+ <SettingsEditionsNotif
+ editionStatus={{ installationStatus: 'NONE', installError: 'Foo error' }}
+ preventRestart={false}
+ setEditionStatus={setEditionStatus}
+ />
+ );
+ click(wrapper.find('a'));
+ expect(dismissMsg).toHaveBeenCalled();
+ await new Promise(setImmediate);
+ 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();
+});
--- /dev/null
+// 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>
+`;
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';
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[];
export default class App extends React.PureComponent<Props, State> {
mounted: boolean;
- timer?: NodeJS.Timer;
static contextTypes = {
router: PropTypes.object.isRequired
constructor(props: Props) {
super(props);
this.state = {
- editionsReadOnly: false,
- loadingEditions: true,
loadingPlugins: true,
pending: {
installing: [],
componentDidMount() {
this.mounted = true;
- this.fetchEditions();
this.fetchPendingPlugins();
- this.fetchEditionStatus();
this.fetchQueryPlugins();
}
() => {}
);
- 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 });
};
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;
<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} />
)}
<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}
*/
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);
export interface Props {
editions?: Edition[];
editionStatus?: EditionStatus;
- editionsUrl: string;
loading: boolean;
readOnly: boolean;
- sonarqubeVersion: string;
updateCenterActive: boolean;
updateEditionStatus: (editionStatus: EditionStatus) => void;
}
<EditionBoxes
loading={false}
editionStatus={DEFAULT_STATUS}
- editionsUrl=""
readOnly={false}
- sonarqubeVersion="6.7.5"
updateCenterActive={true}
updateEditionStatus={jest.fn()}
{...props}
+++ /dev/null
-/*
- * 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 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';
-
-interface Props {
- editions?: Edition[];
- editionStatus: EditionStatus;
- readOnly: boolean;
- updateEditionStatus: (editionStatus: EditionStatus) => void;
-}
-
-interface State {
- openRestart: boolean;
-}
-
-export default class EditionsStatusNotif 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();
- dismissErrorMessage().then(
- () =>
- this.props.updateEditionStatus({ ...this.props.editionStatus, installError: undefined }),
- () => {}
- );
- };
-
- renderRestartMsg(edition?: Edition) {
- const { editionStatus, readOnly } = this.props;
- return (
- <div className="alert alert-success">
- <span>
- {edition ? (
- translateWithParameters(
- 'marketplace.status_x.' + editionStatus.installationStatus,
- edition.name
- )
- ) : (
- translate('marketplace.status', editionStatus.installationStatus)
- )}
- </span>
- {!readOnly && (
- <button className="js-restart spacer-left" onClick={this.handleOpenRestart}>
- {translate('marketplace.restart')}
- </button>
- )}
- {!readOnly && this.state.openRestart && <RestartForm onClose={this.hanleCloseRestart} />}
- </div>
- );
- }
-
- renderManualMsg(edition?: Edition) {
- const { editionStatus } = this.props;
- return (
- <div className="alert alert-danger">
- {edition ? (
- translateWithParameters(
- 'marketplace.status_x.' + editionStatus.installationStatus,
- edition.name
- )
- ) : (
- 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>
- )}
- <a
- href="https://redirect.sonarsource.com/doc/how-to-install-an-edition.html"
- target="_blank">
- {translate('marketplace.how_to_install')}
- </a>
- </p>
- <a className="little-spacer-left" href="https://www.sonarsource.com" target="_blank">
- {translate('marketplace.how_to_install')}
- </a>
- </div>
- );
- }
-
- renderStatusAlert() {
- const { editionStatus } = this.props;
- const { installationStatus, nextEditionKey } = editionStatus;
- const nextEdition =
- this.props.editions && this.props.editions.find(edition => edition.key === nextEditionKey);
-
- switch (installationStatus) {
- case 'AUTOMATIC_IN_PROGRESS':
- return (
- <div className="alert alert-info">
- <i className="spinner spacer-right text-bottom" />
- <span>{translate('marketplace.status.AUTOMATIC_IN_PROGRESS')}</span>
- </div>
- );
- case 'AUTOMATIC_READY':
- case 'UNINSTALL_IN_PROGRESS':
- return this.renderRestartMsg(nextEdition);
- case 'MANUAL_IN_PROGRESS':
- return this.renderManualMsg(nextEdition);
- }
- return null;
- }
-
- 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>
- );
- }
-}
+++ /dev/null
-/*
- * 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 { click } from '../../../../helpers/testUtils';
-import EditionsStatusNotif from '../EditionsStatusNotif';
-
-jest.mock('../../../../api/marketplace', () => ({
- dismissErrorMessage: jest.fn(() => Promise.resolve())
-}));
-
-const dismissMsg = require('../../../../api/marketplace').dismissErrorMessage as jest.Mock<any>;
-
-beforeEach(() => {
- dismissMsg.mockClear();
-});
-
-it('should display an in progress notif', () => {
- const wrapper = shallow(
- <EditionsStatusNotif
- editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }}
- readOnly={false}
- updateEditionStatus={jest.fn()}
- />
- );
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should display a ready notification', () => {
- const wrapper = shallow(
- <EditionsStatusNotif
- editionStatus={{ installationStatus: 'AUTOMATIC_READY' }}
- readOnly={false}
- updateEditionStatus={jest.fn()}
- />
- );
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should display install errors', () => {
- const wrapper = shallow(
- <EditionsStatusNotif
- editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS', installError: 'Foo error' }}
- readOnly={false}
- updateEditionStatus={jest.fn()}
- />
- );
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should allow to dismiss install errors', async () => {
- const updateEditionStatus = jest.fn();
- const wrapper = shallow(
- <EditionsStatusNotif
- editionStatus={{ installationStatus: 'NONE', installError: 'Foo error' }}
- readOnly={false}
- updateEditionStatus={updateEditionStatus}
- />
- );
- click(wrapper.find('a'));
- expect(dismissMsg).toHaveBeenCalled();
- await new Promise(setImmediate);
- expect(updateEditionStatus).toHaveBeenCalledWith({
- installationStatus: 'NONE',
- installError: undefined
- });
-});
+++ /dev/null
-// 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>
-`;
* 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 {
});
}
-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'])
border-right: none;
padding: 6px 0;
}
+
+.navbar-notif-cancelable {
+ display: flex;
+ justify-content: space-between;
+}
*/
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>
);
}
--- /dev/null
+/*
+ * 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))
+ );
+};
--- /dev/null
+/*
+ * 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;
--- /dev/null
+/*
+ * 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;
+}
*/
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';
globalMessages,
favorites,
languages,
+ marketplace,
metrics,
notifications,
organizations,
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);
.alert-emphasis-variant(#3c763d, #dff0d8, #d6e9c6);
}
-.alert-cancel {
- display: flex;
- justify-content: space-between;
-}
-
.page-notifs .alert {
padding: 8px 10px;
}
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?