]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10133 Full-width banner to prompt restart after plugin change (#102)
authorPascal Mugnier <pascal.mugnier@sonarsource.com>
Tue, 10 Apr 2018 11:47:42 +0000 (13:47 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 10 Apr 2018 18:20:54 +0000 (20:20 +0200)
20 files changed:
server/sonar-web/src/main/js/api/plugins.ts
server/sonar-web/src/main/js/app/components/AdminContainer.tsx
server/sonar-web/src/main/js/app/components/nav/settings/PendingPluginsActionNotif.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/PendingPluginsActionNotif-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/PendingPluginsActionNotif-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
server/sonar-web/src/main/js/app/styles/components/alerts.css
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/PendingActions.tsx [deleted file]
server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx
server/sonar-web/src/main/js/store/marketplace/actions.ts
server/sonar-web/src/main/js/store/marketplace/reducer.ts
server/sonar-web/src/main/js/store/rootReducer.js

index 8d98b04de20ab827ec71ca57c815a7fd4bba0cef..faf104eba0be9824ab27d8817939cceaaee69c8a 100644 (file)
@@ -49,6 +49,12 @@ export interface Update {
   previousUpdates?: Update[];
 }
 
+export interface PluginPendingResult {
+  installing: PluginPending[];
+  updating: PluginPending[];
+  removing: PluginPending[];
+}
+
 export interface PluginAvailable extends Plugin {
   release: Release;
   update: Update;
@@ -74,11 +80,7 @@ export function getAvailablePlugins(): Promise<{
   return getJSON('/api/plugins/available').catch(throwGlobalError);
 }
 
-export function getPendingPlugins(): Promise<{
-  installing: PluginPending[];
-  updating: PluginPending[];
-  removing: PluginPending[];
-}> {
+export function getPendingPlugins(): Promise<PluginPendingResult> {
   return getJSON('/api/plugins/pending').catch(throwGlobalError);
 }
 
index 3f797a3419f4a48a099a2c6daefc716e5a04d4ba..173efed4d1dd4270830cd7c9b90f3f1e0bf3fbe6 100644 (file)
@@ -25,14 +25,20 @@ import SettingsNav from './nav/settings/SettingsNav';
 import {
   getAppState,
   getGlobalSettingValue,
-  getMarketplaceEditionStatus
+  getMarketplaceEditionStatus,
+  getMarketplacePendingPlugins
 } 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 {
+  fetchEditions,
+  setEditionStatus,
+  fetchPendingPlugins
+} from '../../store/marketplace/actions';
 import { translate } from '../../helpers/l10n';
 import { Extension } from '../types';
+import { PluginPendingResult } from '../../api/plugins';
 
 interface Props {
   appState: {
@@ -43,7 +49,9 @@ interface Props {
   editionsUrl: string;
   editionStatus?: EditionStatus;
   fetchEditions: (url: string, version: string) => void;
+  fetchPendingPlugins: () => void;
   location: {};
+  pendingPlugins: PluginPendingResult;
   setAdminPages: (adminPages: Extension[]) => void;
   setEditionStatus: (editionStatus: EditionStatus) => void;
 }
@@ -88,8 +96,10 @@ class AdminContainer extends React.PureComponent<Props> {
         <SettingsNav
           editionStatus={this.props.editionStatus}
           extensions={adminPages}
+          fetchPendingPlugins={this.props.fetchPendingPlugins}
           location={this.props.location}
           organizationsEnabled={organizationsEnabled}
+          pendingPlugins={this.props.pendingPlugins}
         />
         {this.props.children}
       </div>
@@ -100,9 +110,10 @@ class AdminContainer extends React.PureComponent<Props> {
 const mapStateToProps = (state: any) => ({
   appState: getAppState(state),
   editionStatus: getMarketplaceEditionStatus(state),
-  editionsUrl: (getGlobalSettingValue(state, 'sonar.editions.jsonUrl') || {}).value
+  editionsUrl: (getGlobalSettingValue(state, 'sonar.editions.jsonUrl') || {}).value,
+  pendingPlugins: getMarketplacePendingPlugins(state)
 });
 
-const mapDispatchToProps = { setAdminPages, setEditionStatus, fetchEditions };
+const mapDispatchToProps = { setAdminPages, setEditionStatus, fetchEditions, fetchPendingPlugins };
 
 export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer as any);
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/PendingPluginsActionNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/PendingPluginsActionNotif.tsx
new file mode 100644 (file)
index 0000000..e1e3906
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import RestartForm from '../../../../components/common/RestartForm';
+import { cancelPendingPlugins, PluginPendingResult } from '../../../../api/plugins';
+import { Button } from '../../../../components/ui/buttons';
+import { translate } from '../../../../helpers/l10n';
+import NavBarNotif from '../../../../components/nav/NavBarNotif';
+
+interface Props {
+  pending: PluginPendingResult;
+  refreshPending: () => void;
+}
+
+interface State {
+  openRestart: boolean;
+}
+
+export default class PendingPluginsActionNotif extends React.PureComponent<Props, State> {
+  state: State = { openRestart: false };
+
+  handleOpenRestart = () => {
+    this.setState({ openRestart: true });
+  };
+
+  hanleCloseRestart = () => {
+    this.setState({ openRestart: false });
+  };
+
+  handleRevert = () => {
+    cancelPendingPlugins().then(this.props.refreshPending, () => {});
+  };
+
+  render() {
+    const { installing, updating, removing } = this.props.pending;
+    const hasPendingActions = installing.length || updating.length || removing.length;
+    if (!hasPendingActions) {
+      return null;
+    }
+
+    return (
+      <NavBarNotif className="alert alert-info">
+        <span className="little-spacer-right">
+          {translate('marketplace.sonarqube_needs_to_be_restarted_to')}
+        </span>
+        {[
+          { length: installing.length, msg: 'marketplace.install_x_plugins' },
+          { length: updating.length, msg: 'marketplace.update_x_plugins' },
+          { length: removing.length, msg: 'marketplace.uninstall_x_plugins' }
+        ]
+          .filter(({ length }) => length > 0)
+          .map(({ length, msg }, idx) => (
+            <span key={msg}>
+              {idx > 0 && '; '}
+              <FormattedMessage
+                defaultMessage={translate(msg)}
+                id={msg}
+                values={{ nb: <strong>{length}</strong> }}
+              />
+            </span>
+          ))}
+        <Button className="spacer-left js-restart" onClick={this.handleOpenRestart}>
+          {translate('marketplace.restart')}
+        </Button>
+        <Button className="spacer-left js-cancel-all button-red" onClick={this.handleRevert}>
+          {translate('marketplace.revert')}
+        </Button>
+        {this.state.openRestart && <RestartForm onClose={this.hanleCloseRestart} />}
+      </NavBarNotif>
+    );
+  }
+}
index 3c8850a95efc02fc2fe65bd94f3550263a343387..c4f1af5e32b6a385611a257dcbacd12c454221a8 100644 (file)
@@ -52,7 +52,7 @@ export default class SettingsEditionsNotif extends React.PureComponent<Props, St
     const { editionStatus } = this.props;
     return (
       <NavBarNotif className="alert alert-info">
-        <i className="spinner spacer-right text-bottom" />
+        <i className="spinner spacer-right" />
         <span>
           {edition
             ? translateWithParameters(
index 3064c54212e7867404e37bbe79f239ec48cec6c5..8379d1822bbe680fb14fa0a2da2f355066ec1ba5 100644 (file)
@@ -21,6 +21,7 @@ import * as React from 'react';
 import * as classNames from 'classnames';
 import { IndexLink, Link } from 'react-router';
 import SettingsEditionsNotifContainer from './SettingsEditionsNotifContainer';
+import PendingPluginsActionNotif from './PendingPluginsActionNotif';
 import * as theme from '../../../../app/theme';
 import ContextNavBar from '../../../../components/nav/ContextNavBar';
 import NavBarTabs from '../../../../components/nav/NavBarTabs';
@@ -28,12 +29,15 @@ import { EditionStatus } from '../../../../api/marketplace';
 import { Extension } from '../../../types';
 import { translate } from '../../../../helpers/l10n';
 import Dropdown from '../../../../components/controls/Dropdown';
+import { PluginPendingResult } from '../../../../api/plugins';
 
 interface Props {
   editionStatus?: EditionStatus;
   extensions: Extension[];
+  fetchPendingPlugins: () => void;
   location: {};
   organizationsEnabled: boolean;
+  pendingPlugins: PluginPendingResult;
 }
 
 export default class SettingsNav extends React.PureComponent<Props> {
@@ -216,22 +220,42 @@ export default class SettingsNav extends React.PureComponent<Props> {
   }
 
   render() {
-    const { editionStatus, extensions } = this.props;
+    const { editionStatus, extensions, pendingPlugins } = this.props;
     const hasSupportExtension = extensions.find(extension => extension.key === 'license/support');
 
-    let notifComponent;
+    const notifComponents = [];
     if (
       editionStatus &&
       (editionStatus.installError || editionStatus.installationStatus !== 'NONE')
     ) {
-      notifComponent = <SettingsEditionsNotifContainer editionStatus={editionStatus} />;
+      notifComponents.push(<SettingsEditionsNotifContainer editionStatus={editionStatus} />);
     }
 
+    if (
+      pendingPlugins.installing.length > 0 ||
+      pendingPlugins.removing.length > 0 ||
+      pendingPlugins.updating.length > 0
+    ) {
+      notifComponents.push(
+        <PendingPluginsActionNotif
+          pending={pendingPlugins}
+          refreshPending={this.props.fetchPendingPlugins}
+        />
+      );
+    }
+
+    const notifContainer =
+      notifComponents.length > 0 ? (
+        <div className="alert-container">
+          {notifComponents.map((element, index) => <div key={index}>{element}</div>)}
+        </div>
+      ) : null;
+
     return (
       <ContextNavBar
-        height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw}
+        height={theme.contextNavHeightRaw + 38 * notifComponents.length}
         id="context-navigation"
-        notif={notifComponent}>
+        notif={notifContainer}>
         <header className="navbar-context-header">
           <h1>{translate('layout.settings')}</h1>
         </header>
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/PendingPluginsActionNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/PendingPluginsActionNotif-test.tsx
new file mode 100644 (file)
index 0000000..b531164
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+/* eslint-disable import/order */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { click } from '../../../../../helpers/testUtils';
+import PendingPluginsActionNotif from '../PendingPluginsActionNotif';
+
+jest.mock('../../../../../api/plugins', () => ({
+  cancelPendingPlugins: jest.fn(() => Promise.resolve())
+}));
+
+const cancelPendingPlugins = require('../../../../../api/plugins')
+  .cancelPendingPlugins as jest.Mock<any>;
+
+beforeEach(() => {
+  cancelPendingPlugins.mockClear();
+});
+
+it('should display pending actions', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should not display anything', () => {
+  expect(getWrapper({ pending: { installing: [], updating: [], removing: [] } }).type()).toBeNull();
+});
+
+it('should open the restart form', () => {
+  const wrapper = getWrapper();
+  click(wrapper.find('.js-restart'));
+  expect(wrapper.find('RestartForm').exists()).toBeTruthy();
+});
+
+it('should cancel all pending and refresh them', async () => {
+  const refreshPending = jest.fn();
+  const wrapper = getWrapper({ refreshPending });
+  click(wrapper.find('.js-cancel-all'));
+  expect(cancelPendingPlugins).toHaveBeenCalled();
+  await new Promise(setImmediate);
+
+  expect(refreshPending).toHaveBeenCalled();
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <PendingPluginsActionNotif
+      pending={{
+        installing: [
+          {
+            key: 'foo',
+            name: 'Foo',
+            description: 'foo description',
+            version: 'fooversion',
+            implementationBuild: 'foobuild'
+          },
+          {
+            key: 'bar',
+            name: 'Bar',
+            description: 'bar description',
+            version: 'barversion',
+            implementationBuild: 'barbuild'
+          }
+        ],
+        updating: [],
+        removing: [
+          {
+            key: 'baz',
+            name: 'Baz',
+            description: 'baz description',
+            version: 'bazversion',
+            implementationBuild: 'bazbuild'
+          }
+        ]
+      }}
+      refreshPending={() => {}}
+      {...props}
+    />
+  );
+}
index e589b8794942fbc1bde0ea738c0b73cdbc813f15..ae3f5e900aff2b6dd36be030cb212763fd1b53b6 100644 (file)
@@ -24,7 +24,13 @@ import SettingsNav from '../SettingsNav';
 it('should work with extensions', () => {
   const extensions = [{ key: 'foo', name: 'Foo' }];
   const wrapper = shallow(
-    <SettingsNav extensions={extensions} location={{}} organizationsEnabled={false} />
+    <SettingsNav
+      extensions={extensions}
+      fetchPendingPlugins={() => {}}
+      location={{}}
+      organizationsEnabled={false}
+      pendingPlugins={{ installing: [], removing: [], updating: [] }}
+    />
   );
   expect(wrapper).toMatchSnapshot();
   expect(wrapper.find('Dropdown').map(x => x.dive())).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/PendingPluginsActionNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/PendingPluginsActionNotif-test.tsx.snap
new file mode 100644 (file)
index 0000000..f7c1f27
--- /dev/null
@@ -0,0 +1,56 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display pending actions 1`] = `
+<NavBarNotif
+  className="alert alert-info"
+>
+  <span
+    className="little-spacer-right"
+  >
+    marketplace.sonarqube_needs_to_be_restarted_to
+  </span>
+  <span
+    key="marketplace.install_x_plugins"
+  >
+    <FormattedMessage
+      defaultMessage="marketplace.install_x_plugins"
+      id="marketplace.install_x_plugins"
+      values={
+        Object {
+          "nb": <strong>
+            2
+          </strong>,
+        }
+      }
+    />
+  </span>
+  <span
+    key="marketplace.uninstall_x_plugins"
+  >
+    ; 
+    <FormattedMessage
+      defaultMessage="marketplace.uninstall_x_plugins"
+      id="marketplace.uninstall_x_plugins"
+      values={
+        Object {
+          "nb": <strong>
+            1
+          </strong>,
+        }
+      }
+    />
+  </span>
+  <Button
+    className="spacer-left js-restart"
+    onClick={[Function]}
+  >
+    marketplace.restart
+  </Button>
+  <Button
+    className="spacer-left js-cancel-all button-red"
+    onClick={[Function]}
+  >
+    marketplace.revert
+  </Button>
+</NavBarNotif>
+`;
index f843a2d29bab95a61778d40e0ec30ea71351de7f..051f00fce945db8437cfed7efc9f34cb64088a13 100644 (file)
@@ -44,7 +44,7 @@ exports[`should display an in progress notif 1`] = `
   className="alert alert-info"
 >
   <i
-    className="spinner spacer-right text-bottom"
+    className="spinner spacer-right"
   />
   <span>
     marketplace.edition_status.AUTOMATIC_IN_PROGRESS
index 439e362124e82430b909e46d8a390d412da10914..d7495bbbfc32649fffb829c17d0409cda9e90334 100644 (file)
@@ -4,6 +4,7 @@ exports[`should work with extensions 1`] = `
 <ContextNavBar
   height={72}
   id="context-navigation"
+  notif={null}
 >
   <header
     className="navbar-context-header"
index b405e80aa34b1570a6f2e27922f4b4580bffaa23..aa8e515e341e94e523fc2e539c283236559b8c2d 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+.alert-container .alert {
+  margin-bottom: 0px;
+  padding: 0 8px;
+  line-height: 38px;
+}
+
 .alert {
   display: block;
   margin-bottom: 8px;
index 2dcf58ab2e0d613ac64937c71c29c3ac4b42cbc5..9446bfe471c7d87703d2815202426c1ef192810f 100644 (file)
@@ -24,17 +24,15 @@ import Helmet from 'react-helmet';
 import Header from './Header';
 import EditionBoxes from './EditionBoxes';
 import Footer from './Footer';
-import PendingActions from './PendingActions';
 import PluginsList from './PluginsList';
 import Search from './Search';
 import { filterPlugins, parseQuery, Query, serializeQuery } from './utils';
 import {
   getAvailablePlugins,
   getInstalledPluginsWithUpdates,
-  getPendingPlugins,
   getPluginUpdates,
   Plugin,
-  PluginPending,
+  PluginPendingResult,
   getInstalledPlugins
 } from '../../api/plugins';
 import { Edition, EditionStatus } from '../../api/marketplace';
@@ -46,20 +44,17 @@ export interface Props {
   editions?: Edition[];
   editionsReadOnly: boolean;
   editionStatus?: EditionStatus;
+  fetchPendingPlugins: () => void;
   loadingEditions: boolean;
   location: { pathname: string; query: RawQuery };
+  pendingPlugins: PluginPendingResult;
   standaloneMode: boolean;
-  updateCenterActive: boolean;
   setEditionStatus: (editionStatus: EditionStatus) => void;
+  updateCenterActive: boolean;
 }
 
 interface State {
   loadingPlugins: boolean;
-  pending: {
-    installing: PluginPending[];
-    updating: PluginPending[];
-    removing: PluginPending[];
-  };
   plugins: Plugin[];
 }
 
@@ -74,18 +69,13 @@ export default class App extends React.PureComponent<Props, State> {
     super(props);
     this.state = {
       loadingPlugins: true,
-      pending: {
-        installing: [],
-        updating: [],
-        removing: []
-      },
       plugins: []
     };
   }
 
   componentDidMount() {
     this.mounted = true;
-    this.fetchPendingPlugins();
+    this.props.fetchPendingPlugins();
     this.fetchQueryPlugins();
   }
 
@@ -127,16 +117,6 @@ export default class App extends React.PureComponent<Props, State> {
     );
   };
 
-  fetchPendingPlugins = () =>
-    getPendingPlugins().then(
-      pending => {
-        if (this.mounted) {
-          this.setState({ pending });
-        }
-      },
-      () => {}
-    );
-
   updateQuery = (newQuery: Partial<Query>) => {
     const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
     this.context.router.push({ pathname: this.props.location.pathname, query });
@@ -149,19 +129,14 @@ export default class App extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { editions, editionStatus, standaloneMode } = this.props;
-    const { loadingPlugins, plugins, pending } = this.state;
+    const { editions, editionStatus, standaloneMode, pendingPlugins } = this.props;
+    const { loadingPlugins, plugins } = this.state;
     const query = parseQuery(this.props.location.query);
     const filteredPlugins = query.search ? filterPlugins(plugins, query.search) : plugins;
 
     return (
       <div className="page page-limited" id="marketplace-page">
         <Helmet title={translate('marketplace.page')} />
-        <div className="page-notifs">
-          {standaloneMode && (
-            <PendingActions pending={pending} refreshPending={this.fetchPendingPlugins} />
-          )}
-        </div>
         <Header />
         <EditionBoxes
           canInstall={standaloneMode && !this.props.editionsReadOnly}
@@ -180,10 +155,10 @@ export default class App extends React.PureComponent<Props, State> {
         {loadingPlugins && <i className="spinner" />}
         {!loadingPlugins && (
           <PluginsList
-            pending={pending}
+            pending={pendingPlugins}
             plugins={filteredPlugins}
             readOnly={!standaloneMode}
-            refreshPending={this.fetchPendingPlugins}
+            refreshPending={this.props.fetchPendingPlugins}
           />
         )}
         {!loadingPlugins && <Footer total={filteredPlugins.length} />}
index 4f7676767ba615d13848da76ecf748fb6bd1a691..73611432c0b0e67106be3005f0bb6ad2a259761c 100644 (file)
@@ -24,11 +24,13 @@ import {
   getGlobalSettingValue,
   getMarketplaceState,
   getMarketplaceEditions,
-  getMarketplaceEditionStatus
+  getMarketplaceEditionStatus,
+  getMarketplacePendingPlugins
 } from '../../store/rootReducer';
 import { Edition, EditionStatus } from '../../api/marketplace';
-import { setEditionStatus } from '../../store/marketplace/actions';
+import { setEditionStatus, fetchPendingPlugins } from '../../store/marketplace/actions';
 import { RawQuery } from '../../helpers/query';
+import { PluginPendingResult } from '../../api/plugins';
 
 interface OwnProps {
   location: { pathname: string; query: RawQuery };
@@ -39,12 +41,14 @@ interface StateToProps {
   editionsReadOnly: boolean;
   editionStatus?: EditionStatus;
   loadingEditions: boolean;
+  pendingPlugins: PluginPendingResult;
   standaloneMode: boolean;
   updateCenterActive: boolean;
 }
 
 interface DispatchToProps {
   setEditionStatus: (editionStatus: EditionStatus) => void;
+  fetchPendingPlugins: () => void;
 }
 
 const mapStateToProps = (state: any) => ({
@@ -52,12 +56,13 @@ const mapStateToProps = (state: any) => ({
   editionsReadOnly: getMarketplaceState(state).readOnly,
   editionStatus: getMarketplaceEditionStatus(state),
   loadingEditions: getMarketplaceState(state).loading,
+  pendingPlugins: getMarketplacePendingPlugins(state),
   standaloneMode: getAppState(state).standalone,
   updateCenterActive:
     (getGlobalSettingValue(state, 'sonar.updatecenter.activate') || {}).value === 'true'
 });
 
-const mapDispatchToProps = { setEditionStatus };
+const mapDispatchToProps = { setEditionStatus, fetchPendingPlugins };
 
 export default connect<StateToProps, DispatchToProps, OwnProps>(
   mapStateToProps,
diff --git a/server/sonar-web/src/main/js/apps/marketplace/PendingActions.tsx b/server/sonar-web/src/main/js/apps/marketplace/PendingActions.tsx
deleted file mode 100644 (file)
index d392d5e..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import RestartForm from '../../components/common/RestartForm';
-import { cancelPendingPlugins, PluginPending } from '../../api/plugins';
-import { Button } from '../../components/ui/buttons';
-import { translate } from '../../helpers/l10n';
-
-interface Props {
-  pending: {
-    installing: PluginPending[];
-    updating: PluginPending[];
-    removing: PluginPending[];
-  };
-  refreshPending: () => void;
-}
-
-interface State {
-  openRestart: boolean;
-}
-
-export default class PendingActions extends React.PureComponent<Props, State> {
-  state: State = { openRestart: false };
-
-  handleOpenRestart = () => {
-    this.setState({ openRestart: true });
-  };
-
-  hanleCloseRestart = () => {
-    this.setState({ openRestart: false });
-  };
-
-  handleRevert = () => {
-    cancelPendingPlugins().then(this.props.refreshPending, () => {});
-  };
-
-  render() {
-    const { installing, updating, removing } = this.props.pending;
-    const hasPendingActions = installing.length || updating.length || removing.length;
-    if (!hasPendingActions) {
-      return null;
-    }
-
-    return (
-      <div className="js-pending alert alert-warning">
-        <div className="display-inline-block">
-          <p>{translate('marketplace.sonarqube_needs_to_be_restarted_to')}</p>
-          <ul className="list-styled spacer-top">
-            {installing.length > 0 && (
-              <li>
-                <FormattedMessage
-                  defaultMessage={translate('marketplace.install_x_plugins')}
-                  id="marketplace.install_x_plugins"
-                  values={{ nb: <strong>{installing.length}</strong> }}
-                />
-              </li>
-            )}
-            {updating.length > 0 && (
-              <li>
-                <FormattedMessage
-                  defaultMessage={translate('marketplace.update_x_plugins')}
-                  id="marketplace.update_x_plugins"
-                  values={{ nb: <strong>{updating.length}</strong> }}
-                />
-              </li>
-            )}
-            {removing.length > 0 && (
-              <li>
-                <FormattedMessage
-                  defaultMessage={translate('marketplace.uninstall_x_plugins')}
-                  id="marketplace.uninstall_x_plugins"
-                  values={{ nb: <strong>{removing.length}</strong> }}
-                />
-              </li>
-            )}
-          </ul>
-        </div>
-        <div className="pull-right">
-          <Button className="js-restart little-spacer-right" onClick={this.handleOpenRestart}>
-            {translate('marketplace.restart')}
-          </Button>
-          <Button className="js-cancel-all button-red" onClick={this.handleRevert}>
-            {translate('marketplace.revert')}
-          </Button>
-        </div>
-        {this.state.openRestart && <RestartForm onClose={this.hanleCloseRestart} />}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx
deleted file mode 100644 (file)
index ff02327..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-/* eslint-disable import/order */
-import * as React from 'react';
-import { shallow } from 'enzyme';
-import { click } from '../../../helpers/testUtils';
-import PendingActions from '../PendingActions';
-
-jest.mock('../../../api/plugins', () => ({
-  cancelPendingPlugins: jest.fn(() => Promise.resolve())
-}));
-
-const cancelPendingPlugins = require('../../../api/plugins').cancelPendingPlugins as jest.Mock<any>;
-
-beforeEach(() => {
-  cancelPendingPlugins.mockClear();
-});
-
-it('should display pending actions', () => {
-  expect(getWrapper()).toMatchSnapshot();
-});
-
-it('should not display anything', () => {
-  expect(getWrapper({ pending: { installing: [], updating: [], removing: [] } }).type()).toBeNull();
-});
-
-it('should open the restart form', () => {
-  const wrapper = getWrapper();
-  click(wrapper.find('.js-restart'));
-  expect(wrapper.find('RestartForm').exists()).toBeTruthy();
-});
-
-it('should cancel all pending and refresh them', async () => {
-  const refreshPending = jest.fn();
-  const wrapper = getWrapper({ refreshPending });
-  click(wrapper.find('.js-cancel-all'));
-  expect(cancelPendingPlugins).toHaveBeenCalled();
-  await new Promise(setImmediate);
-
-  expect(refreshPending).toHaveBeenCalled();
-});
-
-function getWrapper(props = {}) {
-  return shallow(
-    <PendingActions
-      pending={{
-        installing: [
-          {
-            key: 'foo',
-            name: 'Foo',
-            description: 'foo description',
-            version: 'fooversion',
-            implementationBuild: 'foobuild'
-          },
-          {
-            key: 'bar',
-            name: 'Bar',
-            description: 'bar description',
-            version: 'barversion',
-            implementationBuild: 'barbuild'
-          }
-        ],
-        updating: [],
-        removing: [
-          {
-            key: 'baz',
-            name: 'Baz',
-            description: 'baz description',
-            version: 'bazversion',
-            implementationBuild: 'bazbuild'
-          }
-        ]
-      }}
-      refreshPending={() => {}}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap
deleted file mode 100644 (file)
index c868859..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display pending actions 1`] = `
-<div
-  className="js-pending alert alert-warning"
->
-  <div
-    className="display-inline-block"
-  >
-    <p>
-      marketplace.sonarqube_needs_to_be_restarted_to
-    </p>
-    <ul
-      className="list-styled spacer-top"
-    >
-      <li>
-        <FormattedMessage
-          defaultMessage="marketplace.install_x_plugins"
-          id="marketplace.install_x_plugins"
-          values={
-            Object {
-              "nb": <strong>
-                2
-              </strong>,
-            }
-          }
-        />
-      </li>
-      <li>
-        <FormattedMessage
-          defaultMessage="marketplace.uninstall_x_plugins"
-          id="marketplace.uninstall_x_plugins"
-          values={
-            Object {
-              "nb": <strong>
-                1
-              </strong>,
-            }
-          }
-        />
-      </li>
-    </ul>
-  </div>
-  <div
-    className="pull-right"
-  >
-    <Button
-      className="js-restart little-spacer-right"
-      onClick={[Function]}
-    >
-      marketplace.restart
-    </Button>
-    <Button
-      className="js-cancel-all button-red"
-      onClick={[Function]}
-    >
-      marketplace.revert
-    </Button>
-  </div>
-</div>
-`;
index 63f5695de0081cc9d7f29d403143b5cdf105f9b8..835b51831e135cba958b62acc26685c6a5e2355c 100644 (file)
@@ -34,7 +34,10 @@ export default function NavBarNotif(props: Props) {
   return (
     <div className={classNames('navbar-notif', props.className)}>
       <div className="navbar-limited clearfix">
-        <div className={classNames({ 'navbar-notif-cancelable': !!props.onCancel })}>
+        <div
+          className={classNames('display-flex-center', {
+            'navbar-notif-cancelable': !!props.onCancel
+          })}>
           {props.children}
           {props.onCancel && <DeleteButton className="button-small" onClick={props.onCancel} />}
         </div>
index dfc5f8da83476d84405dbf6940faf08f78667ec4..894c74f23980d34cd748a3488e9c4eb6eeb76251 100644 (file)
 import { Dispatch } from 'react-redux';
 import { getEditionsForVersion, getEditionsForLastVersion } from './utils';
 import { Edition, EditionStatus, getEditionStatus, getEditionsList } from '../../api/marketplace';
+import { getPendingPlugins, PluginPendingResult } from '../../api/plugins';
 
 interface LoadEditionsAction {
   type: 'LOAD_EDITIONS';
   loading: boolean;
 }
 
+interface SetPendingPluginsAction {
+  type: 'SET_PENDING_PLUGINS';
+  pending: PluginPendingResult;
+}
+
 interface SetEditionsAction {
   type: 'SET_EDITIONS';
   editions: Edition[];
@@ -37,12 +43,20 @@ interface SetEditionStatusAction {
   status: EditionStatus;
 }
 
-export type Action = LoadEditionsAction | SetEditionsAction | SetEditionStatusAction;
+export type Action =
+  | LoadEditionsAction
+  | SetEditionsAction
+  | SetEditionStatusAction
+  | SetPendingPluginsAction;
 
 export function loadEditions(loading = true): LoadEditionsAction {
   return { type: 'LOAD_EDITIONS', loading };
 }
 
+export function setPendingPlugins(pending: PluginPendingResult): SetPendingPluginsAction {
+  return { type: 'SET_PENDING_PLUGINS', pending };
+}
+
 export function setEditions(editions: Edition[], readOnly?: boolean): SetEditionsAction {
   return { type: 'SET_EDITIONS', editions, readOnly: !!readOnly };
 }
@@ -62,6 +76,15 @@ export const setEditionStatus = (status: EditionStatus) => (dispatch: Dispatch<A
   }
 };
 
+export const fetchPendingPlugins = () => (dispatch: Dispatch<Action>) => {
+  getPendingPlugins().then(
+    pending => {
+      dispatch(setPendingPlugins(pending));
+    },
+    () => {}
+  );
+};
+
 export const fetchEditions = (url: string, version: string) => (dispatch: Dispatch<Action>) => {
   dispatch(loadEditions(true));
   getEditionsList(url).then(
index 2afdef1e678c5c133d337f68536def85bb73e558..11c5e28d7f543b2e46ae0ac802a901ccbdb95c65 100644 (file)
  */
 import { Action } from './actions';
 import { Edition, EditionStatus } from '../../api/marketplace';
+import { PluginPendingResult } from '../../api/plugins';
 
 interface State {
   editions?: Edition[];
   loading: boolean;
   status?: EditionStatus;
   readOnly: boolean;
+  pending: PluginPendingResult;
 }
 
 const defaultState: State = {
   loading: true,
-  readOnly: false
+  readOnly: false,
+  pending: { installing: [], removing: [], updating: [] }
 };
 
 export default function(state: State = defaultState, action: Action): State {
@@ -39,6 +42,12 @@ export default function(state: State = defaultState, action: Action): State {
   if (action.type === 'LOAD_EDITIONS') {
     return { ...state, loading: action.loading };
   }
+  if (action.type === 'SET_PENDING_PLUGINS') {
+    return {
+      ...state,
+      pending: action.pending
+    };
+  }
   if (action.type === 'SET_EDITION_STATUS') {
     const hasChanged = Object.keys(action.status).some(
       (key: keyof EditionStatus) => !state.status || state.status[key] !== action.status[key]
@@ -53,3 +62,4 @@ export default function(state: State = defaultState, action: Action): State {
 
 export const getEditions = (state: State) => state.editions;
 export const getEditionStatus = (state: State) => state.status;
+export const getPendingPlugins = (state: State) => state.pending;
index 1a8ca7d54e611f4dd1885fc6a0cfe9df1906f936..51ea1ad9971e8f68d7e4740485dd80aa4a0fe0ee 100644 (file)
@@ -82,6 +82,9 @@ export const getMarketplaceEditions = state => fromMarketplace.getEditions(state
 export const getMarketplaceEditionStatus = state =>
   fromMarketplace.getEditionStatus(state.marketplace);
 
+export const getMarketplacePendingPlugins = state =>
+  fromMarketplace.getPendingPlugins(state.marketplace);
+
 export const getMetrics = state => fromMetrics.getMetrics(state.metrics);
 
 export const getMetricByKey = (state, key) => fromMetrics.getMetricByKey(state.metrics, key);