diff options
author | Pascal Mugnier <pascal.mugnier@sonarsource.com> | 2018-04-10 13:47:42 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-04-10 20:20:54 +0200 |
commit | 50766c5a770d31733a4f5f0f29a3cbf663473d77 (patch) | |
tree | 6aa335d1afd44ba45e43e6ccc254f192ad69d4d0 /server | |
parent | f239ffffd3818fec2aa2d74b12875355324df73e (diff) | |
download | sonarqube-50766c5a770d31733a4f5f0f29a3cbf663473d77.tar.gz sonarqube-50766c5a770d31733a4f5f0f29a3cbf663473d77.zip |
SONAR-10133 Full-width banner to prompt restart after plugin change (#102)
Diffstat (limited to 'server')
19 files changed, 278 insertions, 231 deletions
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<PluginPendingResult> { 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<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 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<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> + ); + } +} 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<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( 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<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/apps/marketplace/__tests__/PendingActions-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/PendingPluginsActionNotif-test.tsx index ff023270fac..b531164eeac 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/PendingPluginsActionNotif-test.tsx @@ -20,14 +20,15 @@ /* eslint-disable import/order */ import * as React from 'react'; import { shallow } from 'enzyme'; -import { click } from '../../../helpers/testUtils'; -import PendingActions from '../PendingActions'; +import { click } from '../../../../../helpers/testUtils'; +import PendingPluginsActionNotif from '../PendingPluginsActionNotif'; -jest.mock('../../../api/plugins', () => ({ +jest.mock('../../../../../api/plugins', () => ({ cancelPendingPlugins: jest.fn(() => Promise.resolve()) })); -const cancelPendingPlugins = require('../../../api/plugins').cancelPendingPlugins as jest.Mock<any>; +const cancelPendingPlugins = require('../../../../../api/plugins') + .cancelPendingPlugins as jest.Mock<any>; beforeEach(() => { cancelPendingPlugins.mockClear(); @@ -59,7 +60,7 @@ it('should cancel all pending and refresh them', async () => { function getWrapper(props = {}) { return shallow( - <PendingActions + <PendingPluginsActionNotif pending={{ installing: [ { 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( - <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 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`] = ` +<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> +`; 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" > <i - className="spinner spacer-right text-bottom" + className="spinner spacer-right" /> <span> 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`] = ` <ContextNavBar height={72} id="context-navigation" + notif={null} > <header className="navbar-context-header" diff --git a/server/sonar-web/src/main/js/app/styles/components/alerts.css b/server/sonar-web/src/main/js/app/styles/components/alerts.css index b405e80aa34..aa8e515e341 100644 --- a/server/sonar-web/src/main/js/app/styles/components/alerts.css +++ b/server/sonar-web/src/main/js/app/styles/components/alerts.css @@ -17,6 +17,12 @@ * 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; diff --git a/server/sonar-web/src/main/js/apps/marketplace/App.tsx b/server/sonar-web/src/main/js/apps/marketplace/App.tsx index 2dcf58ab2e0..9446bfe471c 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/App.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx @@ -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} />} diff --git a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx index 4f7676767ba..73611432c0b 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx @@ -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 index d392d5efcc3..00000000000 --- a/server/sonar-web/src/main/js/apps/marketplace/PendingActions.tsx +++ /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__/__snapshots__/PendingActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap deleted file mode 100644 index c8688596746..00000000000 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap +++ /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> -`; diff --git a/server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx b/server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx index 63f5695de00..835b51831e1 100644 --- a/server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx +++ b/server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/store/marketplace/actions.ts b/server/sonar-web/src/main/js/store/marketplace/actions.ts index dfc5f8da834..894c74f2398 100644 --- a/server/sonar-web/src/main/js/store/marketplace/actions.ts +++ b/server/sonar-web/src/main/js/store/marketplace/actions.ts @@ -20,12 +20,18 @@ 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( diff --git a/server/sonar-web/src/main/js/store/marketplace/reducer.ts b/server/sonar-web/src/main/js/store/marketplace/reducer.ts index 2afdef1e678..11c5e28d7f5 100644 --- a/server/sonar-web/src/main/js/store/marketplace/reducer.ts +++ b/server/sonar-web/src/main/js/store/marketplace/reducer.ts @@ -19,17 +19,20 @@ */ 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; diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index 1a8ca7d54e6..51ea1ad9971 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -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); |