@@ -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); | |||
} |
@@ -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"> |
@@ -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> | |||
`; |
@@ -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> |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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; |
@@ -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 | |||
}) | |||
); |
@@ -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,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> | |||
); | |||
}; |
@@ -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} />; |
@@ -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" |
@@ -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; | |||
} |
@@ -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 | |||
#------------------------------------------------------------------------------ | |||
# |