@@ -24,7 +24,7 @@ import org.sonar.api.web.Page; | |||
public class GlobalPage implements Page { | |||
public String getId() { | |||
return "global_page"; | |||
return "uiextensionsplugin/global_page"; | |||
} | |||
public String getTitle() { |
@@ -27,7 +27,7 @@ import org.sonar.api.web.ResourceQualifier; | |||
public class ProjectPage implements Page { | |||
public String getId() { | |||
return "/project_page"; | |||
return "uiextensionsplugin/project_page"; | |||
} | |||
public String getTitle() { |
@@ -29,7 +29,7 @@ import org.sonar.api.web.UserRole; | |||
public class ResourceConfigurationPage implements Page { | |||
public String getId() { | |||
return "/resource_configuration_sample"; | |||
return "uiextensionsplugin/resource_configuration_sample"; | |||
} | |||
public String getTitle() { |
@@ -27,7 +27,7 @@ import org.sonar.api.web.UserRole; | |||
public class SettingsPage implements Page { | |||
public String getId() { | |||
return "settings_page"; | |||
return "uiextensionsplugin/settings_page"; | |||
} | |||
public String getTitle() { |
@@ -0,0 +1,4 @@ | |||
window.registerExtension('uiextensionsplugin/global_page', function (options) { | |||
options.el.textContent = 'uiextensionsplugin/global_page'; | |||
return function () {}; | |||
}); |
@@ -0,0 +1,4 @@ | |||
window.registerExtension('uiextensionsplugin/project_page', function (options) { | |||
options.el.textContent = 'uiextensionsplugin/project_page'; | |||
return function () {}; | |||
}); |
@@ -0,0 +1,4 @@ | |||
window.registerExtension('uiextensionsplugin/resource_configuration_sample', function (options) { | |||
options.el.textContent = 'uiextensionsplugin/resource_configuration_sample'; | |||
return function () {}; | |||
}); |
@@ -0,0 +1,4 @@ | |||
window.registerExtension('uiextensionsplugin/settings_page', function (options) { | |||
options.el.textContent = 'uiextensionsplugin/settings_page'; | |||
return function () {}; | |||
}); |
@@ -160,7 +160,6 @@ | |||
"react/no-render-return-value": 2, | |||
"react/no-unescaped-entities": 2, | |||
"react/no-unknown-property": 2, | |||
"react/no-unused-prop-types": 2, | |||
"react/react-in-jsx-scope": 2, | |||
"react/require-render-return": 2, | |||
"react/self-closing-comp": 2 |
@@ -20,48 +20,37 @@ | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import SettingsNav from './nav/settings/SettingsNav'; | |||
import { getCurrentUser } from '../../store/rootReducer'; | |||
import { getCurrentUser, getAppState } from '../../store/rootReducer'; | |||
import { isUserAdmin } from '../../helpers/users'; | |||
import { onFail } from '../../store/rootActions'; | |||
import { getSettingsNavigation } from '../../api/nav'; | |||
import { setAdminPages } from '../../store/appState/duck'; | |||
class AdminContainer extends React.Component { | |||
state = { | |||
loading: true | |||
}; | |||
componentDidMount () { | |||
if (!isUserAdmin(this.props.currentUser)) { | |||
// workaround cyclic dependencies | |||
const handleRequiredAuthorization = require('../utils/handleRequiredAuthorization').default; | |||
handleRequiredAuthorization(); | |||
} | |||
this.mounted = true; | |||
this.loadData(); | |||
} | |||
componentWillUnmount () { | |||
this.mounted = false; | |||
} | |||
loadData () { | |||
getSettingsNavigation().then( | |||
r => this.setState({ extensions: r.extensions, loading: false }), | |||
r => this.props.setAdminPages(r.extensions), | |||
onFail(this.props.dispatch) | |||
); | |||
} | |||
render () { | |||
if (!isUserAdmin(this.props.currentUser) || this.state.loading) { | |||
if (!isUserAdmin(this.props.currentUser) || !this.props.adminPages) { | |||
return null; | |||
} | |||
return ( | |||
<div> | |||
<SettingsNav | |||
location={this.props.location} | |||
extensions={this.state.extensions}/> | |||
<SettingsNav location={this.props.location} extensions={this.props.adminPages}/> | |||
{this.props.children} | |||
</div> | |||
); | |||
@@ -69,7 +58,10 @@ class AdminContainer extends React.Component { | |||
} | |||
const mapStateToProps = state => ({ | |||
adminPages: getAppState(state).adminPages, | |||
currentUser: getCurrentUser(state) | |||
}); | |||
export default connect(mapStateToProps)(AdminContainer); | |||
const mapDispatchToProps = { setAdminPages }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer); |
@@ -0,0 +1,66 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { getComponent } from '../../store/rootReducer'; | |||
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; | |||
class ProjectAdminContainer extends React.Component { | |||
props: { | |||
project: { | |||
configuration?: { | |||
showSettings: boolean | |||
} | |||
} | |||
}; | |||
componentDidMount () { | |||
this.checkPermissions(); | |||
} | |||
componentDidUpdate () { | |||
this.checkPermissions(); | |||
} | |||
isProjectAdmin () { | |||
const { configuration } = this.props.project; | |||
return configuration != null && configuration.showSettings; | |||
} | |||
checkPermissions () { | |||
if (!this.isProjectAdmin()) { | |||
handleRequiredAuthorization(); | |||
} | |||
} | |||
render () { | |||
if (!this.isProjectAdmin()) { | |||
return null; | |||
} | |||
return this.props.children; | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
project: getComponent(state, ownProps.location.query.id) | |||
}); | |||
export default connect(mapStateToProps)(ProjectAdminContainer); |
@@ -0,0 +1,98 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import { addGlobalErrorMessage } from '../../../store/globalMessages/duck'; | |||
import { getCurrentUser } from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getExtensionStart } from './utils'; | |||
type Props = { | |||
currentUser: Object, | |||
extension: { | |||
id: string, | |||
title: string | |||
}, | |||
onFail: (string) => void, | |||
options?: {}, | |||
router: Object | |||
}; | |||
class Extension extends React.Component { | |||
container: Object; | |||
props: Props; | |||
stop: ?Function; | |||
componentDidMount () { | |||
this.startExtension(); | |||
} | |||
componentDidUpdate (prevProps: Props) { | |||
if (prevProps.extension !== this.props.extension) { | |||
this.stopExtension(); | |||
this.startExtension(); | |||
} | |||
} | |||
componentWillUnmount () { | |||
this.stopExtension(); | |||
} | |||
handleStart = (start: Function) => { | |||
this.stop = start({ | |||
el: this.container, | |||
currentUser: this.props.currentUser, | |||
router: this.props.router, | |||
...this.props.options | |||
}); | |||
}; | |||
handleFailure = () => { | |||
this.props.onFail(translate('page_extension_failed')); | |||
}; | |||
startExtension () { | |||
const { extension } = this.props; | |||
getExtensionStart(extension.id).then(this.handleStart, this.handleFailure); | |||
} | |||
stopExtension () { | |||
this.stop && this.stop(); | |||
this.stop = null; | |||
} | |||
render () { | |||
return ( | |||
<div> | |||
<div ref={container => this.container = container}/> | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = state => ({ | |||
currentUser: getCurrentUser(state) | |||
}); | |||
const mapDispatchToProps = { onFail: addGlobalErrorMessage }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Extension)); |
@@ -0,0 +1,44 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { Link } from 'react-router'; | |||
export default class ExtensionNotFound extends React.Component { | |||
componentDidMount () { | |||
document.querySelector('html').classList.add('dashboard-page'); | |||
} | |||
componentWillUnmount () { | |||
document.querySelector('html').classList.remove('dashboard-page'); | |||
} | |||
render () { | |||
return ( | |||
<div id="bd" className="page-wrapper-simple"> | |||
<div id="nonav" className="page-simple"> | |||
<h2 className="big-spacer-bottom">The page you were looking for does not exist.</h2> | |||
<p className="spacer-bottom">You may have mistyped the address or the page may have moved.</p> | |||
<p><Link to="/">Go back to the homepage</Link></p> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,51 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import Extension from './Extension'; | |||
import ExtensionNotFound from './ExtensionNotFound'; | |||
import { getAppState } from '../../../store/rootReducer'; | |||
class GlobalAdminPageExtension extends React.Component { | |||
props: { | |||
adminPages: Array<{ id: string }>, | |||
params: { | |||
extensionKey: string, | |||
pluginKey: string | |||
} | |||
} | |||
render () { | |||
const { extensionKey, pluginKey } = this.props.params; | |||
const extension = this.props.adminPages.find(p => p.id === `${pluginKey}/${extensionKey}`); | |||
return extension ? ( | |||
<Extension extension={extension}/> | |||
) : ( | |||
<ExtensionNotFound/> | |||
); | |||
} | |||
} | |||
const mapStateToProps = state => ({ | |||
adminPages: getAppState(state).adminPages | |||
}); | |||
export default connect(mapStateToProps)(GlobalAdminPageExtension); |
@@ -0,0 +1,51 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import Extension from './Extension'; | |||
import ExtensionNotFound from './ExtensionNotFound'; | |||
import { getAppState } from '../../../store/rootReducer'; | |||
class GlobalPageExtension extends React.Component { | |||
props: { | |||
globalPages: Array<{ id: string }>, | |||
params: { | |||
extensionKey: string, | |||
pluginKey: string | |||
} | |||
} | |||
render () { | |||
const { extensionKey, pluginKey } = this.props.params; | |||
const extension = this.props.globalPages.find(p => p.id === `${pluginKey}/${extensionKey}`); | |||
return extension ? ( | |||
<Extension extension={extension}/> | |||
) : ( | |||
<ExtensionNotFound/> | |||
); | |||
} | |||
} | |||
const mapStateToProps = state => ({ | |||
globalPages: getAppState(state).globalPages | |||
}); | |||
export default connect(mapStateToProps)(GlobalPageExtension); |
@@ -0,0 +1,63 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import Extension from './Extension'; | |||
import ExtensionNotFound from './ExtensionNotFound'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import { addGlobalErrorMessage } from '../../../store/globalMessages/duck'; | |||
type Props = { | |||
component: { | |||
configuration?: { | |||
extensions: Array<{ id: string }> | |||
} | |||
}, | |||
location: { query: { id: string } }, | |||
params: { | |||
extensionKey: string, | |||
pluginKey: string | |||
} | |||
}; | |||
class ProjectAdminPageExtension extends React.Component { | |||
props: Props; | |||
render () { | |||
const { extensionKey, pluginKey } = this.props.params; | |||
const { component } = this.props; | |||
const extension = component.configuration && | |||
component.configuration.extensions.find(p => p.id === `${pluginKey}/${extensionKey}`); | |||
return extension ? ( | |||
<Extension extension={extension} options={{ component }}/> | |||
) : ( | |||
<ExtensionNotFound/> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps: Props) => ({ | |||
component: getComponent(state, ownProps.location.query.id) | |||
}); | |||
const mapDispatchToProps = { onFail: addGlobalErrorMessage }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(ProjectAdminPageExtension); |
@@ -0,0 +1,60 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import Extension from './Extension'; | |||
import ExtensionNotFound from './ExtensionNotFound'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import { addGlobalErrorMessage } from '../../../store/globalMessages/duck'; | |||
type Props = { | |||
component: { | |||
extensions: Array<{ id: string }> | |||
}, | |||
location: { query: { id: string } }, | |||
params: { | |||
extensionKey: string, | |||
pluginKey: string | |||
} | |||
}; | |||
class ProjectPageExtension extends React.Component { | |||
props: Props; | |||
render () { | |||
const { extensionKey, pluginKey } = this.props.params; | |||
const { component } = this.props; | |||
const extension = component.extensions.find(p => p.id === `${pluginKey}/${extensionKey}`); | |||
return extension ? ( | |||
<Extension extension={extension} options={{ component }}/> | |||
) : ( | |||
<ExtensionNotFound/> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps: Props) => ({ | |||
component: getComponent(state, ownProps.location.query.id) | |||
}); | |||
const mapDispatchToProps = { onFail: addGlobalErrorMessage }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(ProjectPageExtension); |
@@ -0,0 +1,32 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import ProjectPageExtension from './ProjectPageExtension'; | |||
export default class ViewDashboard extends React.Component { | |||
render () { | |||
return ( | |||
<ProjectPageExtension | |||
location={this.props.location} | |||
params={{ pluginKey: 'governance', extensionKey: 'governance' }}/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,48 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
// @flow | |||
import { getExtensionFromCache } from '../../utils/installExtensionsHandler'; | |||
const installScript = (key: string) => { | |||
return new Promise(resolve => { | |||
const scriptTag = document.createElement('script'); | |||
scriptTag.src = `${window.baseUrl}/static/${key}.js`; | |||
scriptTag.onload = resolve; | |||
document.getElementsByTagName('body')[0].appendChild(scriptTag); | |||
}); | |||
}; | |||
export const getExtensionStart = (key: string) => ( | |||
new Promise((resolve, reject) => { | |||
const fromCache = getExtensionFromCache(key); | |||
if (fromCache) { | |||
return resolve(fromCache); | |||
} | |||
installScript(key).then(() => { | |||
const start = getExtensionFromCache(key); | |||
if (start) { | |||
resolve(start); | |||
} else { | |||
reject(); | |||
} | |||
}); | |||
}) | |||
); |
@@ -30,17 +30,25 @@ import './ComponentNav.css'; | |||
export default React.createClass({ | |||
componentDidMount () { | |||
this.mounted = true; | |||
this.loadStatus(); | |||
this.populateRecentHistory(); | |||
}, | |||
componentWillUnmount () { | |||
this.mounted = false; | |||
}, | |||
loadStatus () { | |||
getTasksForComponent(this.props.component.id).then(r => { | |||
this.setState({ | |||
isPending: r.queue.some(task => task.status === STATUSES.PENDING), | |||
isInProgress: r.queue.some(task => task.status === STATUSES.IN_PROGRESS), | |||
isFailed: r.current && r.current.status === STATUSES.FAILED | |||
}); | |||
if (this.mounted) { | |||
this.setState({ | |||
isPending: r.queue.some(task => task.status === STATUSES.PENDING), | |||
isInProgress: r.queue.some(task => task.status === STATUSES.IN_PROGRESS), | |||
isFailed: r.current && r.current.status === STATUSES.FAILED | |||
}); | |||
} | |||
}); | |||
}, | |||
@@ -17,12 +17,12 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
const SETTINGS_URLS = [ | |||
'/project/admin', | |||
'/project/settings', | |||
'/project/quality_profiles', | |||
'/project/quality_gate', | |||
@@ -54,24 +54,12 @@ export default class ComponentNavMenu extends React.Component { | |||
return Object.keys(this.props.conf).some(key => this.props.conf[key]); | |||
} | |||
renderLink (url, title, highlightUrl = url) { | |||
const fullUrl = window.baseUrl + url; | |||
const isActive = typeof highlightUrl === 'string' ? | |||
window.location.pathname.indexOf(window.baseUrl + highlightUrl) === 0 : | |||
highlightUrl(fullUrl); | |||
return ( | |||
<li key={url} className={classNames({ 'active': isActive })}> | |||
<a href={fullUrl}>{title}</a> | |||
</li> | |||
); | |||
} | |||
renderDashboardLink () { | |||
const pathname = this.isView() ? '/view' : '/dashboard'; | |||
return ( | |||
<li> | |||
<Link | |||
to={{ pathname: '/dashboard', query: { id: this.props.component.key } }} | |||
to={{ pathname, query: { id: this.props.component.key } }} | |||
activeClassName="active"> | |||
<i className="icon-home"/> | |||
</Link> | |||
@@ -96,6 +84,10 @@ export default class ComponentNavMenu extends React.Component { | |||
} | |||
renderActivityLink () { | |||
if (this.isView() || this.isDeveloper()) { | |||
return null; | |||
} | |||
return ( | |||
<li> | |||
<Link to={{ pathname: '/project/activity', query: { id: this.props.component.key } }} | |||
@@ -296,11 +288,11 @@ export default class ComponentNavMenu extends React.Component { | |||
); | |||
} | |||
renderExtension = ({ id, name }) => { | |||
renderExtension = ({ id, name }, isAdmin = false) => { | |||
const pathname = isAdmin ? `/project/admin/extension/${id}` : `/project/extension/${id}`; | |||
return ( | |||
<li key={id}> | |||
<Link to={{ pathname: `/project/extension/${id}`, query: { id: this.props.component.key } }} | |||
activeClassName="active"> | |||
<Link to={{ pathname, query: { id: this.props.component.key } }} activeClassName="active"> | |||
{name} | |||
</Link> | |||
</li> | |||
@@ -309,7 +301,7 @@ export default class ComponentNavMenu extends React.Component { | |||
renderExtensions () { | |||
const extensions = this.props.conf.extensions || []; | |||
return extensions.map(this.renderExtension); | |||
return extensions.map(e => this.renderExtension(e, true)); | |||
} | |||
renderTools () { |
@@ -118,7 +118,7 @@ exports[`test should work with extensions 1`] = ` | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/extension/foo", | |||
"pathname": "/project/admin/extension/foo", | |||
"query": Object { | |||
"id": "foo", | |||
}, |
@@ -21,6 +21,7 @@ import configureLocale from './utils/configureLocale'; | |||
import exposeLibraries from './utils/exposeLibraries'; | |||
import startAjaxMonitoring from './utils/startAjaxMonitoring'; | |||
import startReactApp from './utils/startReactApp'; | |||
import installExtensionsHandler from './utils/installExtensionsHandler'; | |||
import { installGlobal } from '../helpers/l10n'; | |||
import './styles/index'; | |||
@@ -39,3 +40,4 @@ startAjaxMonitoring(); | |||
installGlobal(); | |||
startReactApp(); | |||
exposeLibraries(); | |||
installExtensionsHandler(); |
@@ -0,0 +1,33 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
// @flow | |||
const extensions = {}; | |||
const registerExtension = (key: string, start: Function) => { | |||
extensions[key] = start; | |||
}; | |||
export default () => { | |||
window.registerExtension = registerExtension; | |||
}; | |||
export const getExtensionFromCache = (key: string) => { | |||
return extensions[key]; | |||
}; |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import React from 'react'; | |||
import { render } from 'react-dom'; | |||
import { Router, Route, IndexRoute } from 'react-router'; | |||
import { Router, Route, IndexRoute, Redirect } from 'react-router'; | |||
import { Provider } from 'react-redux'; | |||
import LocalizationContainer from '../components/LocalizationContainer'; | |||
import MigrationContainer from '../components/MigrationContainer'; | |||
@@ -28,7 +28,13 @@ import GlobalContainer from '../components/GlobalContainer'; | |||
import SimpleContainer from '../components/SimpleContainer'; | |||
import Landing from '../components/Landing'; | |||
import ProjectContainer from '../components/ProjectContainer'; | |||
import ProjectAdminContainer from '../components/ProjectAdminContainer'; | |||
import ProjectPageExtension from '../components/extensions/ProjectPageExtension'; | |||
import ProjectAdminPageExtension from '../components/extensions/ProjectAdminPageExtension'; | |||
import ViewDashboard from '../components/extensions/ViewDashboard'; | |||
import AdminContainer from '../components/AdminContainer'; | |||
import GlobalPageExtension from '../components/extensions/GlobalPageExtension'; | |||
import GlobalAdminPageExtension from '../components/extensions/GlobalAdminPageExtension'; | |||
import MarkdownHelp from '../components/MarkdownHelp'; | |||
import NotFound from '../components/NotFound'; | |||
import aboutRoutes from '../../apps/about/routes'; | |||
@@ -97,6 +103,7 @@ const startReactApp = () => { | |||
<Route path="account">{accountRoutes}</Route> | |||
<Route path="coding_rules">{codingRulesRoutes}</Route> | |||
<Route path="component">{componentRoutes}</Route> | |||
<Route path="extension/:pluginKey/:extensionKey" component={GlobalPageExtension}/> | |||
<Route path="issues">{issuesRoutes}</Route> | |||
<Route path="projects">{projectsRoutes}</Route> | |||
<Route path="quality_gates">{qualityGatesRoutes}</Route> | |||
@@ -109,16 +116,24 @@ const startReactApp = () => { | |||
<Route path="component_measures">{componentMeasuresRoutes}</Route> | |||
<Route path="custom_measures">{customMeasuresRoutes}</Route> | |||
<Route path="dashboard">{overviewRoutes}</Route> | |||
<Redirect from="governance" to="/view"/> | |||
<Route path="project"> | |||
<Route path="activity">{projectActivityRoutes}</Route> | |||
<Route path="admin" component={ProjectAdminContainer}> | |||
<Route path="extension/:pluginKey/:extensionKey" component={ProjectAdminPageExtension}/> | |||
</Route> | |||
<Redirect from="extension/governance/governance" to="/view"/> | |||
<Route path="extension/:pluginKey/:extensionKey" component={ProjectPageExtension}/> | |||
<Route path="background_tasks">{backgroundTasksRoutes}</Route> | |||
<Route path="settings">{settingsRoutes}</Route> | |||
{projectAdminRoutes} | |||
</Route> | |||
<Route path="project_roles">{projectPermissionsRoutes}</Route> | |||
<Route path="view" component={ViewDashboard}/> | |||
</Route> | |||
<Route component={AdminContainer}> | |||
<Route path="admin/extension/:pluginKey/:extensionKey" component={GlobalAdminPageExtension}/> | |||
<Route path="background_tasks">{backgroundTasksRoutes}</Route> | |||
<Route path="groups">{groupsRoutes}</Route> | |||
<Route path="metrics">{metricsRoutes}</Route> |
@@ -17,19 +17,37 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import shallowCompare from 'react-addons-shallow-compare'; | |||
import { withRouter } from 'react-router'; | |||
import OverviewApp from './OverviewApp'; | |||
import EmptyOverview from './EmptyOverview'; | |||
import { ComponentType } from '../propTypes'; | |||
export default class App extends React.Component { | |||
static propTypes = { | |||
component: ComponentType.isRequired | |||
}; | |||
type Props = { | |||
component: { | |||
id: string, | |||
key: string, | |||
qualifier: string | |||
}, | |||
router: Object | |||
}; | |||
shouldComponentUpdate (nextProps, nextState) { | |||
return shallowCompare(this, nextProps, nextState); | |||
class App extends React.Component { | |||
props: Props; | |||
state: Object; | |||
componentDidMount () { | |||
if (['VW', 'SVW'].includes(this.props.component.qualifier)) { | |||
this.props.router.replace({ | |||
pathname: '/view', | |||
query: { id: this.props.component.key } | |||
}); | |||
} | |||
} | |||
shouldComponentUpdate (nextProps: Props) { | |||
return shallowCompare(this, nextProps); | |||
} | |||
render () { | |||
@@ -55,3 +73,7 @@ export default class App extends React.Component { | |||
); | |||
} | |||
} | |||
export default withRouter(App); | |||
export const UnconnectedApp = App; |
@@ -19,24 +19,24 @@ | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import App from '../App'; | |||
import { UnconnectedApp } from '../App'; | |||
import OverviewApp from '../OverviewApp'; | |||
import EmptyOverview from '../EmptyOverview'; | |||
it('should render OverviewApp', () => { | |||
const component = { id: 'id', snapshotDate: '2016-01-01' }; | |||
const output = shallow(<App component={component}/>); | |||
const output = shallow(<UnconnectedApp component={component}/>); | |||
expect(output.type()).toBe(OverviewApp); | |||
}); | |||
it('should render EmptyOverview', () => { | |||
const component = { id: 'id' }; | |||
const output = shallow(<App component={component}/>); | |||
const output = shallow(<UnconnectedApp component={component}/>); | |||
expect(output.type()).toBe(EmptyOverview); | |||
}); | |||
it('should pass leakPeriodIndex', () => { | |||
const component = { id: 'id', snapshotDate: '2016-01-01' }; | |||
const output = shallow(<App component={component}/>); | |||
const output = shallow(<UnconnectedApp component={component}/>); | |||
expect(output.prop('leakPeriodIndex')).toBe('1'); | |||
}); |
@@ -39,8 +39,6 @@ const Meta = ({ component, measures }) => { | |||
const shouldShowQualityProfiles = !isView && !isDeveloper && hasQualityProfiles; | |||
const shouldShowQualityGate = !isView && !isDeveloper && hasQualityGate; | |||
const showShowAnalyses = isProject || isView || isDeveloper; | |||
return ( | |||
<div className="overview-meta"> | |||
{hasDescription && ( | |||
@@ -63,7 +61,7 @@ const Meta = ({ component, measures }) => { | |||
<MetaKey component={component}/> | |||
{showShowAnalyses && ( | |||
{isProject && ( | |||
<AnalysesList project={component.key}/> | |||
)} | |||
</div> |
@@ -30,7 +30,6 @@ import './projectActivity.css'; | |||
type Props = { | |||
location: { query: { id: string } }, | |||
fetchProjectActivity: (project: string) => void, | |||
/* eslint-disable react/no-unused-prop-types */ | |||
project: { configuration?: { showHistory: boolean } } | |||
}; | |||
@@ -26,7 +26,6 @@ import { ProfileType } from '../propTypes'; | |||
export default class ProfileDetails extends React.Component { | |||
static propTypes = { | |||
/* eslint-disable react/no-unused-prop-types */ | |||
profile: ProfileType, | |||
canAdmin: React.PropTypes.bool, | |||
updateProfiles: React.PropTypes.func |
@@ -26,7 +26,6 @@ import { TYPE_PROPERTY_SET } from '../../constants'; | |||
export default class Input extends React.Component { | |||
static propTypes = { | |||
/* eslint-disable react/no-unused-prop-types */ | |||
setting: React.PropTypes.object.isRequired, | |||
value: React.PropTypes.any, | |||
onChange: React.PropTypes.func.isRequired |
@@ -20,7 +20,6 @@ | |||
// @flow | |||
import keyBy from 'lodash/keyBy'; | |||
import { RECEIVE_VALUES } from './actions'; | |||
import { actions as appStateActions } from '../../../../store/appState/duck'; | |||
type State = { [key: string]: {} }; | |||
@@ -30,7 +29,7 @@ const reducer = (state: State = {}, action: Object) => { | |||
return { ...state, ...settingsByKey }; | |||
} | |||
if (action.type === appStateActions.SET_APP_STATE) { | |||
if (action.type === 'SET_APP_STATE') { | |||
const settingsByKey = {}; | |||
Object.keys(action.appState.settings).forEach(key => ( | |||
settingsByKey[key] = { value: action.appState.settings[key] } |
@@ -24,7 +24,6 @@ import './styles.css'; | |||
export default class DateInput extends React.Component { | |||
static propTypes = { | |||
/* eslint-disable react/no-unused-prop-types */ | |||
value: React.PropTypes.string, | |||
format: React.PropTypes.string, | |||
name: React.PropTypes.string, |
@@ -22,7 +22,6 @@ import Item from './item'; | |||
export default React.createClass({ | |||
propTypes: { | |||
/* eslint-disable react/no-unused-prop-types */ | |||
items: React.PropTypes.array.isRequired, | |||
renderItem: React.PropTypes.func.isRequired, | |||
getItemKey: React.PropTypes.func.isRequired, |
@@ -24,7 +24,6 @@ import Footer from './footer'; | |||
export default React.createClass({ | |||
propTypes: { | |||
/* eslint-disable react/no-unused-prop-types */ | |||
loadItems: React.PropTypes.func.isRequired, | |||
renderItem: React.PropTypes.func.isRequired, | |||
getItemKey: React.PropTypes.func.isRequired, |
@@ -19,28 +19,36 @@ | |||
*/ | |||
// @flow | |||
type AppState = { | |||
adminPages?: Array<*>, | |||
authenticationError: boolean, | |||
authorizationError: boolean, | |||
qualifiers: ?Array<string> | |||
}; | |||
export type Action = { | |||
type: string, | |||
type SetAppStateAction = { | |||
type: 'SET_APP_STATE', | |||
appState: AppState | |||
}; | |||
export const actions = { | |||
SET_APP_STATE: 'SET_APP_STATE', | |||
REQUIRE_AUTHORIZATION: 'REQUIRE_AUTHORIZATION' | |||
type SetAdminPagesAction = { | |||
type: 'SET_ADMIN_PAGES', | |||
adminPages: Array<*> | |||
}; | |||
export const setAppState = (appState: AppState): Action => ({ | |||
type: actions.SET_APP_STATE, | |||
export type Action = SetAppStateAction | SetAdminPagesAction; | |||
export const setAppState = (appState: AppState): SetAppStateAction => ({ | |||
type: 'SET_APP_STATE', | |||
appState | |||
}); | |||
export const setAdminPages = (adminPages: Array<*>): SetAdminPagesAction => ({ | |||
type: 'SET_ADMIN_PAGES', | |||
adminPages | |||
}); | |||
export const requireAuthorization = () => ({ | |||
type: actions.REQUIRE_AUTHORIZATION | |||
type: 'REQUIRE_AUTHORIZATION' | |||
}); | |||
const defaultValue = { | |||
@@ -50,11 +58,15 @@ const defaultValue = { | |||
}; | |||
export default (state: AppState = defaultValue, action: Action) => { | |||
if (action.type === actions.SET_APP_STATE) { | |||
if (action.type === 'SET_APP_STATE') { | |||
return { ...state, ...action.appState }; | |||
} | |||
if (action.type === actions.REQUIRE_AUTHORIZATION) { | |||
if (action.type === 'SET_ADMIN_PAGES') { | |||
return { ...state, adminPages: action.adminPages }; | |||
} | |||
if (action.type === 'REQUIRE_AUTHORIZATION') { | |||
return { ...state, authorizationError: true }; | |||
} | |||
@@ -19,7 +19,6 @@ | |||
*/ | |||
// @flow | |||
import uniqueId from 'lodash/uniqueId'; | |||
import { actions } from '../appState/duck'; | |||
type Level = 'ERROR' | 'SUCCESS'; | |||
@@ -80,7 +79,7 @@ const globalMessages = (state: State = [], action: Action = {}) => { | |||
level: action.level | |||
}]; | |||
case actions.REQUIRE_AUTHORIZATION: | |||
case 'REQUIRE_AUTHORIZATION': | |||
// FIXME l10n | |||
return [{ | |||
id: uniqueId('global-message-'), |
@@ -256,6 +256,7 @@ over_x_days.short={0} days | |||
over_x_days_detailed=over {0} days ({1}) | |||
over_x_days_detailed.short={0} days ({1}) | |||
page_size=Page size | |||
page_extension_failed=Page extension failed. | |||
paging_first=First | |||
paging_last=Last | |||
paging_next=Next |