diff options
38 files changed, 677 insertions, 82 deletions
diff --git a/it/it-plugins/ui-extensions-plugin/src/main/java/GlobalPage.java b/it/it-plugins/ui-extensions-plugin/src/main/java/GlobalPage.java index d243da0828d..2dcbd264181 100644 --- a/it/it-plugins/ui-extensions-plugin/src/main/java/GlobalPage.java +++ b/it/it-plugins/ui-extensions-plugin/src/main/java/GlobalPage.java @@ -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() { diff --git a/it/it-plugins/ui-extensions-plugin/src/main/java/ProjectPage.java b/it/it-plugins/ui-extensions-plugin/src/main/java/ProjectPage.java index db08565862a..66da8e6ad33 100644 --- a/it/it-plugins/ui-extensions-plugin/src/main/java/ProjectPage.java +++ b/it/it-plugins/ui-extensions-plugin/src/main/java/ProjectPage.java @@ -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() { diff --git a/it/it-plugins/ui-extensions-plugin/src/main/java/ResourceConfigurationPage.java b/it/it-plugins/ui-extensions-plugin/src/main/java/ResourceConfigurationPage.java index b804359642d..953b1f9df15 100644 --- a/it/it-plugins/ui-extensions-plugin/src/main/java/ResourceConfigurationPage.java +++ b/it/it-plugins/ui-extensions-plugin/src/main/java/ResourceConfigurationPage.java @@ -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() { diff --git a/it/it-plugins/ui-extensions-plugin/src/main/java/SettingsPage.java b/it/it-plugins/ui-extensions-plugin/src/main/java/SettingsPage.java index 9c3fd26c990..c5b801a6c90 100644 --- a/it/it-plugins/ui-extensions-plugin/src/main/java/SettingsPage.java +++ b/it/it-plugins/ui-extensions-plugin/src/main/java/SettingsPage.java @@ -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() { diff --git a/it/it-plugins/ui-extensions-plugin/src/main/resources/static/global_page.js b/it/it-plugins/ui-extensions-plugin/src/main/resources/static/global_page.js new file mode 100644 index 00000000000..02369e084e1 --- /dev/null +++ b/it/it-plugins/ui-extensions-plugin/src/main/resources/static/global_page.js @@ -0,0 +1,4 @@ +window.registerExtension('uiextensionsplugin/global_page', function (options) { + options.el.textContent = 'uiextensionsplugin/global_page'; + return function () {}; +});
\ No newline at end of file diff --git a/it/it-plugins/ui-extensions-plugin/src/main/resources/static/project_page.js b/it/it-plugins/ui-extensions-plugin/src/main/resources/static/project_page.js new file mode 100644 index 00000000000..1e28dace449 --- /dev/null +++ b/it/it-plugins/ui-extensions-plugin/src/main/resources/static/project_page.js @@ -0,0 +1,4 @@ +window.registerExtension('uiextensionsplugin/project_page', function (options) { + options.el.textContent = 'uiextensionsplugin/project_page'; + return function () {}; +});
\ No newline at end of file diff --git a/it/it-plugins/ui-extensions-plugin/src/main/resources/static/resource_configuration_sample.js b/it/it-plugins/ui-extensions-plugin/src/main/resources/static/resource_configuration_sample.js new file mode 100644 index 00000000000..6d84892378a --- /dev/null +++ b/it/it-plugins/ui-extensions-plugin/src/main/resources/static/resource_configuration_sample.js @@ -0,0 +1,4 @@ +window.registerExtension('uiextensionsplugin/resource_configuration_sample', function (options) { + options.el.textContent = 'uiextensionsplugin/resource_configuration_sample'; + return function () {}; +});
\ No newline at end of file diff --git a/it/it-plugins/ui-extensions-plugin/src/main/resources/static/settings_page.js b/it/it-plugins/ui-extensions-plugin/src/main/resources/static/settings_page.js new file mode 100644 index 00000000000..d3a69f44aeb --- /dev/null +++ b/it/it-plugins/ui-extensions-plugin/src/main/resources/static/settings_page.js @@ -0,0 +1,4 @@ +window.registerExtension('uiextensionsplugin/settings_page', function (options) { + options.el.textContent = 'uiextensionsplugin/settings_page'; + return function () {}; +});
\ No newline at end of file diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc index df5d5291501..461c8bbc32e 100644 --- a/server/sonar-web/.eslintrc +++ b/server/sonar-web/.eslintrc @@ -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 diff --git a/server/sonar-web/src/main/js/app/components/AdminContainer.js b/server/sonar-web/src/main/js/app/components/AdminContainer.js index 26da1f1be44..0b85f97fb71 100644 --- a/server/sonar-web/src/main/js/app/components/AdminContainer.js +++ b/server/sonar-web/src/main/js/app/components/AdminContainer.js @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js new file mode 100644 index 00000000000..5ffef1f5908 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/extensions/Extension.js b/server/sonar-web/src/main/js/app/components/extensions/Extension.js new file mode 100644 index 00000000000..5b642937860 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/Extension.js @@ -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)); diff --git a/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.js b/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.js new file mode 100644 index 00000000000..e2311b1fde4 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.js new file mode 100644 index 00000000000..e79e25e4052 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.js @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.js new file mode 100644 index 00000000000..22e4eeeca8e --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.js @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js new file mode 100644 index 00000000000..7c9f36b73f4 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.js new file mode 100644 index 00000000000..c249b94b1c7 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.js @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/extensions/ViewDashboard.js b/server/sonar-web/src/main/js/app/components/extensions/ViewDashboard.js new file mode 100644 index 00000000000..8b57e56f570 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/ViewDashboard.js @@ -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' }}/> + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/extensions/utils.js b/server/sonar-web/src/main/js/app/components/extensions/utils.js new file mode 100644 index 00000000000..b9500c38073 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/extensions/utils.js @@ -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(); + } + }); + }) +); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js index c2dfa20660c..73ceb41f446 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js @@ -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 + }); + } }); }, diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js index 9c5d53f63e1..c41a461f35b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js @@ -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 () { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap index c76ff04de72..b2013614907 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap @@ -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", }, diff --git a/server/sonar-web/src/main/js/app/index.js b/server/sonar-web/src/main/js/app/index.js index edca4bb505c..33ec72df68c 100644 --- a/server/sonar-web/src/main/js/app/index.js +++ b/server/sonar-web/src/main/js/app/index.js @@ -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(); diff --git a/server/sonar-web/src/main/js/app/utils/installExtensionsHandler.js b/server/sonar-web/src/main/js/app/utils/installExtensionsHandler.js new file mode 100644 index 00000000000..f8cd418260b --- /dev/null +++ b/server/sonar-web/src/main/js/app/utils/installExtensionsHandler.js @@ -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]; +}; diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index 2c04125c885..ae1411b2643 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -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> diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.js b/server/sonar-web/src/main/js/apps/overview/components/App.js index 2aefdf292f3..dab6387f782 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.js +++ b/server/sonar-web/src/main/js/apps/overview/components/App.js @@ -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; diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js index 520688a4eb7..8f6f25c0f2b 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.js @@ -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'); }); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js index 61e26530eee..a0b76bf0690 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js @@ -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> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js index 57d8475bf83..26dc82ece68 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js @@ -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 } } }; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js index 46c0b395ae2..93020e2dff7 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js @@ -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 diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js b/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js index b2ddc8e6959..02fcbfacef2 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/Input.js @@ -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 diff --git a/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js b/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js index 55f2182af2c..8b7ef942c37 100644 --- a/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js +++ b/server/sonar-web/src/main/js/apps/settings/store/values/reducer.js @@ -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] } diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.js b/server/sonar-web/src/main/js/components/controls/DateInput.js index 4f011cef19e..2d15e9130c7 100644 --- a/server/sonar-web/src/main/js/components/controls/DateInput.js +++ b/server/sonar-web/src/main/js/components/controls/DateInput.js @@ -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, diff --git a/server/sonar-web/src/main/js/components/select-list/list.js b/server/sonar-web/src/main/js/components/select-list/list.js index 92604b288a3..476346880e2 100644 --- a/server/sonar-web/src/main/js/components/select-list/list.js +++ b/server/sonar-web/src/main/js/components/select-list/list.js @@ -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, diff --git a/server/sonar-web/src/main/js/components/select-list/main.js b/server/sonar-web/src/main/js/components/select-list/main.js index b763e6463d6..6547746ebe4 100644 --- a/server/sonar-web/src/main/js/components/select-list/main.js +++ b/server/sonar-web/src/main/js/components/select-list/main.js @@ -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, diff --git a/server/sonar-web/src/main/js/store/appState/duck.js b/server/sonar-web/src/main/js/store/appState/duck.js index 1fe430c990a..912ceaa85c2 100644 --- a/server/sonar-web/src/main/js/store/appState/duck.js +++ b/server/sonar-web/src/main/js/store/appState/duck.js @@ -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 }; } diff --git a/server/sonar-web/src/main/js/store/globalMessages/duck.js b/server/sonar-web/src/main/js/store/globalMessages/duck.js index 57abed0977b..f3a5d1cdf58 100644 --- a/server/sonar-web/src/main/js/store/globalMessages/duck.js +++ b/server/sonar-web/src/main/js/store/globalMessages/duck.js @@ -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-'), diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 8ec7fda9c47..0ca4ee2690e 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 |