@@ -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 { checkStatus, corsRequest, getJSON, parseJSON } from '../helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export interface Edition { | |||
name: string; | |||
desc: string; | |||
more_link: string; | |||
request_license_link: string; | |||
download_link: string; | |||
} | |||
export interface Editions { | |||
[key: string]: Edition; | |||
} | |||
export interface EditionStatus { | |||
currentEditionKey?: string; | |||
nextEditionKey?: string; | |||
installationStatus: | |||
| 'NONE' | |||
| 'AUTOMATIC_IN_PROGRESS' | |||
| 'MANUAL_IN_PROGRESS' | |||
| 'AUTOMATIC_READY' | |||
| 'AUTOMATIC_FAILURE'; | |||
} | |||
export function getEditionStatus(): Promise<EditionStatus> { | |||
return getJSON('/api/editions/status').catch(throwGlobalError); | |||
} | |||
export function getEditionsList(): Promise<Editions> { | |||
// TODO Replace with real url | |||
const url = | |||
'https://gist.githubusercontent.com/gregaubert/e34535494f8a94bec7cbc4d750ae7d06/raw/ba8670a28d4bc6fbac18f92e450ec42029cc5dcb/editions.json'; | |||
return corsRequest(url) | |||
.submit() | |||
.then(checkStatus) | |||
.then(parseJSON); | |||
} |
@@ -17,35 +17,56 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import Helmet from 'react-helmet'; | |||
import { connect } from 'react-redux'; | |||
import SettingsNav from './nav/settings/SettingsNav'; | |||
import { getAppState } from '../../store/rootReducer'; | |||
import { onFail } from '../../store/rootActions'; | |||
import { getSettingsNavigation } from '../../api/nav'; | |||
import { setAdminPages } from '../../store/appState/duck'; | |||
import { EditionStatus, getEditionStatus } from '../../api/marketplace'; | |||
import { setAdminPages, setEditionStatus } from '../../store/appState/duck'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { Extension } from '../types'; | |||
interface Props { | |||
appState: { | |||
adminPages: Extension[]; | |||
editionStatus?: EditionStatus; | |||
organizationsEnabled: boolean; | |||
}; | |||
location: {}; | |||
setAdminPages: (adminPages: Extension[]) => void; | |||
setEditionStatus: (editionStatus: EditionStatus) => void; | |||
} | |||
class AdminContainer extends React.PureComponent<Props> { | |||
static contextTypes = { | |||
canAdmin: PropTypes.bool.isRequired | |||
}; | |||
class AdminContainer extends React.PureComponent { | |||
componentDidMount() { | |||
if (!this.props.appState.canAdmin) { | |||
if (!this.context.canAdmin) { | |||
// workaround cyclic dependencies | |||
const handleRequiredAuthorization = require('../utils/handleRequiredAuthorization').default; | |||
handleRequiredAuthorization(); | |||
} else { | |||
this.loadData(); | |||
} | |||
this.loadData(); | |||
} | |||
loadData() { | |||
getSettingsNavigation().then( | |||
r => this.props.setAdminPages(r.extensions), | |||
onFail(this.props.dispatch) | |||
Promise.all([getSettingsNavigation(), getEditionStatus()]).then( | |||
([r, editionStatus]) => { | |||
this.props.setAdminPages(r.extensions); | |||
this.props.setEditionStatus(editionStatus); | |||
}, | |||
() => {} | |||
); | |||
} | |||
render() { | |||
const { adminPages } = this.props.appState; | |||
const { adminPages, editionStatus, organizationsEnabled } = this.props.appState; | |||
// Check that the adminPages are loaded | |||
if (!adminPages) { | |||
@@ -57,17 +78,22 @@ class AdminContainer extends React.PureComponent { | |||
return ( | |||
<div> | |||
<Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} /> | |||
<SettingsNav location={this.props.location} extensions={adminPages} /> | |||
<SettingsNav | |||
customOrganizations={organizationsEnabled} | |||
editionStatus={editionStatus} | |||
extensions={adminPages} | |||
location={this.props.location} | |||
/> | |||
{this.props.children} | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = state => ({ | |||
const mapStateToProps = (state: any) => ({ | |||
appState: getAppState(state) | |||
}); | |||
const mapDispatchToProps = { setAdminPages }; | |||
const mapDispatchToProps = { setAdminPages, setEditionStatus }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer); | |||
export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer as any); |
@@ -21,7 +21,7 @@ import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import * as classNames from 'classnames'; | |||
import * as PropTypes from 'prop-types'; | |||
import { Branch, Component, ComponentExtension } from '../../../types'; | |||
import { Branch, Component, Extension } from '../../../types'; | |||
import NavBarTabs from '../../../../components/nav/NavBarTabs'; | |||
import { | |||
isShortLivingBranch, | |||
@@ -419,7 +419,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
); | |||
} | |||
renderExtension = ({ key, name }: ComponentExtension, isAdmin: boolean) => { | |||
renderExtension = ({ key, name }: Extension, isAdmin: boolean) => { | |||
const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`; | |||
return ( | |||
<li key={key}> |
@@ -0,0 +1,60 @@ | |||
/* | |||
* 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 NavBarNotif from '../../../../components/nav/NavBarNotif'; | |||
import { EditionStatus } from '../../../../api/marketplace'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
interface Props { | |||
editionStatus: EditionStatus; | |||
} | |||
export default class SettingsEditionsNotif extends React.PureComponent<Props> { | |||
render() { | |||
const { editionStatus } = this.props; | |||
if (editionStatus.installationStatus === 'AUTOMATIC_IN_PROGRESS') { | |||
return ( | |||
<NavBarNotif className="alert alert-info"> | |||
<i className="spinner spacer-right text-bottom" /> | |||
<span>{translate('marketplace.status.AUTOMATIC_IN_PROGRESS')}</span> | |||
</NavBarNotif> | |||
); | |||
} else if (editionStatus.installationStatus === 'AUTOMATIC_READY') { | |||
return ( | |||
<NavBarNotif className="alert alert-success"> | |||
<span>{translate('marketplace.status.AUTOMATIC_READY')}</span> | |||
</NavBarNotif> | |||
); | |||
} else if ( | |||
['MANUAL_IN_PROGRESS', 'AUTOMATIC_FAILURE'].includes(editionStatus.installationStatus) | |||
) { | |||
return ( | |||
<NavBarNotif className="alert alert-danger"> | |||
{translate('marketplace.status', editionStatus.installationStatus)} | |||
<a className="little-spacer-left" href="https://www.sonarsource.com" target="_blank"> | |||
{translate('marketplace.how_to_install')} | |||
</a> | |||
</NavBarNotif> | |||
); | |||
} | |||
return null; | |||
} | |||
} |
@@ -17,23 +17,31 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { IndexLink, Link } from 'react-router'; | |||
import { connect } from 'react-redux'; | |||
import ContextNavBar from '../../../../components/nav/ContextNavBar'; | |||
import SettingsEditionsNotif from './SettingsEditionsNotif'; | |||
import NavBarTabs from '../../../../components/nav/NavBarTabs'; | |||
import { EditionStatus } from '../../../../api/marketplace'; | |||
import { Extension } from '../../../types'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { areThereCustomOrganizations } from '../../../../store/rootReducer'; | |||
class SettingsNav extends React.PureComponent { | |||
interface Props { | |||
editionStatus?: EditionStatus; | |||
extensions: Extension[]; | |||
customOrganizations: boolean; | |||
location: {}; | |||
} | |||
export default class SettingsNav extends React.PureComponent<Props> { | |||
static defaultProps = { | |||
extensions: [] | |||
}; | |||
isSomethingActive(urls) { | |||
isSomethingActive(urls: string[]): boolean { | |||
const path = window.location.pathname; | |||
return urls.some(url => path.indexOf(window.baseUrl + url) === 0); | |||
return urls.some((url: string) => path.indexOf((window as any).baseUrl + url) === 0); | |||
} | |||
isSecurityActive() { | |||
@@ -56,7 +64,7 @@ class SettingsNav extends React.PureComponent { | |||
return this.isSomethingActive(urls); | |||
} | |||
renderExtension = ({ key, name }) => { | |||
renderExtension = ({ key, name }: Extension) => { | |||
return ( | |||
<li key={key}> | |||
<Link to={`/admin/extension/${key}`} activeClassName="active"> | |||
@@ -67,6 +75,7 @@ class SettingsNav extends React.PureComponent { | |||
}; | |||
render() { | |||
const { customOrganizations, editionStatus, extensions } = this.props; | |||
const isSecurity = this.isSecurityActive(); | |||
const isProjects = this.isProjectsActive(); | |||
const isSystem = this.isSystemActive(); | |||
@@ -79,14 +88,21 @@ class SettingsNav extends React.PureComponent { | |||
active: !isSecurity && !isProjects && !isSystem && !isSupport | |||
}); | |||
const extensionsWithoutSupport = this.props.extensions.filter( | |||
const extensionsWithoutSupport = extensions.filter( | |||
extension => extension.key !== 'license/support' | |||
); | |||
const hasSupportExtension = extensionsWithoutSupport.length < this.props.extensions.length; | |||
const hasSupportExtension = extensionsWithoutSupport.length < extensions.length; | |||
let notifComponent; | |||
if (editionStatus && editionStatus.installationStatus !== 'NONE') { | |||
notifComponent = <SettingsEditionsNotif editionStatus={editionStatus} />; | |||
} | |||
return ( | |||
<ContextNavBar id="context-navigation" height={65}> | |||
<ContextNavBar | |||
id="context-navigation" | |||
height={notifComponent ? 95 : 65} | |||
notif={notifComponent}> | |||
<h1 className="navbar-context-header"> | |||
<strong>{translate('layout.settings')}</strong> | |||
</h1> | |||
@@ -130,21 +146,21 @@ class SettingsNav extends React.PureComponent { | |||
{translate('users.page')} | |||
</IndexLink> | |||
</li> | |||
{!this.props.customOrganizations && ( | |||
{!customOrganizations && ( | |||
<li> | |||
<IndexLink to="/admin/groups" activeClassName="active"> | |||
{translate('user_groups.page')} | |||
</IndexLink> | |||
</li> | |||
)} | |||
{!this.props.customOrganizations && ( | |||
{!customOrganizations && ( | |||
<li> | |||
<IndexLink to="/admin/permissions" activeClassName="active"> | |||
{translate('global_permissions.page')} | |||
</IndexLink> | |||
</li> | |||
)} | |||
{!this.props.customOrganizations && ( | |||
{!customOrganizations && ( | |||
<li> | |||
<IndexLink to="/admin/permission_templates" activeClassName="active"> | |||
{translate('permission_templates')} | |||
@@ -159,7 +175,7 @@ class SettingsNav extends React.PureComponent { | |||
{translate('sidebar.projects')} <i className="icon-dropdown" /> | |||
</a> | |||
<ul className="dropdown-menu"> | |||
{!this.props.customOrganizations && ( | |||
{!customOrganizations && ( | |||
<li> | |||
<IndexLink to="/admin/projects_management" activeClassName="active"> | |||
{translate('management')} | |||
@@ -210,11 +226,3 @@ class SettingsNav extends React.PureComponent { | |||
); | |||
} | |||
} | |||
const mapStateToProps = state => ({ | |||
customOrganizations: areThereCustomOrganizations(state) | |||
}); | |||
export default connect(mapStateToProps)(SettingsNav); | |||
export const UnconnectedSettingsNav = SettingsNav; |
@@ -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 { shallow } from 'enzyme'; | |||
import SettingsEditionsNotif from '../SettingsEditionsNotif'; | |||
it('should display an in progress notif', () => { | |||
const wrapper = shallow( | |||
<SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should display an error notification', () => { | |||
const wrapper = shallow( | |||
<SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_FAILURE' }} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should display a ready notification', () => { | |||
const wrapper = shallow( | |||
<SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_READY' }} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -17,12 +17,31 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { UnconnectedSettingsNav } from '../SettingsNav'; | |||
import SettingsNav from '../SettingsNav'; | |||
it('should work with extensions', () => { | |||
const extensions = [{ key: 'foo', name: 'Foo' }]; | |||
const wrapper = shallow(<UnconnectedSettingsNav extensions={extensions} />); | |||
const wrapper = shallow( | |||
<SettingsNav | |||
customOrganizations={false} | |||
editionStatus={{ installationStatus: 'NONE' }} | |||
extensions={extensions} | |||
location={{}} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should display an edition notification', () => { | |||
const wrapper = shallow( | |||
<SettingsNav | |||
customOrganizations={false} | |||
editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }} | |||
extensions={[]} | |||
location={{}} | |||
/> | |||
); | |||
expect({ ...wrapper.find('ContextNavBar').props(), children: [] }).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,39 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display a ready notification 1`] = ` | |||
<NavBarNotif | |||
className="alert alert-success" | |||
> | |||
<span> | |||
marketplace.status.AUTOMATIC_READY | |||
</span> | |||
</NavBarNotif> | |||
`; | |||
exports[`should display an error notification 1`] = ` | |||
<NavBarNotif | |||
className="alert alert-danger" | |||
> | |||
marketplace.status.AUTOMATIC_FAILURE | |||
<a | |||
className="little-spacer-left" | |||
href="https://www.sonarsource.com" | |||
target="_blank" | |||
> | |||
marketplace.how_to_install | |||
</a> | |||
</NavBarNotif> | |||
`; | |||
exports[`should display an in progress notif 1`] = ` | |||
<NavBarNotif | |||
className="alert alert-info" | |||
> | |||
<i | |||
className="spinner spacer-right text-bottom" | |||
/> | |||
<span> | |||
marketplace.status.AUTOMATIC_IN_PROGRESS | |||
</span> | |||
</NavBarNotif> | |||
`; |
@@ -1,5 +1,20 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display an edition notification 1`] = ` | |||
Object { | |||
"children": Array [], | |||
"height": 95, | |||
"id": "context-navigation", | |||
"notif": <SettingsEditionsNotif | |||
editionStatus={ | |||
Object { | |||
"installationStatus": "AUTOMATIC_IN_PROGRESS", | |||
} | |||
} | |||
/>, | |||
} | |||
`; | |||
exports[`should work with extensions 1`] = ` | |||
<ContextNavBar | |||
height={65} |
@@ -57,7 +57,7 @@ export interface ShortLivingBranch { | |||
export type Branch = MainBranch | LongLivingBranch | ShortLivingBranch; | |||
export interface ComponentExtension { | |||
export interface Extension { | |||
key: string; | |||
name: string; | |||
} | |||
@@ -71,7 +71,7 @@ export interface Component { | |||
}>; | |||
configuration?: ComponentConfiguration; | |||
description?: string; | |||
extensions?: ComponentExtension[]; | |||
extensions?: Extension[]; | |||
isFavorite?: boolean; | |||
key: string; | |||
name: string; | |||
@@ -83,7 +83,7 @@ export interface Component { | |||
} | |||
interface ComponentConfiguration { | |||
extensions?: ComponentExtension[]; | |||
extensions?: Extension[]; | |||
showBackgroundTasks?: boolean; | |||
showLinks?: boolean; | |||
showManualMeasures?: boolean; |
@@ -22,6 +22,7 @@ import * as PropTypes from 'prop-types'; | |||
import { sortBy, uniqBy } from 'lodash'; | |||
import Helmet from 'react-helmet'; | |||
import Header from './Header'; | |||
import EditionBoxes from './EditionBoxes'; | |||
import Footer from './Footer'; | |||
import PendingActions from './PendingActions'; | |||
import PluginsList from './PluginsList'; | |||
@@ -34,11 +35,13 @@ import { | |||
Plugin, | |||
PluginPending | |||
} from '../../api/plugins'; | |||
import { EditionStatus } from '../../api/marketplace'; | |||
import { RawQuery } from '../../helpers/query'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { filterPlugins, parseQuery, Query, serializeQuery } from './utils'; | |||
export interface Props { | |||
editionStatus?: EditionStatus; | |||
location: { pathname: string; query: RawQuery }; | |||
updateCenterActive: boolean; | |||
} | |||
@@ -109,7 +112,11 @@ export default class App extends React.PureComponent<Props, State> { | |||
}); | |||
} | |||
}, | |||
() => {} | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
@@ -121,7 +128,11 @@ export default class App extends React.PureComponent<Props, State> { | |||
this.setState({ loading: false, plugins }); | |||
} | |||
}, | |||
() => {} | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
@@ -154,6 +165,10 @@ export default class App extends React.PureComponent<Props, State> { | |||
<div className="page page-limited" id="marketplace-page"> | |||
<Helmet title={translate('marketplace.page')} /> | |||
<Header /> | |||
<EditionBoxes | |||
editionStatus={this.props.editionStatus} | |||
updateCenterActive={this.props.updateCenterActive} | |||
/> | |||
<PendingActions refreshPending={this.fetchPendingPlugins} pending={pending} /> | |||
<Search | |||
query={query} |
@@ -19,9 +19,11 @@ | |||
*/ | |||
import { connect } from 'react-redux'; | |||
import App from './App'; | |||
import { getGlobalSettingValue } from '../../store/rootReducer'; | |||
import { getAppState, getGlobalSettingValue } from '../../store/rootReducer'; | |||
import './style.css'; | |||
const mapStateToProps = (state: any) => ({ | |||
editionStatus: getAppState(state).editionStatus, | |||
updateCenterActive: (getGlobalSettingValue(state, 'sonar.updatecenter.activate') || {}).value | |||
}); | |||
@@ -0,0 +1,104 @@ | |||
/* | |||
* 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 EditionBox from './components/EditionBox'; | |||
import { Editions, EditionStatus, getEditionsList } from '../../api/marketplace'; | |||
import { translate } from '../../helpers/l10n'; | |||
export interface Props { | |||
editionStatus?: EditionStatus; | |||
updateCenterActive: boolean; | |||
} | |||
interface State { | |||
editions: Editions; | |||
editionsError: boolean; | |||
loading: boolean; | |||
} | |||
export default class EditionBoxes extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { editions: {}, editionsError: false, loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchEditions(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchEditions = () => { | |||
this.setState({ loading: true }); | |||
getEditionsList().then( | |||
editions => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: false, | |||
editions, | |||
editionsError: false | |||
}); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ editionsError: true, loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
render() { | |||
const { editions, loading } = this.state; | |||
if (loading) { | |||
return null; | |||
} | |||
return ( | |||
<div className="spacer-bottom marketplace-editions"> | |||
{this.state.editionsError ? ( | |||
<span className="alert alert-info"> | |||
<FormattedMessage | |||
defaultMessage={translate('marketplace.editions_unavailable')} | |||
id="marketplace.editions_unavailable" | |||
values={{ | |||
url: ( | |||
<a href="https://www.sonarsource.com" target="_blank"> | |||
SonarSource.com | |||
</a> | |||
) | |||
}} | |||
/> | |||
</span> | |||
) : ( | |||
Object.keys(editions).map(key => ( | |||
<EditionBox | |||
edition={editions[key]} | |||
editionKey={key} | |||
editionStatus={this.props.editionStatus} | |||
key={key} | |||
/> | |||
)) | |||
)} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,66 @@ | |||
/* | |||
* 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 { shallow } from 'enzyme'; | |||
import EditionBoxes from '../EditionBoxes'; | |||
import { EditionStatus } from '../../../api/marketplace'; | |||
const DEFAULT_STATUS: EditionStatus = { | |||
currentEditionKey: 'foo', | |||
nextEditionKey: '', | |||
installationStatus: 'NONE' | |||
}; | |||
it('should display the edition boxes', () => { | |||
const wrapper = getWrapper(); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ | |||
editions: { | |||
foo: { | |||
name: 'Foo', | |||
desc: 'Foo desc', | |||
download_link: 'download_url', | |||
more_link: 'more_url', | |||
request_license_link: 'license_url' | |||
}, | |||
bar: { | |||
name: 'Bar', | |||
desc: 'Bar desc', | |||
download_link: 'download_url', | |||
more_link: 'more_url', | |||
request_license_link: 'license_url' | |||
} | |||
}, | |||
loading: false | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should display an error message', () => { | |||
const wrapper = getWrapper(); | |||
wrapper.setState({ loading: false, editionsError: true }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow( | |||
<EditionBoxes editionStatus={DEFAULT_STATUS} updateCenterActive={true} {...props} /> | |||
); | |||
} |
@@ -36,14 +36,14 @@ it('should display pending actions', () => { | |||
expect(getWrapper()).toMatchSnapshot(); | |||
}); | |||
it('should not display nothing', () => { | |||
it('should not display anything', () => { | |||
expect(getWrapper({ pending: { installing: [], updating: [], removing: [] } })).toMatchSnapshot(); | |||
}); | |||
it('should open the restart form', () => { | |||
const wrapper = getWrapper(); | |||
click(wrapper.find('.js-restart')); | |||
expect(wrapper.find('RestartForm')).toHaveLength(1); | |||
expect(wrapper.find('RestartForm').exists()).toBeTruthy(); | |||
}); | |||
it('should cancel all pending and refresh them', async () => { |
@@ -0,0 +1,73 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display an error message 1`] = ` | |||
<div | |||
className="spacer-bottom marketplace-editions" | |||
> | |||
<span | |||
className="alert alert-info" | |||
> | |||
<FormattedMessage | |||
defaultMessage="marketplace.editions_unavailable" | |||
id="marketplace.editions_unavailable" | |||
values={ | |||
Object { | |||
"url": <a | |||
href="https://www.sonarsource.com" | |||
target="_blank" | |||
> | |||
SonarSource.com | |||
</a>, | |||
} | |||
} | |||
/> | |||
</span> | |||
</div> | |||
`; | |||
exports[`should display the edition boxes 1`] = `null`; | |||
exports[`should display the edition boxes 2`] = ` | |||
<div | |||
className="spacer-bottom marketplace-editions" | |||
> | |||
<EditionBox | |||
edition={ | |||
Object { | |||
"desc": "Foo desc", | |||
"download_link": "download_url", | |||
"more_link": "more_url", | |||
"name": "Foo", | |||
"request_license_link": "license_url", | |||
} | |||
} | |||
editionKey="foo" | |||
editionStatus={ | |||
Object { | |||
"currentEditionKey": "foo", | |||
"installationStatus": "NONE", | |||
"nextEditionKey": "", | |||
} | |||
} | |||
/> | |||
<EditionBox | |||
edition={ | |||
Object { | |||
"desc": "Bar desc", | |||
"download_link": "download_url", | |||
"more_link": "more_url", | |||
"name": "Bar", | |||
"request_license_link": "license_url", | |||
} | |||
} | |||
editionKey="bar" | |||
editionStatus={ | |||
Object { | |||
"currentEditionKey": "foo", | |||
"installationStatus": "NONE", | |||
"nextEditionKey": "", | |||
} | |||
} | |||
/> | |||
</div> | |||
`; |
@@ -64,4 +64,4 @@ exports[`should display pending actions 1`] = ` | |||
</div> | |||
`; | |||
exports[`should not display nothing 1`] = `null`; | |||
exports[`should not display anything 1`] = `null`; |
@@ -0,0 +1,72 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import CheckIcon from '../../../components/icons-components/CheckIcon'; | |||
import { Edition, EditionStatus } from '../../../api/marketplace'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export interface Props { | |||
edition: Edition; | |||
editionKey: string; | |||
editionStatus?: EditionStatus; | |||
} | |||
export default class EditionBox extends React.PureComponent<Props> { | |||
render() { | |||
const { edition, editionKey, editionStatus } = this.props; | |||
const isInstalled = editionStatus && editionStatus.currentEditionKey === editionKey; | |||
const isInstalling = editionStatus && editionStatus.nextEditionKey === editionKey; | |||
const installInProgress = | |||
editionStatus && editionStatus.installationStatus === 'AUTOMATIC_IN_PROGRESS'; | |||
return ( | |||
<div className="boxed-group boxed-group-inner marketplace-edition"> | |||
{isInstalled && | |||
!isInstalling && ( | |||
<span className="marketplace-edition-badge badge badge-normal-size"> | |||
<CheckIcon size={14} className="little-spacer-right text-text-top" /> | |||
{translate('marketplace.installed')} | |||
</span> | |||
)} | |||
{isInstalling && ( | |||
<span className="marketplace-edition-badge badge badge-normal-size"> | |||
{translate('marketplace.installing')} | |||
</span> | |||
)} | |||
<div> | |||
<h3 className="spacer-bottom">{edition.name}</h3> | |||
<p>{edition.desc}</p> | |||
</div> | |||
<div className="marketplace-edition-action spacer-top"> | |||
<a href={edition.more_link} target="_blank"> | |||
{translate('marketplace.learn_more')} | |||
</a> | |||
{!isInstalled && ( | |||
<button disabled={installInProgress}>{translate('marketplace.install')}</button> | |||
)} | |||
{isInstalled && ( | |||
<button className="button-red" disabled={installInProgress}> | |||
{translate('marketplace.uninstall')} | |||
</button> | |||
)} | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,100 @@ | |||
/* | |||
* 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 { shallow } from 'enzyme'; | |||
import { Edition, EditionStatus } from '../../../../api/marketplace'; | |||
import EditionBox from '../EditionBox'; | |||
const DEFAULT_STATUS: EditionStatus = { | |||
currentEditionKey: '', | |||
nextEditionKey: '', | |||
installationStatus: 'NONE' | |||
}; | |||
const DEFAULT_EDITION: Edition = { | |||
name: 'Foo', | |||
desc: 'Foo desc', | |||
download_link: 'download_url', | |||
more_link: 'more_url', | |||
request_license_link: 'license_url' | |||
}; | |||
it('should display the edition', () => { | |||
expect(getWrapper()).toMatchSnapshot(); | |||
}); | |||
it('should display installed badge', () => { | |||
expect( | |||
getWrapper({ | |||
editionStatus: { | |||
currentEditionKey: 'foo', | |||
nextEditionKey: '', | |||
installationStatus: 'NONE' | |||
} | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should display installing badge', () => { | |||
expect( | |||
getWrapper({ | |||
editionStatus: { | |||
currentEditionKey: 'foo', | |||
nextEditionKey: 'foo', | |||
installationStatus: 'NONE' | |||
} | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should disable install button', () => { | |||
expect( | |||
getWrapper({ | |||
editionStatus: { | |||
currentEditionKey: 'foo', | |||
nextEditionKey: '', | |||
installationStatus: 'AUTOMATIC_IN_PROGRESS' | |||
} | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should disable uninstall button', () => { | |||
expect( | |||
getWrapper({ | |||
editionStatus: { | |||
currentEditionKey: '', | |||
nextEditionKey: 'foo', | |||
installationStatus: 'AUTOMATIC_IN_PROGRESS' | |||
} | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow( | |||
<EditionBox | |||
edition={DEFAULT_EDITION} | |||
editionKey="foo" | |||
editionStatus={DEFAULT_STATUS} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,192 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should disable install button 1`] = ` | |||
<div | |||
className="boxed-group boxed-group-inner marketplace-edition" | |||
> | |||
<span | |||
className="marketplace-edition-badge badge badge-normal-size" | |||
> | |||
<CheckIcon | |||
className="little-spacer-right text-text-top" | |||
size={14} | |||
/> | |||
marketplace.installed | |||
</span> | |||
<div> | |||
<h3 | |||
className="spacer-bottom" | |||
> | |||
Foo | |||
</h3> | |||
<p> | |||
Foo desc | |||
</p> | |||
</div> | |||
<div | |||
className="marketplace-edition-action spacer-top" | |||
> | |||
<a | |||
href="more_url" | |||
target="_blank" | |||
> | |||
marketplace.learn_more | |||
</a> | |||
<button | |||
className="button-red" | |||
disabled={true} | |||
> | |||
marketplace.uninstall | |||
</button> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should disable uninstall button 1`] = ` | |||
<div | |||
className="boxed-group boxed-group-inner marketplace-edition" | |||
> | |||
<span | |||
className="marketplace-edition-badge badge badge-normal-size" | |||
> | |||
marketplace.installing | |||
</span> | |||
<div> | |||
<h3 | |||
className="spacer-bottom" | |||
> | |||
Foo | |||
</h3> | |||
<p> | |||
Foo desc | |||
</p> | |||
</div> | |||
<div | |||
className="marketplace-edition-action spacer-top" | |||
> | |||
<a | |||
href="more_url" | |||
target="_blank" | |||
> | |||
marketplace.learn_more | |||
</a> | |||
<button | |||
disabled={true} | |||
> | |||
marketplace.install | |||
</button> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should display installed badge 1`] = ` | |||
<div | |||
className="boxed-group boxed-group-inner marketplace-edition" | |||
> | |||
<span | |||
className="marketplace-edition-badge badge badge-normal-size" | |||
> | |||
<CheckIcon | |||
className="little-spacer-right text-text-top" | |||
size={14} | |||
/> | |||
marketplace.installed | |||
</span> | |||
<div> | |||
<h3 | |||
className="spacer-bottom" | |||
> | |||
Foo | |||
</h3> | |||
<p> | |||
Foo desc | |||
</p> | |||
</div> | |||
<div | |||
className="marketplace-edition-action spacer-top" | |||
> | |||
<a | |||
href="more_url" | |||
target="_blank" | |||
> | |||
marketplace.learn_more | |||
</a> | |||
<button | |||
className="button-red" | |||
disabled={false} | |||
> | |||
marketplace.uninstall | |||
</button> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should display installing badge 1`] = ` | |||
<div | |||
className="boxed-group boxed-group-inner marketplace-edition" | |||
> | |||
<span | |||
className="marketplace-edition-badge badge badge-normal-size" | |||
> | |||
marketplace.installing | |||
</span> | |||
<div> | |||
<h3 | |||
className="spacer-bottom" | |||
> | |||
Foo | |||
</h3> | |||
<p> | |||
Foo desc | |||
</p> | |||
</div> | |||
<div | |||
className="marketplace-edition-action spacer-top" | |||
> | |||
<a | |||
href="more_url" | |||
target="_blank" | |||
> | |||
marketplace.learn_more | |||
</a> | |||
<button | |||
className="button-red" | |||
disabled={false} | |||
> | |||
marketplace.uninstall | |||
</button> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should display the edition 1`] = ` | |||
<div | |||
className="boxed-group boxed-group-inner marketplace-edition" | |||
> | |||
<div> | |||
<h3 | |||
className="spacer-bottom" | |||
> | |||
Foo | |||
</h3> | |||
<p> | |||
Foo desc | |||
</p> | |||
</div> | |||
<div | |||
className="marketplace-edition-action spacer-top" | |||
> | |||
<a | |||
href="more_url" | |||
target="_blank" | |||
> | |||
marketplace.learn_more | |||
</a> | |||
<button | |||
disabled={false} | |||
> | |||
marketplace.install | |||
</button> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,32 @@ | |||
.marketplace-editions { | |||
display: flex; | |||
flex-direction: row; | |||
justify-content: space-between; | |||
margin-left: -8px; | |||
margin-right: -8px; | |||
} | |||
.marketplace-edition { | |||
position: relative; | |||
flex: 1; | |||
display: flex; | |||
flex-direction: column; | |||
justify-content: space-between; | |||
background-color: #f3f3f3; | |||
margin-left: 8px; | |||
margin-right: 8px; | |||
} | |||
.marketplace-edition-badge { | |||
position: absolute; | |||
right: -1px; | |||
top: 16px; | |||
padding: 4px 8px; | |||
border-radius: 2px 0 0 2px; | |||
} | |||
.marketplace-edition-action { | |||
display: flex; | |||
align-items: baseline; | |||
justify-content: space-between; | |||
} |
@@ -78,11 +78,9 @@ class Request { | |||
constructor(private url: string, private options: { method?: string } = {}) {} | |||
submit(): Promise<Response> { | |||
getSubmitData(customHeaders: any = {}): { url: string; options: RequestInit } { | |||
let url = this.url; | |||
const options: RequestInit = { ...DEFAULT_OPTIONS, ...this.options }; | |||
const customHeaders: any = {}; | |||
if (this.data) { | |||
if (this.data instanceof FormData) { | |||
@@ -100,10 +98,13 @@ class Request { | |||
options.headers = { | |||
...DEFAULT_HEADERS, | |||
...customHeaders, | |||
...getCSRFToken() | |||
...customHeaders | |||
}; | |||
return { url, options }; | |||
} | |||
submit(): Promise<Response> { | |||
const { url, options } = this.getSubmitData({ ...getCSRFToken() }); | |||
return window.fetch((window as any).baseUrl + url, options); | |||
} | |||
@@ -127,6 +128,19 @@ export function request(url: string): Request { | |||
return new Request(url); | |||
} | |||
/** | |||
* Make a cors request | |||
*/ | |||
export function corsRequest(url: string, mode: RequestMode = 'cors'): Request { | |||
const options: RequestInit = { mode }; | |||
const request = new Request(url, options); | |||
request.submit = function() { | |||
const { url, options } = this.getSubmitData(); | |||
return window.fetch(url, options); | |||
}; | |||
return request; | |||
} | |||
/** | |||
* Check that response status is ok | |||
*/ |
@@ -17,10 +17,15 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Extension } from '../../app/types'; | |||
import { EditionStatus } from '../../api/marketplace'; | |||
interface AppState { | |||
adminPages?: any[]; | |||
adminPages?: Extension[]; | |||
authenticationError: boolean; | |||
authorizationError: boolean; | |||
editionStatus?: EditionStatus; | |||
organizationsEnabled: boolean; | |||
qualifiers?: string[]; | |||
} | |||
@@ -32,14 +37,23 @@ interface SetAppStateAction { | |||
interface SetAdminPagesAction { | |||
type: 'SET_ADMIN_PAGES'; | |||
adminPages: any[]; | |||
adminPages: Extension[]; | |||
} | |||
interface SetEditionStatusAction { | |||
type: 'SET_EDITION_STATUS'; | |||
editionStatus: EditionStatus; | |||
} | |||
interface RequireAuthorizationAction { | |||
type: 'REQUIRE_AUTHORIZATION'; | |||
} | |||
export type Action = SetAppStateAction | SetAdminPagesAction | RequireAuthorizationAction; | |||
export type Action = | |||
| SetAppStateAction | |||
| SetAdminPagesAction | |||
| SetEditionStatusAction | |||
| RequireAuthorizationAction; | |||
export function setAppState(appState: AppState): SetAppStateAction { | |||
return { | |||
@@ -48,7 +62,7 @@ export function setAppState(appState: AppState): SetAppStateAction { | |||
}; | |||
} | |||
export function setAdminPages(adminPages: any[]): SetAdminPagesAction { | |||
export function setAdminPages(adminPages: Extension[]): SetAdminPagesAction { | |||
return { type: 'SET_ADMIN_PAGES', adminPages }; | |||
} | |||
@@ -56,6 +70,10 @@ export function requireAuthorization(): RequireAuthorizationAction { | |||
return { type: 'REQUIRE_AUTHORIZATION' }; | |||
} | |||
export function setEditionStatus(editionStatus: EditionStatus): SetEditionStatusAction { | |||
return { type: 'SET_EDITION_STATUS', editionStatus }; | |||
} | |||
const defaultValue: AppState = { | |||
authenticationError: false, | |||
authorizationError: false, | |||
@@ -71,6 +89,10 @@ export default function(state: AppState = defaultValue, action: Action): AppStat | |||
return { ...state, adminPages: action.adminPages }; | |||
} | |||
if (action.type === 'SET_EDITION_STATUS') { | |||
return { ...state, editionStatus: action.editionStatus }; | |||
} | |||
if (action.type === 'REQUIRE_AUTHORIZATION') { | |||
return { ...state, authorizationError: true }; | |||
} |
@@ -2069,6 +2069,7 @@ marketplace.revert=Revert | |||
marketplace.system_upgrades=System Upgrades | |||
marketplace.install=Install | |||
marketplace.installed=Installed | |||
marketplace.installing=Installing... | |||
marketplace._installed=installed | |||
marketplace.available_under_commercial_license=Available under our commercial editions | |||
marketplace.learn_more=Learn more | |||
@@ -2089,6 +2090,12 @@ marketplace.update_to_x=Update to {0} | |||
marketplace.uninstall=Uninstall | |||
marketplace.i_accept_the=I accept the | |||
marketplace.terms_and_conditions=Terms and Conditions | |||
marketplace.editions_unavailable=Explore our Editions: advanced feature packs brought to you by SonarSource on {url} | |||
marketplace.status.AUTOMATIC_IN_PROGRESS=Updating your installation... Please wait... | |||
marketplace.status.AUTOMATIC_READY=New installation complete. Please restart Server to benefit from it. | |||
marketplace.status.MANUAL_IN_PROGRESS=Can't install Developer Edition because of internet access issue. Please manually install the package in your SonarQube's plugins folder. | |||
marketplace.status.AUTOMATIC_FAILURE=Can't install Developer Edition. Please manually install the package in your SonarQube's plugins folder. | |||
marketplace.how_to_install=How to install it? | |||
#------------------------------------------------------------------------------ | |||
# |