diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-10-11 17:32:30 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-10-23 08:01:13 -0700 |
commit | 847b1833f8f86657e9ddc206fab04e29ec5dbdc9 (patch) | |
tree | 1384b20f4716865607837808daba49a9a23b526c | |
parent | ddb6f1e13ff2cde359672c1df474c5b6e6f137da (diff) | |
download | sonarqube-847b1833f8f86657e9ddc206fab04e29ec5dbdc9.tar.gz sonarqube-847b1833f8f86657e9ddc206fab04e29ec5dbdc9.zip |
SONAR-9935 Migrate Update center to Marketplace
32 files changed, 1803 insertions, 5 deletions
diff --git a/server/sonar-web/src/main/js/api/plugins.ts b/server/sonar-web/src/main/js/api/plugins.ts new file mode 100644 index 00000000000..75004b3fc1d --- /dev/null +++ b/server/sonar-web/src/main/js/api/plugins.ts @@ -0,0 +1,168 @@ +/* + * 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 { getJSON, post } from '../helpers/request'; +import { findLastIndex } from 'lodash'; +import throwGlobalError from '../app/utils/throwGlobalError'; + +export interface Plugin { + key: string; + name: string; + description: string; + category?: string; + license?: string; + organizationName?: string; + organizationUrl?: string; + homepageUrl?: string; + issueTrackerUrl?: string; + termsAndConditionsUrl?: string; +} + +export interface Release { + version: string; + date: string; + description?: string; + changeLogUrl?: string; +} + +export interface Update { + status: string; + release?: Release; + requires: Plugin[]; + previousUpdates?: Update[]; +} + +export interface PluginAvailable extends Plugin { + release: Release; + update: Update; +} + +export interface PluginPending extends Plugin { + version: string; + implementationBuild: string; +} + +export interface PluginInstalled extends PluginPending { + filename: string; + hash: string; + sonarLintSupported: boolean; + updatedAt: number; + updates?: Update[]; +} + +export function getAvailablePlugins(): Promise<{ + plugins: PluginAvailable[]; + updateCenterRefresh: string; +}> { + return getJSON('/api/plugins/available').catch(throwGlobalError); +} + +export function getPendingPlugins(): Promise<{ + installing: PluginPending[]; + updating: PluginPending[]; + removing: PluginPending[]; +}> { + return getJSON('/api/plugins/pending').catch(throwGlobalError); +} + +function getLastUpdates(updates: undefined | Update[]): Update[] { + if (!updates) { + return []; + } + const lastUpdate = [ + 'INCOMPATIBLE', + 'REQUIRES_SYSTEM_UPGRADE', + 'DEPS_REQUIRE_SYSTEM_UPGRADE' + ].map(status => { + const index = findLastIndex(updates, update => update.status === status); + return index > -1 ? updates[index] : undefined; + }); + return lastUpdate.filter(Boolean) as Update[]; +} + +function addChangelog(update: Update, updates?: Update[]) { + if (!updates) { + return update; + } + const index = updates.indexOf(update); + const previousUpdates = index > 0 ? updates.slice(0, index) : []; + return { ...update, previousUpdates }; +} + +export function getInstalledPluginsWithUpdates(): Promise<PluginInstalled[]> { + return Promise.all([ + getJSON('/api/plugins/installed', { f: 'category' }), + getJSON('/api/plugins/updates') + ]) + .then(([installed, updates]) => + installed.plugins.map((plugin: PluginInstalled) => { + const updatePlugin: PluginInstalled = updates.plugins.find( + (p: PluginInstalled) => p.key === plugin.key + ); + if (updatePlugin) { + return { + ...updatePlugin, + ...plugin, + updates: getLastUpdates(updatePlugin.updates).map(update => + addChangelog(update, updatePlugin.updates) + ) + }; + } + return plugin; + }) + ) + .catch(throwGlobalError); +} + +export function getPluginUpdates(): Promise<PluginInstalled[]> { + return Promise.all([getJSON('/api/plugins/updates'), getJSON('/api/plugins/installed')]) + .then(([updates, installed]) => + updates.plugins.map((updatePlugin: PluginInstalled) => { + const updates = getLastUpdates(updatePlugin.updates).map(update => + addChangelog(update, updatePlugin.updates) + ); + const plugin = installed.plugins.find((p: PluginInstalled) => p.key === updatePlugin.key); + if (plugin) { + return { + ...plugin, + ...updatePlugin, + updates + }; + } + return { ...updatePlugin, updates }; + }) + ) + .catch(throwGlobalError); +} + +export function installPlugin(data: { key: string }): Promise<void | Response> { + return post('/api/plugins/install', data).catch(throwGlobalError); +} + +export function uninstallPlugin(data: { key: string }): Promise<void | Response> { + return post('/api/plugins/uninstall', data).catch(throwGlobalError); +} + +export function updatePlugin(data: { key: string }): Promise<void | Response> { + return post('/api/plugins/update', data).catch(throwGlobalError); +} + +export function cancelPendingPlugins(): Promise<void | Response> { + return post('/api/plugins/cancel_all').catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js index 69cbec3e6dd..e307778eec4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js @@ -192,6 +192,12 @@ class SettingsNav extends React.PureComponent { </ul> </li> + <li> + <IndexLink to="/admin/marketplace" activeClassName="active"> + {translate('marketplace.page')} + </IndexLink> + </li> + {hasSupportExtension && ( <li> <IndexLink to="/admin/extension/license/support" activeClassName="active"> diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap index d70451a6f1d..62c113f24bb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap @@ -188,6 +188,14 @@ exports[`should work with extensions 1`] = ` </li> </ul> </li> + <li> + <IndexLink + activeClassName="active" + to="/admin/marketplace" + > + marketplace.page + </IndexLink> + </li> </NavBarTabs> </ContextNavBar> `; diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index 06dc6283e04..b71489f5e86 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -48,6 +48,7 @@ import componentMeasuresRoutes from '../../apps/component-measures/routes'; import customMeasuresRoutes from '../../apps/custom-measures/routes'; import groupsRoutes from '../../apps/groups/routes'; import issuesRoutes from '../../apps/issues/routes'; +import marketplaceRoutes from '../../apps/marketplace/routes'; import metricsRoutes from '../../apps/metrics/routes'; import overviewRoutes from '../../apps/overview/routes'; import organizationsRoutes from '../../apps/organizations/routes'; @@ -227,6 +228,7 @@ const startReactApp = () => { <Route path="settings" childRoutes={settingsRoutes} /> <Route path="system" childRoutes={systemRoutes} /> <Route path="update_center" childRoutes={updateCenterRoutes} /> + <Route path="marketplace" childRoutes={marketplaceRoutes} /> <Route path="users" childRoutes={usersRoutes} /> </Route> </Route> diff --git a/server/sonar-web/src/main/js/apps/marketplace/App.tsx b/server/sonar-web/src/main/js/apps/marketplace/App.tsx new file mode 100644 index 00000000000..0cb9096adc3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/App.tsx @@ -0,0 +1,173 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import { sortBy, uniqBy } from 'lodash'; +import Helmet from 'react-helmet'; +import Header from './Header'; +import Footer from './Footer'; +import PendingActions from './PendingActions'; +import PluginsList from './PluginsList'; +import Search from './Search'; +import { + getAvailablePlugins, + getInstalledPluginsWithUpdates, + getPendingPlugins, + getPluginUpdates, + Plugin, + PluginPending +} from '../../api/plugins'; +import { RawQuery } from '../../helpers/query'; +import { translate } from '../../helpers/l10n'; +import { filterPlugins, parseQuery, Query, serializeQuery } from './utils'; + +export interface Props { + location: { pathname: string; query: RawQuery }; + updateCenterActive: boolean; +} + +interface State { + loading: boolean; + pending: { + installing: PluginPending[]; + updating: PluginPending[]; + removing: PluginPending[]; + }; + plugins: Plugin[]; +} + +export default class App extends React.PureComponent<Props, State> { + mounted: boolean; + + static contextTypes = { + router: PropTypes.object.isRequired + }; + + constructor(props: Props) { + super(props); + this.state = { + loading: true, + pending: { + installing: [], + updating: [], + removing: [] + }, + plugins: [] + }; + } + + componentDidMount() { + this.mounted = true; + this.fetchPendingPlugins(); + this.fetchQueryPlugins(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.location.query.filter !== this.props.location.query.filter) { + this.fetchQueryPlugins(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchQueryPlugins = () => { + const query = parseQuery(this.props.location.query); + if (query.filter === 'updates') { + this.fetchUpdatesOnly(); + } else { + this.fetchAllPlugins(); + } + }; + + fetchAllPlugins = () => { + this.setState({ loading: true }); + Promise.all([getInstalledPluginsWithUpdates(), getAvailablePlugins()]).then( + ([installed, available]) => { + if (this.mounted) { + this.setState({ + loading: false, + plugins: sortBy(uniqBy([...installed, ...available.plugins], 'key'), 'name') + }); + } + }, + () => {} + ); + }; + + fetchUpdatesOnly = () => { + this.setState({ loading: true }); + getPluginUpdates().then( + plugins => { + if (this.mounted) { + this.setState({ loading: false, plugins }); + } + }, + () => {} + ); + }; + + 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 + }); + }; + + render() { + const { plugins, pending } = 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')} /> + <Header /> + <PendingActions refreshPending={this.fetchPendingPlugins} pending={pending} /> + <Search + query={query} + updateCenterActive={this.props.updateCenterActive} + updateQuery={this.updateQuery} + /> + <PluginsList + plugins={filteredPlugins} + pending={pending} + refreshPending={this.fetchPendingPlugins} + updateQuery={this.updateQuery} + /> + <Footer total={filteredPlugins.length} /> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx new file mode 100644 index 00000000000..4319d2b1890 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx @@ -0,0 +1,28 @@ +/* + * 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 App from './App'; +import { getGlobalSettingValue } from '../../store/rootReducer'; + +const mapStateToProps = (state: any) => ({ + updateCenterActive: (getGlobalSettingValue(state, 'sonar.updatecenter.activate') || {}).value +}); + +export default connect(mapStateToProps)(App as any); diff --git a/server/sonar-web/src/main/js/apps/marketplace/Footer.tsx b/server/sonar-web/src/main/js/apps/marketplace/Footer.tsx new file mode 100644 index 00000000000..d5b950ddc6c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/Footer.tsx @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { translateWithParameters } from '../../helpers/l10n'; + +interface Props { + total: number; +} + +export default function Footer({ total }: Props) { + return ( + <footer className="spacer-top note text-center"> + {translateWithParameters('x_show', total)} + </footer> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/Header.tsx b/server/sonar-web/src/main/js/apps/marketplace/Header.tsx new file mode 100644 index 00000000000..8dbfd3777a9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/Header.tsx @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { translate } from '../../helpers/l10n'; + +export default function Header() { + return ( + <header id="marketplace-header" className="page-header"> + <h1 className="page-title">{translate('marketplace.page')}</h1> + <p className="page-description">{translate('marketplace.page.description')}</p> + </header> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PendingActions.tsx b/server/sonar-web/src/main/js/apps/marketplace/PendingActions.tsx new file mode 100644 index 00000000000..921afeef3da --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PendingActions.tsx @@ -0,0 +1,120 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { translate } from '../../helpers/l10n'; +import { cancelPendingPlugins, PluginPending } from '../../api/plugins'; +import RestartForm from '../../components/common/RestartForm'; + +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; + } + + const nbPluginsClass = 'big little-spacer-left little-spacer-right'; + return ( + <div className="js-pending panel panel-warning big-spacer-bottom"> + <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 className={'text-success ' + nbPluginsClass}> + {installing.length} + </strong> + ) + }} + /> + </li> + )} + {updating.length > 0 && ( + <li> + <FormattedMessage + defaultMessage={translate('marketplace.update_x_plugins')} + id="marketplace.update_x_plugins" + values={{ + nb: ( + <strong className={'text-success ' + nbPluginsClass}> + {updating.length} + </strong> + ) + }} + /> + </li> + )} + {removing.length > 0 && ( + <li> + <FormattedMessage + defaultMessage={translate('marketplace.uninstall_x_plugins')} + id="marketplace.uninstall_x_plugins" + values={{ + nb: ( + <strong className={'text-danger ' + nbPluginsClass}>{removing.length}</strong> + ) + }} + /> + </li> + )} + </ul> + </div> + <div className="pull-right button-group"> + <button className="js-restart" 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/PluginActions.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginActions.tsx new file mode 100644 index 00000000000..d14cc83cd94 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginActions.tsx @@ -0,0 +1,127 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import Checkbox from '../../components/controls/Checkbox'; +import PluginUpdateButton from './PluginUpdateButton'; +import { Plugin, installPlugin, updatePlugin, uninstallPlugin } from '../../api/plugins'; +import { isPluginAvailable, isPluginInstalled } from './utils'; +import { translate } from '../../helpers/l10n'; + +interface Props { + plugin: Plugin; + refreshPending: () => void; +} + +interface State { + acceptTerms: boolean; + loading: boolean; +} + +export default class PluginActions extends React.PureComponent<Props, State> { + mounted: boolean; + state: State = { acceptTerms: false, loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + doPluginAction = (apiAction: (data: { key: string }) => Promise<void | Response>) => { + this.setState({ loading: true }); + apiAction({ key: this.props.plugin.key }).then( + () => { + this.props.refreshPending(); + if (this.mounted) { + this.setState({ loading: false }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + handleInstall = () => this.doPluginAction(installPlugin); + handleUpdate = () => this.doPluginAction(updatePlugin); + handleUninstall = () => this.doPluginAction(uninstallPlugin); + handleTermsCheck = (checked: boolean) => this.setState({ acceptTerms: checked }); + + render() { + const { plugin } = this.props; + const { loading } = this.state; + return ( + <div className="js-actions"> + {isPluginAvailable(plugin) && + plugin.termsAndConditionsUrl && ( + <p className="little-spacer-bottom"> + <Checkbox + checked={this.state.acceptTerms} + className="js-terms" + id={'plugin-terms-' + plugin.key} + onCheck={this.handleTermsCheck}> + <label className="little-spacer-left" htmlFor={'plugin-terms-' + plugin.key}> + {translate('marketplace.i_accept_the')} + </label> + </Checkbox> + <a + className="js-plugin-terms nowrap little-spacer-left" + href={plugin.termsAndConditionsUrl} + target="_blank"> + {translate('marketplace.terms_and_conditions')} + </a> + </p> + )} + {loading && <i className="spinner spacer-right" />} + {isPluginInstalled(plugin) && ( + <div className="button-group"> + {plugin.updates && + plugin.updates.map((update, idx) => ( + <PluginUpdateButton + key={idx} + onClick={this.handleUpdate} + update={update} + disabled={loading} + /> + ))} + <button + className="js-uninstall button-red" + disabled={loading} + onClick={this.handleUninstall}> + {translate('marketplace.uninstall')} + </button> + </div> + )} + {isPluginAvailable(plugin) && ( + <button + className="js-install" + disabled={loading || (plugin.termsAndConditionsUrl != null && !this.state.acceptTerms)} + onClick={this.handleInstall}> + {translate('marketplace.install')} + </button> + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginAvailable.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginAvailable.tsx new file mode 100644 index 00000000000..26c12116a49 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginAvailable.tsx @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import PluginDescription from './PluginDescription'; +import PluginLicense from './PluginLicense'; +import PluginOrganization from './PluginOrganization'; +import PluginStatus from './PluginStatus'; +import PluginChangeLogButton from './PluginChangeLogButton'; +import { PluginAvailable } from '../../api/plugins'; +import { translateWithParameters } from '../../helpers/l10n'; +import { Query } from './utils'; + +interface Props { + plugin: PluginAvailable; + refreshPending: () => void; + status?: string; + updateQuery: (newQuery: Partial<Query>) => void; +} + +export default function PluginAvailable({ plugin, refreshPending, status, updateQuery }: Props) { + return ( + <tr> + <PluginDescription plugin={plugin} updateQuery={updateQuery} /> + <td className="text-top big-spacer-right"> + <ul> + <li className="diplay-flex-row little-spacer-bottom"> + <div className="pull-left spacer-right"> + <span className="badge badge-success">{plugin.release.version}</span> + </div> + <div> + {plugin.release.description} + <PluginChangeLogButton release={plugin.release} update={plugin.update} /> + {plugin.update.requires.length > 0 && ( + <p className="little-spacer-top"> + <strong> + {translateWithParameters( + 'marketplace.installing_this_plugin_will_also_install_x', + plugin.update.requires.map(requiredPlugin => requiredPlugin.name).join(', ') + )} + </strong> + </p> + )} + </div> + </li> + </ul> + </td> + + <td className="text-top width-20 big-spacer-right"> + <ul> + <PluginLicense license={plugin.license} /> + <PluginOrganization plugin={plugin} /> + </ul> + </td> + + <PluginStatus plugin={plugin} status={status} refreshPending={refreshPending} /> + </tr> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginChangeLog.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginChangeLog.tsx new file mode 100644 index 00000000000..21690e4e136 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginChangeLog.tsx @@ -0,0 +1,54 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import BubblePopup from '../../components/common/BubblePopup'; +import PluginChangeLogItem from './PluginChangeLogItem'; +import { Release, Update } from '../../api/plugins'; +import { translate } from '../../helpers/l10n'; + +interface Props { + popupPosition?: any; + release: Release; + update: Update; +} + +export default function PluginChangeLog({ popupPosition, release, update }: Props) { + return ( + <BubblePopup position={popupPosition} customClass="bubble-popup-bottom-right"> + <div className="abs-width-300 bubble-popup-container"> + <div className="bubble-popup-title">{translate('changelog')}</div> + <ul className="js-plugin-changelog-list"> + {update.previousUpdates && + update.previousUpdates.map( + previousUpdate => + previousUpdate.release ? ( + <PluginChangeLogItem + key={previousUpdate.release.version} + release={previousUpdate.release} + update={previousUpdate} + /> + ) : null + )} + <PluginChangeLogItem release={release} update={update} /> + </ul> + </div> + </BubblePopup> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogButton.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogButton.tsx new file mode 100644 index 00000000000..ec87c9ea26c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogButton.tsx @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import BubblePopupHelper from '../../components/common/BubblePopupHelper'; +import PluginChangeLog from './PluginChangeLog'; +import { Release, Update } from '../../api/plugins'; + +interface Props { + release: Release; + update: Update; +} + +interface State { + changelogOpen: boolean; +} + +export default class PluginChangeLogButton extends React.PureComponent<Props, State> { + state: State = { changelogOpen: false }; + + handleChangelogClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { + event.preventDefault(); + event.stopPropagation(); + this.toggleChangelog(); + }; + + toggleChangelog = (show?: boolean) => { + if (show != undefined) { + this.setState({ changelogOpen: show }); + } else { + this.setState(state => ({ changelogOpen: !state.changelogOpen })); + } + }; + + render() { + return ( + <div className="display-inline-block little-spacer-left"> + <button + className="button-link js-changelog issue-rule icon-ellipsis-h" + onClick={this.handleChangelogClick} + /> + <BubblePopupHelper + isOpen={this.state.changelogOpen} + position="bottomright" + popup={<PluginChangeLog release={this.props.release} update={this.props.update} />} + togglePopup={this.toggleChangelog} + /> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogItem.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogItem.tsx new file mode 100644 index 00000000000..163c8165ced --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogItem.tsx @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import DateFormatter from '../../components/intl/DateFormatter'; +import Tooltip from '../../components/controls/Tooltip'; +import { Release, Update } from '../../api/plugins'; +import { translate, translateWithParameters } from '../../helpers/l10n'; + +interface Props { + release: Release; + update: Update; +} + +export default function PluginChangeLogItem({ release, update }: Props) { + return ( + <li className="big-spacer-bottom"> + <div className="little-spacer-bottom"> + {update.status === 'COMPATIBLE' || !update.status ? ( + <span className="js-plugin-changelog-version badge badge-success spacer-right"> + {release.version} + </span> + ) : ( + <Tooltip overlay={translateWithParameters('marketplace.status', update.status)}> + <span className="js-plugin-changelog-version badge badge-warning spacer-right"> + {release.version} + </span> + </Tooltip> + )} + <span className="js-plugin-changelog-date note spacer-right"> + <DateFormatter date={release.date} /> + </span> + {release.changeLogUrl && ( + <a className="js-plugin-changelog-link" href={release.changeLogUrl} target="_blank"> + {translate('update_center.release_notes')} + </a> + )} + </div> + <div className="js-plugin-changelog-description">{release.description}</div> + </li> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginDescription.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginDescription.tsx new file mode 100644 index 00000000000..8de1f3b0476 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginDescription.tsx @@ -0,0 +1,54 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Plugin } from '../../api/plugins'; +import { Query } from './utils'; + +interface Props { + plugin: Plugin; + updateQuery: (newQuery: Partial<Query>) => void; +} + +export default class PluginDescription extends React.PureComponent<Props> { + handleCategoryClick = (e: React.SyntheticEvent<HTMLAnchorElement>) => { + e.preventDefault(); + this.props.updateQuery({ search: this.props.plugin.category }); + }; + + render() { + const { plugin } = this.props; + return ( + <td className="text-top width-20 big-spacer-right"> + <div> + <strong className="js-plugin-name">{plugin.name}</strong> + {plugin.category && ( + <a + className="js-plugin-category badge spacer-left" + href="#" + onClick={this.handleCategoryClick}> + {plugin.category} + </a> + )} + </div> + <div className="js-plugin-description little-spacer-top">{plugin.description}</div> + </td> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginInstalled.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginInstalled.tsx new file mode 100644 index 00000000000..85202411f18 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginInstalled.tsx @@ -0,0 +1,83 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import PluginDescription from './PluginDescription'; +import PluginLicense from './PluginLicense'; +import PluginStatus from './PluginStatus'; +import PluginOrganization from './PluginOrganization'; +import PluginUpdates from './PluginUpdates'; +import { PluginInstalled } from '../../api/plugins'; +import { translate } from '../../helpers/l10n'; +import { Query } from './utils'; + +interface Props { + plugin: PluginInstalled; + refreshPending: () => void; + status?: string; + updateQuery: (newQuery: Partial<Query>) => void; +} + +export default function PluginInstalled({ plugin, refreshPending, status, updateQuery }: Props) { + return ( + <tr> + <PluginDescription plugin={plugin} updateQuery={updateQuery} /> + <td className="text-top big-spacer-right"> + <ul> + <li className="little-spacer-bottom"> + <strong className="js-plugin-installed-version little-spacer-right"> + {plugin.version} + </strong> + {translate('marketplace._installed')} + </li> + <PluginUpdates updates={plugin.updates} /> + </ul> + </td> + + <td className="text-top width-20 big-spacer-right"> + <ul> + {(plugin.homepageUrl || plugin.issueTrackerUrl) && ( + <li className="little-spacer-bottom"> + <ul className="list-inline"> + {plugin.homepageUrl && ( + <li> + <a className="js-plugin-homepage" href={plugin.homepageUrl} target="_blank"> + {translate('marketplace.homepage')} + </a> + </li> + )} + {plugin.issueTrackerUrl && ( + <li> + <a className="js-plugin-issues" href={plugin.issueTrackerUrl} target="_blank"> + {translate('marketplace.issue_tracker')} + </a> + </li> + )} + </ul> + </li> + )} + <PluginLicense license={plugin.license} /> + <PluginOrganization plugin={plugin} /> + </ul> + </td> + + <PluginStatus plugin={plugin} status={status} refreshPending={refreshPending} /> + </tr> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginLicense.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginLicense.tsx new file mode 100644 index 00000000000..550ba9249f3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginLicense.tsx @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { translate } from '../../helpers/l10n'; + +interface Props { + license?: string; +} + +export default function PluginLicense({ license }: Props) { + if (!license) { + return null; + } + return ( + <li className="little-spacer-bottom text-limited" title={license}> + <FormattedMessage + defaultMessage={translate('marketplace.licensed_under_x')} + id="marketplace.licensed_under_x" + values={{ + license: <span className="js-plugin-license">{license}</span> + }} + /> + </li> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginOrganization.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginOrganization.tsx new file mode 100644 index 00000000000..505ebd16f28 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginOrganization.tsx @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { translate } from '../../helpers/l10n'; +import { Plugin } from '../../api/plugins'; + +interface Props { + plugin: Plugin; +} + +export default function PluginOrganization({ plugin }: Props) { + if (!plugin.organizationName) { + return null; + } + return ( + <li className="little-spacer-bottom"> + <FormattedMessage + defaultMessage={translate('marketplace.developed_by_x')} + id="marketplace.developed_by_x" + values={{ + organization: plugin.organizationUrl ? ( + <a className="js-plugin-organization" href={plugin.organizationUrl} target="_blank"> + {plugin.organizationName} + </a> + ) : ( + <span className="js-plugin-organization">{plugin.organizationName}</span> + ) + }} + /> + </li> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginStatus.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginStatus.tsx new file mode 100644 index 00000000000..298206f2e92 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginStatus.tsx @@ -0,0 +1,54 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Plugin } from '../../api/plugins'; +import PluginActions from './PluginActions'; +import { translate } from '../../helpers/l10n'; + +interface Props { + plugin: Plugin; + refreshPending: () => void; + status?: string; +} + +export default function PluginStatus({ plugin, refreshPending, status }: Props) { + return ( + <td className="text-top text-right width-20"> + {status === 'installing' && ( + <p className="text-success">{translate('marketplace.install_pending')}</p> + )} + + {status === 'updating' && ( + <p className="text-success">{translate('marketplace.update_pending')}</p> + )} + + {status === 'removing' && ( + <p className="text-danger">{translate('marketplace.uninstall_pending')}</p> + )} + + {status == null && ( + <div> + <i className="js-spinner spinner hidden" /> + <PluginActions plugin={plugin} refreshPending={refreshPending} /> + </div> + )} + </td> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginUpdateButton.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginUpdateButton.tsx new file mode 100644 index 00000000000..53c154e8fa6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginUpdateButton.tsx @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Update } from '../../api/plugins'; +import { translateWithParameters } from '../../helpers/l10n'; + +interface Props { + disabled: boolean; + onClick: (update: Update) => void; + update: Update; +} + +export default class PluginUpdateButton extends React.PureComponent<Props> { + handleClick = () => this.props.onClick(this.props.update); + + render() { + const { disabled, update } = this.props; + if (update.status !== 'COMPATIBLE' || !update.release) { + return null; + } + return ( + <button className="js-update" onClick={this.handleClick} disabled={disabled}> + {translateWithParameters('marketplace.update_to_x', update.release.version)} + </button> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginUpdateItem.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginUpdateItem.tsx new file mode 100644 index 00000000000..f4d9d19d1c8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginUpdateItem.tsx @@ -0,0 +1,72 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import PluginChangeLogButton from './PluginChangeLogButton'; +import Tooltip from '../../components/controls/Tooltip'; +import { Release, Update } from '../../api/plugins'; +import { translate } from '../../helpers/l10n'; + +interface Props { + update: Update; + release: Release; +} + +interface State { + changelogOpen: boolean; +} + +export default class PluginUpdateItem extends React.PureComponent<Props, State> { + state: State = { changelogOpen: false }; + + handleChangelogClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { + event.preventDefault(); + event.stopPropagation(); + this.toggleChangelog(); + }; + + toggleChangelog = (show?: boolean) => { + if (show != undefined) { + this.setState({ changelogOpen: show }); + } else { + this.setState(state => ({ changelogOpen: !state.changelogOpen })); + } + }; + + render() { + const { release, update } = this.props; + return ( + <li key={release.version} className="diplay-flex-row little-spacer-bottom"> + <div className="pull-left spacer-right"> + {update.status === 'COMPATIBLE' ? ( + <span className="js-update-version badge badge-success">{release.version}</span> + ) : ( + <Tooltip overlay={translate('marketplace.status', update.status)}> + <span className="js-update-version badge badge-warning">{release.version}</span> + </Tooltip> + )} + </div> + <div> + {release.description} + <PluginChangeLogButton release={release} update={update} /> + </div> + </li> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginUpdates.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginUpdates.tsx new file mode 100644 index 00000000000..b662651f1dc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginUpdates.tsx @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import PluginUpdateItem from './PluginUpdateItem'; +import { Update } from '../../api/plugins'; +import { translate } from '../../helpers/l10n'; + +interface Props { + updates?: Update[]; +} + +export default function PluginUpdates({ updates }: Props) { + if (!updates || updates.length <= 0) { + return null; + } + return ( + <li className="spacer-top"> + <strong>{translate('marketplace.updates')}:</strong> + <ul className="little-spacer-top"> + {updates.map( + update => + update.release ? ( + <PluginUpdateItem + key={update.release.version} + release={update.release} + update={update} + /> + ) : null + )} + </ul> + </li> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx b/server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx new file mode 100644 index 00000000000..0e51d154342 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx @@ -0,0 +1,91 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import PluginAvailable from './PluginAvailable'; +import PluginInstalled from './PluginInstalled'; +import { isPluginAvailable, isPluginInstalled, Query } from './utils'; +import { Plugin, PluginPending } from '../../api/plugins'; + +interface Props { + plugins: Plugin[]; + pending: { + installing: PluginPending[]; + updating: PluginPending[]; + removing: PluginPending[]; + }; + refreshPending: () => void; + updateQuery: (newQuery: Partial<Query>) => void; +} + +export default class PluginsList extends React.PureComponent<Props> { + getPluginStatus = (plugin: Plugin): string | undefined => { + const { installing, updating, removing } = this.props.pending; + if (installing.find(p => p.key === plugin.key)) { + return 'installing'; + } + if (updating.find(p => p.key === plugin.key)) { + return 'updating'; + } + if (removing.find(p => p.key === plugin.key)) { + return 'removing'; + } + return undefined; + }; + + renderPlugin = (plugin: Plugin) => { + const status = this.getPluginStatus(plugin); + if (isPluginInstalled(plugin)) { + return ( + <PluginInstalled + plugin={plugin} + status={status} + refreshPending={this.props.refreshPending} + updateQuery={this.props.updateQuery} + /> + ); + } + if (isPluginAvailable(plugin)) { + return ( + <PluginAvailable + plugin={plugin} + status={status} + refreshPending={this.props.refreshPending} + updateQuery={this.props.updateQuery} + /> + ); + } + }; + + render() { + return ( + <div id="marketplace-plugins"> + <ul> + {this.props.plugins.map(plugin => ( + <li key={plugin.key} className="panel panel-vertical"> + <table className="width-100"> + <tbody>{this.renderPlugin(plugin)}</tbody> + </table> + </li> + ))} + </ul> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/Search.tsx b/server/sonar-web/src/main/js/apps/marketplace/Search.tsx new file mode 100644 index 00000000000..194427a709e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/Search.tsx @@ -0,0 +1,98 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { debounce } from 'lodash'; +import RadioToggle from '../../components/controls/RadioToggle'; +import { Query } from './utils'; +import { translate } from '../../helpers/l10n'; + +interface Props { + query: Query; + updateCenterActive: boolean; + updateQuery: (newQuery: Partial<Query>) => void; +} + +interface State { + search?: string; +} + +export default class Search extends React.PureComponent<Props, State> { + constructor(props: Props) { + super(props); + this.state = { search: props.query.search }; + this.updateSearch = debounce(this.updateSearch, 250); + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.query.search !== this.state.search) { + this.setState({ search: nextProps.query.search }); + } + } + + handleSearch = (e: React.SyntheticEvent<HTMLInputElement>) => { + const search = e.currentTarget.value; + this.setState({ search }); + this.updateSearch(search); + }; + + handleFilterChange = (filter: string) => this.props.updateQuery({ filter }); + + updateSearch = (search: string) => this.props.updateQuery({ search }); + + render() { + const { query, updateCenterActive } = this.props; + const radioOptions = [ + { label: translate('marketplace.all'), value: 'all' }, + { + disabled: !updateCenterActive, + label: translate('marketplace.updates_only'), + tooltip: !updateCenterActive ? translate('marketplace.not_activated') : undefined, + value: 'updates' + } + ]; + return ( + <div id="marketplace-search" className="panel panel-vertical bordered-bottom spacer-bottom"> + <div className="display-inline-block text-top nowrap big-spacer-right"> + <RadioToggle + name="marketplace-filter" + onCheck={this.handleFilterChange} + options={radioOptions} + value={query.filter} + /> + </div> + <div className="search-box display-inline-block text-top"> + <button className="search-box-submit button-clean"> + <i className="icon-search" /> + </button> + <input + onChange={this.handleSearch} + value={this.state.search} + className="search-box-input" + type="search" + name="search" + placeholder={translate('search_verb')} + maxLength={100} + autoComplete="off" + /> + </div> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/routes.ts b/server/sonar-web/src/main/js/apps/marketplace/routes.ts new file mode 100644 index 00000000000..e066c3a2ef1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/routes.ts @@ -0,0 +1,30 @@ +/* + * 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 { RouterState, IndexRouteProps } from 'react-router'; + +const routes = [ + { + getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { + import('./AppContainer').then(i => callback(null, { component: i.default })); + } + } +]; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/marketplace/utils.ts b/server/sonar-web/src/main/js/apps/marketplace/utils.ts new file mode 100644 index 00000000000..780fafb3af8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/utils.ts @@ -0,0 +1,64 @@ +/* + * 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 { memoize } from 'lodash'; +import { Plugin, PluginAvailable, PluginInstalled, PluginPending } from '../../api/plugins'; +import { cleanQuery, parseAsString, RawQuery, serializeString } from '../../helpers/query'; + +export interface Query { + filter: string; + search?: string; +} + +export const DEFAULT_FILTER = 'all'; + +export function isPluginAvailable(plugin: Plugin): plugin is PluginAvailable { + return (plugin as any).release !== undefined; +} + +export function isPluginInstalled(plugin: Plugin): plugin is PluginInstalled { + return isPluginPending(plugin) && (plugin as any).updatedAt !== undefined; +} + +export function isPluginPending(plugin: Plugin): plugin is PluginPending { + return (plugin as any).version !== undefined; +} + +export function filterPlugins(plugins: Plugin[], search: string): Plugin[] { + const s = search.toLowerCase(); + return plugins.filter(plugin => { + return ( + plugin.name.toLowerCase().includes(s) || + plugin.description.toLowerCase().includes(s) || + (plugin.category || '').toLowerCase().includes(s) + ); + }); +} + +export const parseQuery = memoize((urlQuery: RawQuery): Query => ({ + filter: parseAsString(urlQuery['filter']) || DEFAULT_FILTER, + search: parseAsString(urlQuery['search']) +})); + +export const serializeQuery = memoize((query: Query): RawQuery => + cleanQuery({ + filter: query.filter === DEFAULT_FILTER ? undefined : serializeString(query.filter), + search: query.search ? serializeString(query.search) : undefined + }) +); diff --git a/server/sonar-web/src/main/js/components/controls/Checkbox.tsx b/server/sonar-web/src/main/js/components/controls/Checkbox.tsx index 1e5afe85195..f0986772213 100644 --- a/server/sonar-web/src/main/js/components/controls/Checkbox.tsx +++ b/server/sonar-web/src/main/js/components/controls/Checkbox.tsx @@ -22,7 +22,7 @@ import * as classNames from 'classnames'; interface Props { checked: boolean; - children?: React.ReactElement<any>; + children?: React.ReactNode; className?: string; id?: string; onCheck: (checked: boolean, id?: string) => void; diff --git a/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx b/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx index bfb7c4cfee1..8c4fe83ff20 100644 --- a/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx +++ b/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx @@ -18,11 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import Tooltip from './Tooltip'; + +interface Option { + disabled?: boolean; + label: string; + tooltip?: string; + value: string; +} interface Props { name: string; onCheck: (value: string) => void; - options: Array<{ label: string; value: string }>; + options: Option[]; value?: string; } @@ -37,21 +45,27 @@ export default class RadioToggle extends React.PureComponent<Props> { this.props.onCheck(newValue); }; - renderOption = (option: { label: string; value: string }) => { + renderOption = (option: Option) => { const checked = option.value === this.props.value; const htmlId = this.props.name + '__' + option.value; return ( <li key={option.value}> <input type="radio" + disabled={option.disabled} name={this.props.name} value={option.value} id={htmlId} checked={checked} onChange={this.handleChange} /> - - <label htmlFor={htmlId}>{option.label}</label> + {option.tooltip ? ( + <Tooltip overlay={option.tooltip}> + <label htmlFor={htmlId}>{option.label}</label> + </Tooltip> + ) : ( + <label htmlFor={htmlId}>{option.label}</label> + )} </li> ); }; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx index f1a231b564c..90b5659e124 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx @@ -33,6 +33,19 @@ it('calls onCheck', () => { expect(onCheck).toBeCalledWith('two'); }); +it('accepts advanced options fields', () => { + expect( + shallow( + getSample({ + options: [ + { value: 'one', label: 'first', tooltip: 'foo' }, + { value: 'two', label: 'second', tooltip: 'bar', disabled: true } + ] + }) + ) + ).toMatchSnapshot(); +}); + function getSample(props?: any) { const options = [{ value: 'one', label: 'first' }, { value: 'two', label: 'second' }]; return <RadioToggle options={options} name="sample" onCheck={() => true} {...props} />; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap index df9579888bd..c0aa0d1ab89 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap @@ -1,5 +1,53 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`accepts advanced options fields 1`] = ` +<ul + className="radio-toggle" +> + <li> + <input + checked={false} + id="sample__one" + name="sample" + onChange={[Function]} + type="radio" + value="one" + /> + <Tooltip + overlay="foo" + placement="bottom" + > + <label + htmlFor="sample__one" + > + first + </label> + </Tooltip> + </li> + <li> + <input + checked={false} + disabled={true} + id="sample__two" + name="sample" + onChange={[Function]} + type="radio" + value="two" + /> + <Tooltip + overlay="bar" + placement="bottom" + > + <label + htmlFor="sample__two" + > + second + </label> + </Tooltip> + </li> +</ul> +`; + exports[`renders 1`] = ` <ul className="radio-toggle" diff --git a/server/sonar-web/src/main/less/init/misc.less b/server/sonar-web/src/main/less/init/misc.less index c927347bea7..cff3da4fdf0 100644 --- a/server/sonar-web/src/main/less/init/misc.less +++ b/server/sonar-web/src/main/less/init/misc.less @@ -230,6 +230,11 @@ td.big-spacer-top { display: inline-block !important; } +.diplay-flex-row { + display: flex !important; + flex-direction: row; +} + .rounded { border-radius: 2px; } 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 3403e48bacb..4b66f3917df 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2050,6 +2050,42 @@ workspace.normal_size=Collapse to normal size workspace.close=Remove from the list of pinned files workspace.no_rule=The rule has been removed or never existed. +#------------------------------------------------------------------------------ +# +# MARKETPLACE +# +#------------------------------------------------------------------------------ +marketplace.page=Marketplace +marketplace.page.description=Discover and install new features +marketplace.sonarqube_needs_to_be_restarted_to=SonarQube needs to be restarted in order to +marketplace.install_x_plugins=install {nb} plugins +marketplace.update_x_plugins=update {nb} plugins +marketplace.uninstall_x_plugins=uninstall {nb} plugins +marketplace.not_activated=Update Center is not activated. +marketplace.all=All +marketplace.updates_only=Updates Only +marketplace.restart=Restart +marketplace.revert=Revert +marketplace.system_upgrades=System Upgrades +marketplace.install=Install +marketplace._installed=installed +marketplace.homepage=Homepage +marketplace.issue_tracker=Issue Tracker +marketplace.licensed_under_x=Licensed under {license} +marketplace.developed_by_x=Developed by {organization} +marketplace.install_pending=Install Pending +marketplace.update_pending=Update Pending +marketplace.uninstall_pending=Uninstall Pending +marketplace.updates=Updates +marketplace.status.COMPATIBLE=Compatible +marketplace.status.INCOMPATIBLE=Incompatible +marketplace.status.REQUIRES_SYSTEM_UPGRADE=Requires system update +marketplace.status.DEPS_REQUIRE_SYSTEM_UPGRADE=Some of dependencies requires system update +marketplace.installing_this_plugin_will_also_install_x=Installing this plugin will also install: {0} +marketplace.update_to_x=Update to {0} +marketplace.uninstall=Uninstall +marketplace.i_accept_the=I accept the +marketplace.terms_and_conditions=Terms and Conditions #------------------------------------------------------------------------------ # |