--- /dev/null
+/*
+ * 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);
+}
</ul>
</li>
+ <li>
+ <IndexLink to="/admin/marketplace" activeClassName="active">
+ {translate('marketplace.page')}
+ </IndexLink>
+ </li>
+
{hasSupportExtension && (
<li>
<IndexLink to="/admin/extension/license/support" activeClassName="active">
</li>
</ul>
</li>
+ <li>
+ <IndexLink
+ activeClassName="active"
+ to="/admin/marketplace"
+ >
+ marketplace.page
+ </IndexLink>
+ </li>
</NavBarTabs>
</ContextNavBar>
`;
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';
<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>
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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);
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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;
--- /dev/null
+/*
+ * 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
+ })
+);
interface Props {
checked: boolean;
- children?: React.ReactElement<any>;
+ children?: React.ReactNode;
className?: string;
id?: string;
onCheck: (checked: boolean, id?: string) => void;
* 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;
}
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>
);
};
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} />;
// 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"
display: inline-block !important;
}
+.diplay-flex-row {
+ display: flex !important;
+ flex-direction: row;
+}
+
.rounded {
border-radius: 2px;
}
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
#------------------------------------------------------------------------------
#