diff options
20 files changed, 508 insertions, 274 deletions
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 12f2f691d8e..bf12d09e0a1 100644 --- a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx @@ -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/apps/marketplace/components/EditionsStatusNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx index f21e8a30588..527b8479ada 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/components/EditionsStatusNotif.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx @@ -18,41 +18,38 @@ * 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'; +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; - readOnly: boolean; - updateEditionStatus: (editionStatus: EditionStatus) => void; + preventRestart: boolean; + setEditionStatus: (editionStatus: EditionStatus) => void; } interface State { openRestart: boolean; } -export default class EditionsStatusNotif extends React.PureComponent<Props, State> { +export default class SettingsEditionsNotif 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(); + handleDismissError = () => dismissErrorMessage().then( - () => - this.props.updateEditionStatus({ ...this.props.editionStatus, installError: undefined }), + () => this.props.setEditionStatus({ ...this.props.editionStatus, installError: undefined }), () => {} ); - }; renderRestartMsg(edition?: Edition) { - const { editionStatus, readOnly } = this.props; + const { editionStatus, preventRestart } = this.props; return ( - <div className="alert alert-success"> + <NavBarNotif className="alert alert-success"> <span> {edition ? ( translateWithParameters( @@ -63,20 +60,21 @@ export default class EditionsStatusNotif extends React.PureComponent<Props, Stat translate('marketplace.status', editionStatus.installationStatus) )} </span> - {!readOnly && ( + {!preventRestart && ( <button className="js-restart spacer-left" onClick={this.handleOpenRestart}> {translate('marketplace.restart')} </button> )} - {!readOnly && this.state.openRestart && <RestartForm onClose={this.hanleCloseRestart} />} - </div> + {!preventRestart && + this.state.openRestart && <RestartForm onClose={this.hanleCloseRestart} />} + </NavBarNotif> ); } renderManualMsg(edition?: Edition) { const { editionStatus } = this.props; return ( - <div className="alert alert-danger"> + <NavBarNotif className="alert alert-danger"> {edition ? ( translateWithParameters( 'marketplace.status_x.' + editionStatus.installationStatus, @@ -85,26 +83,22 @@ export default class EditionsStatusNotif extends React.PureComponent<Props, Stat ) : ( 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> - )} + {edition && ( <a - href="https://redirect.sonarsource.com/doc/how-to-install-an-edition.html" + className="button spacer-left" + download={`sonarqube-${edition.name}.zip`} + href={edition.downloadUrl} target="_blank"> - {translate('marketplace.how_to_install')} + {translate('marketplace.download_package')} </a> - </p> - <a className="little-spacer-left" href="https://www.sonarsource.com" target="_blank"> + )} + <a + className="spacer-left" + href="https://redirect.sonarsource.com/doc/how-to-install-an-edition.html" + target="_blank"> {translate('marketplace.how_to_install')} </a> - </div> + </NavBarNotif> ); } @@ -117,10 +111,10 @@ export default class EditionsStatusNotif extends React.PureComponent<Props, Stat switch (installationStatus) { case 'AUTOMATIC_IN_PROGRESS': return ( - <div className="alert alert-info"> + <NavBarNotif className="alert alert-info"> <i className="spinner spacer-right text-bottom" /> <span>{translate('marketplace.status.AUTOMATIC_IN_PROGRESS')}</span> - </div> + </NavBarNotif> ); case 'AUTOMATIC_READY': case 'UNINSTALL_IN_PROGRESS': @@ -133,18 +127,14 @@ export default class EditionsStatusNotif extends React.PureComponent<Props, Stat 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> - ); + 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 index 00000000000..6bac95a5897 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotifContainer.tsx @@ -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); 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 49bb352fb63..4a983b4feed 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,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/apps/marketplace/components/__tests__/EditionsStatusNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx index 4275351147c..3614b9b06fe 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionsStatusNotif-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx @@ -18,15 +18,15 @@ * 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'; +import { mount, shallow } from 'enzyme'; +import { click } from '../../../../../helpers/testUtils'; +import SettingsEditionsNotif from '../SettingsEditionsNotif'; -jest.mock('../../../../api/marketplace', () => ({ +jest.mock('../../../../../api/marketplace', () => ({ dismissErrorMessage: jest.fn(() => Promise.resolve()) })); -const dismissMsg = require('../../../../api/marketplace').dismissErrorMessage as jest.Mock<any>; +const dismissMsg = require('../../../../../api/marketplace').dismissErrorMessage as jest.Mock<any>; beforeEach(() => { dismissMsg.mockClear(); @@ -34,10 +34,10 @@ beforeEach(() => { it('should display an in progress notif', () => { const wrapper = shallow( - <EditionsStatusNotif + <SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }} - readOnly={false} - updateEditionStatus={jest.fn()} + preventRestart={false} + setEditionStatus={jest.fn()} /> ); expect(wrapper).toMatchSnapshot(); @@ -45,10 +45,31 @@ it('should display an in progress notif', () => { it('should display a ready notification', () => { const wrapper = shallow( - <EditionsStatusNotif + <SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_READY' }} - readOnly={false} - updateEditionStatus={jest.fn()} + 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(); @@ -56,29 +77,40 @@ it('should display a ready notification', () => { it('should display install errors', () => { const wrapper = shallow( - <EditionsStatusNotif + <SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS', installError: 'Foo error' }} - readOnly={false} - updateEditionStatus={jest.fn()} + preventRestart={false} + setEditionStatus={jest.fn()} /> ); expect(wrapper).toMatchSnapshot(); }); it('should allow to dismiss install errors', async () => { - const updateEditionStatus = jest.fn(); - const wrapper = shallow( - <EditionsStatusNotif + const setEditionStatus = jest.fn(); + const wrapper = mount( + <SettingsEditionsNotif editionStatus={{ installationStatus: 'NONE', installError: 'Foo error' }} - readOnly={false} - updateEditionStatus={updateEditionStatus} + preventRestart={false} + setEditionStatus={setEditionStatus} /> ); click(wrapper.find('a')); expect(dismissMsg).toHaveBeenCalled(); await new Promise(setImmediate); - expect(updateEditionStatus).toHaveBeenCalledWith({ + 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 index 00000000000..51ab67171a5 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap @@ -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> +`; 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 c57f3de78fd..4d6f63fd285 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/App.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx @@ -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} 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 90fac954650..6d2b46b010d 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx @@ -19,15 +19,47 @@ */ 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); diff --git a/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx b/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx index e9b7fa6f246..30370c1d3b2 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx @@ -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; } diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx index eeff50797ce..c0347bf9ad4 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx @@ -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/__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 index a76a31c61d2..00000000000 --- a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionsStatusNotif-test.tsx.snap +++ /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> -`; diff --git a/server/sonar-web/src/main/js/apps/marketplace/utils.ts b/server/sonar-web/src/main/js/apps/marketplace/utils.ts index 31835b2ec56..780fafb3af8 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/utils.ts +++ b/server/sonar-web/src/main/js/apps/marketplace/utils.ts @@ -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']) diff --git a/server/sonar-web/src/main/js/components/nav/NavBar.css b/server/sonar-web/src/main/js/components/nav/NavBar.css index fb9393dd02e..0a24b4232ad 100644 --- a/server/sonar-web/src/main/js/components/nav/NavBar.css +++ b/server/sonar-web/src/main/js/components/nav/NavBar.css @@ -28,3 +28,8 @@ border-right: none; padding: 6px 0; } + +.navbar-notif-cancelable { + display: flex; + justify-content: space-between; +} 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 9004c1b7fe9..c276e20f62f 100644 --- a/server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx +++ b/server/sonar-web/src/main/js/components/nav/NavBarNotif.tsx @@ -19,20 +19,38 @@ */ 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 index 00000000000..92e18cd48a7 --- /dev/null +++ b/server/sonar-web/src/main/js/store/marketplace/actions.ts @@ -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 index 00000000000..2d48eba1c1e --- /dev/null +++ b/server/sonar-web/src/main/js/store/marketplace/reducer.ts @@ -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 index 00000000000..51c22d707d8 --- /dev/null +++ b/server/sonar-web/src/main/js/store/marketplace/utils.ts @@ -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; +} diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index 9edca6126da..9897df6974a 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -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); diff --git a/server/sonar-web/src/main/less/components/alerts.less b/server/sonar-web/src/main/less/components/alerts.less index 1cc1c516fe2..3fbc9952960 100644 --- a/server/sonar-web/src/main/less/components/alerts.less +++ b/server/sonar-web/src/main/less/components/alerts.less @@ -66,11 +66,6 @@ .alert-emphasis-variant(#3c763d, #dff0d8, #d6e9c6); } -.alert-cancel { - display: flex; - justify-content: space-between; -} - .page-notifs .alert { padding: 8px 10px; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 30f40064b78..3b7da354e46 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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? |