From: Pascal Mugnier Date: Tue, 10 Apr 2018 11:47:42 +0000 (+0200) Subject: SONAR-10133 Full-width banner to prompt restart after plugin change (#102) X-Git-Tag: 7.5~1378 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=50766c5a770d31733a4f5f0f29a3cbf663473d77;p=sonarqube.git SONAR-10133 Full-width banner to prompt restart after plugin change (#102) --- diff --git a/server/sonar-web/src/main/js/api/plugins.ts b/server/sonar-web/src/main/js/api/plugins.ts index 8d98b04de20..faf104eba0b 100644 --- a/server/sonar-web/src/main/js/api/plugins.ts +++ b/server/sonar-web/src/main/js/api/plugins.ts @@ -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 { return getJSON('/api/plugins/pending').catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx index 3f797a3419f..173efed4d1d 100644 --- a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx @@ -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 { {this.props.children} @@ -100,9 +110,10 @@ class AdminContainer extends React.PureComponent { 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 index 00000000000..e1e3906fb0a --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/settings/PendingPluginsActionNotif.tsx @@ -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 { + 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 ( + + + {translate('marketplace.sonarqube_needs_to_be_restarted_to')} + + {[ + { 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) => ( + + {idx > 0 && '; '} + {length} }} + /> + + ))} + + + {this.state.openRestart && } + + ); + } +} 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 index 3c8850a95ef..c4f1af5e32b 100644 --- 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 @@ -52,7 +52,7 @@ export default class SettingsEditionsNotif extends React.PureComponent - + {edition ? translateWithParameters( diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx index 3064c54212e..8379d1822bb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx @@ -21,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 { @@ -216,22 +220,42 @@ export default class SettingsNav extends React.PureComponent { } 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 = ; + notifComponents.push(); } + if ( + pendingPlugins.installing.length > 0 || + pendingPlugins.removing.length > 0 || + pendingPlugins.updating.length > 0 + ) { + notifComponents.push( + + ); + } + + const notifContainer = + notifComponents.length > 0 ? ( +
+ {notifComponents.map((element, index) =>
{element}
)} +
+ ) : null; + return ( + notif={notifContainer}>

{translate('layout.settings')}

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 index 00000000000..b531164eeac --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/PendingPluginsActionNotif-test.tsx @@ -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; + +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( + {}} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx index e589b879494..ae3f5e900af 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx @@ -24,7 +24,13 @@ import SettingsNav from '../SettingsNav'; it('should work with extensions', () => { const extensions = [{ key: 'foo', name: 'Foo' }]; const wrapper = shallow( - + {}} + 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 index 00000000000..f7c1f279fa4 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/PendingPluginsActionNotif-test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display pending actions 1`] = ` + + + marketplace.sonarqube_needs_to_be_restarted_to + + + + 2 + , + } + } + /> + + + ; + + 1 + , + } + } + /> + + + + +`; 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 index f843a2d29ba..051f00fce94 100644 --- 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 @@ -44,7 +44,7 @@ exports[`should display an in progress notif 1`] = ` className="alert alert-info" > marketplace.edition_status.AUTOMATIC_IN_PROGRESS diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap index 439e362124e..d7495bbbfc3 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap @@ -4,6 +4,7 @@ exports[`should work with extensions 1`] = `
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 { 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 { ); }; - fetchPendingPlugins = () => - getPendingPlugins().then( - pending => { - if (this.mounted) { - this.setState({ pending }); - } - }, - () => {} - ); - updateQuery = (newQuery: Partial) => { 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 { }; 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 (
-
- {standaloneMode && ( - - )} -
{ {loadingPlugins && } {!loadingPlugins && ( )} {!loadingPlugins &&