]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9936 Make the edition notifications global again
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Tue, 24 Oct 2017 07:41:22 +0000 (09:41 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 25 Oct 2017 12:28:47 +0000 (14:28 +0200)
22 files changed:
server/sonar-web/src/main/js/app/components/AdminContainer.tsx
server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotifContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/App.tsx
server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx
server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx
server/sonar-web/src/main/js/apps/marketplace/components/EditionsStatusNotif.tsx [deleted file]
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionsStatusNotif-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionsStatusNotif-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/marketplace/utils.ts
server/sonar-web/src/main/js/components/nav/NavBar.css
server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx
server/sonar-web/src/main/js/store/marketplace/actions.ts [new file with mode: 0644]
server/sonar-web/src/main/js/store/marketplace/reducer.ts [new file with mode: 0644]
server/sonar-web/src/main/js/store/marketplace/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/store/rootReducer.js
server/sonar-web/src/main/less/components/alerts.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 12f2f691d8eb4bc17c77ffb72e9699f673e96eb3..bf12d09e0a12b74ac46aea80c91885429d524e8b 100644 (file)
@@ -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/app/components/nav/settings/SettingsEditionsNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx
new file mode 100644 (file)
index 0000000..527b847
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * 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();
+  }
+}
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 (file)
index 0000000..6bac95a
--- /dev/null
@@ -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);
index 49bb352fb63b30f20d04ca14a5c985ad34a5db58..4a983b4feed47fd0e7858adb679ce8f378d2dc50 100644 (file)
@@ -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/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 (file)
index 0000000..3614b9b
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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();
+});
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 (file)
index 0000000..51ab671
--- /dev/null
@@ -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>
+`;
index c57f3de78fdda17432bd4a2b9be691607bbea306..4d6f63fd28544de814ff64b92e799ef74606b33d 100644 (file)
@@ -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}
index 90fac954650295d7b09edc4f4967f63088e496a7..6d2b46b010d29273f757af8d3a92a26c0edbdace 100644 (file)
  */
 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);
index e9b7fa6f24653c280b608bc61d18b49cc5f1001c..30370c1d3b2410af9c06ec8a6b952e071e8da063 100644 (file)
@@ -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;
 }
index eeff50797ce61fd61906f691305d23414662e1bd..c0347bf9ad496a22c92f0b121e8ce7c97da35437 100644 (file)
@@ -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/EditionsStatusNotif.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/EditionsStatusNotif.tsx
deleted file mode 100644 (file)
index f21e8a3..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionsStatusNotif-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionsStatusNotif-test.tsx
deleted file mode 100644 (file)
index 4275351..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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
-  });
-});
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 (file)
index a76a31c..0000000
+++ /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>
-`;
index 31835b2ec56810c908b6446f766d63f04d3a3f28..780fafb3af81126ba8404d7c03731bd63cfc009c 100644 (file)
@@ -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'])
index fb9393dd02e9a7a2664823e6d522d086c5397b53..0a24b4232ad18537f569b1dc7a58d3c3f50e998d 100644 (file)
@@ -28,3 +28,8 @@
   border-right: none;
   padding: 6px 0;
 }
+
+.navbar-notif-cancelable {
+  display: flex;
+  justify-content: space-between;
+}
index 9004c1b7fe9b27a456cefeecd0f574123876b339..c276e20f62fd9dc59f4aa89572f5c86faf8bf232 100644 (file)
  */
 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 (file)
index 0000000..92e18cd
--- /dev/null
@@ -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 (file)
index 0000000..2d48eba
--- /dev/null
@@ -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 (file)
index 0000000..51c22d7
--- /dev/null
@@ -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;
+}
index 9edca6126da36dcebac40354fb525b8bab263eb3..9897df6974af2f554ddfeba3ddef181823452993 100644 (file)
@@ -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);
index 1cc1c516fe2611eb167b51b44ca6a3c075072daf..3fbc9952960b6a5d65cf6bca31c861baab4378bd 100644 (file)
   .alert-emphasis-variant(#3c763d, #dff0d8, #d6e9c6);
 }
 
-.alert-cancel {
-  display: flex;
-  justify-content: space-between;
-}
-
 .page-notifs .alert {
   padding: 8px 10px;
 }
index 30f40064b7801fcded300a3a7b7de6e5526e3366..3b7da354e469c1f7792cb1a3e0d739eb02fac33b 100644 (file)
@@ -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?