]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9935 Migrate Update center to Marketplace
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 11 Oct 2017 15:32:30 +0000 (17:32 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 23 Oct 2017 15:01:13 +0000 (08:01 -0700)
32 files changed:
server/sonar-web/src/main/js/api/plugins.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/marketplace/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/Footer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/Header.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PendingActions.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginActions.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginAvailable.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginChangeLog.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginDescription.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginInstalled.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginLicense.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginOrganization.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginStatus.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginUpdateButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginUpdateItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginUpdates.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/Search.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/routes.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/Checkbox.tsx
server/sonar-web/src/main/js/components/controls/RadioToggle.tsx
server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap
server/sonar-web/src/main/less/init/misc.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

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 (file)
index 0000000..75004b3
--- /dev/null
@@ -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);
+}
index 69cbec3e6ddcec1bfcb990820d0c2bce1bf6d19a..e307778eec42e796f2470cc9b7b8cd49c253501a 100644 (file)
@@ -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">
index d70451a6f1d1085d637030eaf0f7b16b0608bc8a..62c113f24bb16ade30727fac5ffa1be37d4d449f 100644 (file)
@@ -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>
 `;
index 06dc6283e046ee663b6235c2e1f49c5ce81a19f1..b71489f5e8665ac89a40e6e56f6e750e74c64797 100644 (file)
@@ -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 (file)
index 0000000..0cb9096
--- /dev/null
@@ -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 (file)
index 0000000..4319d2b
--- /dev/null
@@ -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 (file)
index 0000000..d5b950d
--- /dev/null
@@ -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 (file)
index 0000000..8dbfd37
--- /dev/null
@@ -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 (file)
index 0000000..921afee
--- /dev/null
@@ -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 (file)
index 0000000..d14cc83
--- /dev/null
@@ -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 (file)
index 0000000..26c1211
--- /dev/null
@@ -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 (file)
index 0000000..21690e4
--- /dev/null
@@ -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 (file)
index 0000000..ec87c9e
--- /dev/null
@@ -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 (file)
index 0000000..163c816
--- /dev/null
@@ -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 (file)
index 0000000..8de1f3b
--- /dev/null
@@ -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 (file)
index 0000000..8520241
--- /dev/null
@@ -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 (file)
index 0000000..550ba92
--- /dev/null
@@ -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 (file)
index 0000000..505ebd1
--- /dev/null
@@ -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 (file)
index 0000000..298206f
--- /dev/null
@@ -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 (file)
index 0000000..53c154e
--- /dev/null
@@ -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 (file)
index 0000000..f4d9d19
--- /dev/null
@@ -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 (file)
index 0000000..b662651
--- /dev/null
@@ -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 (file)
index 0000000..0e51d15
--- /dev/null
@@ -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 (file)
index 0000000..194427a
--- /dev/null
@@ -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 (file)
index 0000000..e066c3a
--- /dev/null
@@ -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 (file)
index 0000000..780fafb
--- /dev/null
@@ -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
+  })
+);
index 1e5afe851954e76acb898302535fd0ec94df49ef..f098677221354ff22a18ee47743eec33ae304e7a 100644 (file)
@@ -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;
index bfb7c4cfee1994f0e0c74b47e5b7e77ac27995b2..8c4fe83ff200ee507d26640af33f6cd91e0b6408 100644 (file)
  * 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>
     );
   };
index f1a231b564c0e36cdbc53a03a1e312c9f94a95ef..90b5659e124e76ed7335e6382c15a125a191ded4 100644 (file)
@@ -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} />;
index df9579888bd6e0e501a4645b2ff7c5db809e7073..c0aa0d1ab89d2b3ccd96d8f2da3748870fb82a5b 100644 (file)
@@ -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"
index c927347bea701c11408a8bd9c3e101811ea2c040..cff3da4fdf067e748aa8a283a9e13d554eb90452 100644 (file)
@@ -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;
 }
index 3403e48bacb8215db00807de0f922c2719ddb3ba..4b66f3917df93c0996aea2bf0e54b18162b328c0 100644 (file)
@@ -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
 
 #------------------------------------------------------------------------------
 #