Browse Source

SONAR-9935 Migrate Update center to Marketplace

tags/6.7-RC1
Grégoire Aubert 6 years ago
parent
commit
847b1833f8
32 changed files with 1803 additions and 5 deletions
  1. 168
    0
      server/sonar-web/src/main/js/api/plugins.ts
  2. 6
    0
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js
  3. 8
    0
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap
  4. 2
    0
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  5. 173
    0
      server/sonar-web/src/main/js/apps/marketplace/App.tsx
  6. 28
    0
      server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
  7. 33
    0
      server/sonar-web/src/main/js/apps/marketplace/Footer.tsx
  8. 30
    0
      server/sonar-web/src/main/js/apps/marketplace/Header.tsx
  9. 120
    0
      server/sonar-web/src/main/js/apps/marketplace/PendingActions.tsx
  10. 127
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginActions.tsx
  11. 75
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginAvailable.tsx
  12. 54
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginChangeLog.tsx
  13. 67
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogButton.tsx
  14. 58
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogItem.tsx
  15. 54
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginDescription.tsx
  16. 83
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginInstalled.tsx
  17. 43
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginLicense.tsx
  18. 50
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginOrganization.tsx
  19. 54
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginStatus.tsx
  20. 44
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginUpdateButton.tsx
  21. 72
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginUpdateItem.tsx
  22. 50
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginUpdates.tsx
  23. 91
    0
      server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx
  24. 98
    0
      server/sonar-web/src/main/js/apps/marketplace/Search.tsx
  25. 30
    0
      server/sonar-web/src/main/js/apps/marketplace/routes.ts
  26. 64
    0
      server/sonar-web/src/main/js/apps/marketplace/utils.ts
  27. 1
    1
      server/sonar-web/src/main/js/components/controls/Checkbox.tsx
  28. 18
    4
      server/sonar-web/src/main/js/components/controls/RadioToggle.tsx
  29. 13
    0
      server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx
  30. 48
    0
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap
  31. 5
    0
      server/sonar-web/src/main/less/init/misc.less
  32. 36
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 168
- 0
server/sonar-web/src/main/js/api/plugins.ts View File

@@ -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);
}

+ 6
- 0
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js View 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">

+ 8
- 0
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap View 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>
`;

+ 2
- 0
server/sonar-web/src/main/js/app/utils/startReactApp.js View 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>

+ 173
- 0
server/sonar-web/src/main/js/apps/marketplace/App.tsx View File

@@ -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>
);
}
}

+ 28
- 0
server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx View File

@@ -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);

+ 33
- 0
server/sonar-web/src/main/js/apps/marketplace/Footer.tsx View File

@@ -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>
);
}

+ 30
- 0
server/sonar-web/src/main/js/apps/marketplace/Header.tsx View File

@@ -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>
);
}

+ 120
- 0
server/sonar-web/src/main/js/apps/marketplace/PendingActions.tsx View File

@@ -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>
);
}
}

+ 127
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginActions.tsx View File

@@ -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>
);
}
}

+ 75
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginAvailable.tsx View File

@@ -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>
);
}

+ 54
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginChangeLog.tsx View File

@@ -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>
);
}

+ 67
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogButton.tsx View File

@@ -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>
);
}
}

+ 58
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginChangeLogItem.tsx View File

@@ -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>
);
}

+ 54
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginDescription.tsx View File

@@ -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>
);
}
}

+ 83
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginInstalled.tsx View File

@@ -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>
);
}

+ 43
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginLicense.tsx View File

@@ -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>
);
}

+ 50
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginOrganization.tsx View File

@@ -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>
);
}

+ 54
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginStatus.tsx View File

@@ -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>
);
}

+ 44
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginUpdateButton.tsx View File

@@ -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>
);
}
}

+ 72
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginUpdateItem.tsx View File

@@ -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>
);
}
}

+ 50
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginUpdates.tsx View File

@@ -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>
);
}

+ 91
- 0
server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx View File

@@ -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>
);
}
}

+ 98
- 0
server/sonar-web/src/main/js/apps/marketplace/Search.tsx View File

@@ -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>
);
}
}

+ 30
- 0
server/sonar-web/src/main/js/apps/marketplace/routes.ts View File

@@ -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;

+ 64
- 0
server/sonar-web/src/main/js/apps/marketplace/utils.ts View File

@@ -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
})
);

+ 1
- 1
server/sonar-web/src/main/js/components/controls/Checkbox.tsx View 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;

+ 18
- 4
server/sonar-web/src/main/js/components/controls/RadioToggle.tsx View File

@@ -18,11 +18,19 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import Tooltip from './Tooltip';

interface Option {
disabled?: boolean;
label: string;
tooltip?: string;
value: string;
}

interface Props {
name: string;
onCheck: (value: string) => void;
options: Array<{ label: string; value: string }>;
options: Option[];
value?: string;
}

@@ -37,21 +45,27 @@ export default class RadioToggle extends React.PureComponent<Props> {
this.props.onCheck(newValue);
};

renderOption = (option: { label: string; value: string }) => {
renderOption = (option: Option) => {
const checked = option.value === this.props.value;
const htmlId = this.props.name + '__' + option.value;
return (
<li key={option.value}>
<input
type="radio"
disabled={option.disabled}
name={this.props.name}
value={option.value}
id={htmlId}
checked={checked}
onChange={this.handleChange}
/>

<label htmlFor={htmlId}>{option.label}</label>
{option.tooltip ? (
<Tooltip overlay={option.tooltip}>
<label htmlFor={htmlId}>{option.label}</label>
</Tooltip>
) : (
<label htmlFor={htmlId}>{option.label}</label>
)}
</li>
);
};

+ 13
- 0
server/sonar-web/src/main/js/components/controls/__tests__/RadioToggle-test.tsx View 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} />;

+ 48
- 0
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap View 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"

+ 5
- 0
server/sonar-web/src/main/less/init/misc.less View 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;
}

+ 36
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save