aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/app
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/app')
-rw-r--r--server/sonar-web/src/main/js/app/components/ComponentContainer.js40
-rw-r--r--server/sonar-web/src/main/js/app/components/NullComponent.js22
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/__tests__/nav-test.js31
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/app.js91
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/RecentHistory.js66
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/component-nav-breadcrumbs.js42
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/component-nav-favorite.js36
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/component-nav-menu.js262
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/component-nav-meta.js77
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/component-nav.js87
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/dashboard-name-mixin.js32
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/global-nav-branding.js46
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/global-nav-menu.js131
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/global-nav-search.js106
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/global-nav-user.js69
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/global-nav.js73
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/search-view.js264
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/shortcuts-help-view.js27
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/links-mixin.js40
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/settings-nav.js133
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/templates/nav-search-empty.hbs1
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/templates/nav-search-item.hbs22
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/templates/nav-search.hbs8
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs61
-rw-r--r--server/sonar-web/src/main/js/app/index.js46
-rw-r--r--server/sonar-web/src/main/js/app/store/rootReducer.js206
-rw-r--r--server/sonar-web/src/main/js/app/styles/index.js5
-rw-r--r--server/sonar-web/src/main/js/app/utils/configureLocale.js30
-rw-r--r--server/sonar-web/src/main/js/app/utils/exposeLibraries.js28
-rw-r--r--server/sonar-web/src/main/js/app/utils/isCurrentPathKnown.js69
-rw-r--r--server/sonar-web/src/main/js/app/utils/startAjaxMonitoring.js192
-rw-r--r--server/sonar-web/src/main/js/app/utils/startApp.js69
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.js116
33 files changed, 2491 insertions, 37 deletions
diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.js b/server/sonar-web/src/main/js/app/components/ComponentContainer.js
new file mode 100644
index 00000000000..f31850c3122
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.js
@@ -0,0 +1,40 @@
+/*
+ * 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';
+
+export default class ComponentContainer extends React.Component {
+ state = {};
+
+ componentDidMount () {
+ window.sonarqube.appStarted.then(options => {
+ this.setState({ component: options.component });
+ });
+ }
+
+ render () {
+ if (!this.state.component) {
+ return null;
+ }
+
+ return React.cloneElement(this.props.children, {
+ component: this.state.component
+ });
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/NullComponent.js b/server/sonar-web/src/main/js/app/components/NullComponent.js
new file mode 100644
index 00000000000..a3501dfbd16
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/NullComponent.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+export default function () {
+ return null;
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/__tests__/nav-test.js b/server/sonar-web/src/main/js/app/components/nav/__tests__/nav-test.js
new file mode 100644
index 00000000000..d5ddbab8afd
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/__tests__/nav-test.js
@@ -0,0 +1,31 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import ComponentNavBreadcrumbs from '../component/component-nav-breadcrumbs';
+
+describe('ComponentNavBreadcrumbs', () => {
+ it('should not render breadcrumbs with one element', function () {
+ const breadcrumbs = [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }];
+ const result = shallow(<ComponentNavBreadcrumbs breadcrumbs={breadcrumbs}/>);
+ expect(result.find('li').length).toBe(1);
+ expect(result.find('a').length).toBe(1);
+ });
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/app.js b/server/sonar-web/src/main/js/app/components/nav/app.js
new file mode 100644
index 00000000000..629c686c071
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/app.js
@@ -0,0 +1,91 @@
+/*
+ * 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 _ from 'underscore';
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import GlobalNav from './global/global-nav';
+import ComponentNav from './component/component-nav';
+import SettingsNav from './settings/settings-nav';
+import { getGlobalNavigation, getComponentNavigation, getSettingsNavigation } from '../../../api/nav';
+
+export default class App {
+ start () {
+ const options = window.sonarqube;
+
+ require('../../../components/workspace/main');
+
+ return new Promise((resolve) => {
+ const response = {};
+ const requests = [];
+
+ requests.push(
+ App.renderGlobalNav(options).then(r => response.global = r)
+ );
+
+ if (options.space === 'component') {
+ requests.push(
+ App.renderComponentNav(options).then(r => response.component = r)
+ );
+ } else if (options.space === 'settings') {
+ requests.push(
+ App.renderSettingsNav(options).then(r => response.settings = r)
+ );
+ }
+
+ Promise.all(requests).then(() => resolve(response));
+ });
+ }
+
+ static renderGlobalNav (options) {
+ return getGlobalNavigation().then(r => {
+ const el = document.getElementById('global-navigation');
+ if (el) {
+ ReactDOM.render(<GlobalNav {...options} {...r}/>, el);
+ }
+ return r;
+ });
+ }
+
+ static renderComponentNav (options) {
+ return getComponentNavigation(options.componentKey).then(component => {
+ const el = document.getElementById('context-navigation');
+ const nextComponent = {
+ ...component,
+ qualifier: _.last(component.breadcrumbs).qualifier
+ };
+ if (el) {
+ ReactDOM.render(<ComponentNav component={nextComponent} conf={component.configuration || {}}/>, el);
+ }
+ return component;
+ });
+ }
+
+ static renderSettingsNav (options) {
+ return getSettingsNavigation().then(r => {
+ const el = document.getElementById('context-navigation');
+ const opts = _.extend(r, options);
+ if (el) {
+ ReactDOM.render(<SettingsNav {...opts}/>, el);
+ }
+ return r;
+ });
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/RecentHistory.js b/server/sonar-web/src/main/js/app/components/nav/component/RecentHistory.js
new file mode 100644
index 00000000000..6454cfe8afb
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/RecentHistory.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 _ from 'underscore';
+
+const STORAGE_KEY = 'sonar_recent_history';
+const HISTORY_LIMIT = 10;
+
+export default class RecentHistory {
+ static get () {
+ let history = localStorage.getItem(STORAGE_KEY);
+ if (history == null) {
+ history = [];
+ } else {
+ try {
+ history = JSON.parse(history);
+ } catch (e) {
+ RecentHistory.clear();
+ history = [];
+ }
+ }
+ return history;
+ }
+
+ static set (newHistory) {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(newHistory));
+ }
+
+ static clear () {
+ localStorage.removeItem(STORAGE_KEY);
+ }
+
+ static add (componentKey, componentName, icon) {
+ const sonarHistory = RecentHistory.get();
+
+ if (componentKey) {
+ const newEntry = { key: componentKey, name: componentName, icon };
+ let newHistory = _.reject(sonarHistory, entry => entry.key === newEntry.key);
+ newHistory.unshift(newEntry);
+ newHistory = _.first(newHistory, HISTORY_LIMIT);
+ RecentHistory.set(newHistory);
+ }
+ }
+
+ static remove (componentKey) {
+ const history = RecentHistory.get();
+ const newHistory = _.reject(history, entry => entry.key === componentKey);
+ RecentHistory.set(newHistory);
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-breadcrumbs.js b/server/sonar-web/src/main/js/app/components/nav/component/component-nav-breadcrumbs.js
new file mode 100644
index 00000000000..7458095814c
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/component-nav-breadcrumbs.js
@@ -0,0 +1,42 @@
+/*
+ * 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 QualifierIcon from '../../../../components/shared/qualifier-icon';
+
+export default React.createClass({
+ render() {
+ if (!this.props.breadcrumbs) {
+ return null;
+ }
+ const items = this.props.breadcrumbs.map((item, index) => {
+ const url = `${window.baseUrl}/dashboard/index?id=${encodeURIComponent(item.key)}`;
+ return (
+ <li key={index}>
+ <a href={url}>
+ <QualifierIcon qualifier={item.qualifier}/>&nbsp;{item.name}
+ </a>
+ </li>
+ );
+ });
+ return (
+ <ul className="nav navbar-nav nav-crumbs">{items}</ul>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-favorite.js b/server/sonar-web/src/main/js/app/components/nav/component/component-nav-favorite.js
new file mode 100644
index 00000000000..72b590e48fb
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/component-nav-favorite.js
@@ -0,0 +1,36 @@
+/*
+ * 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 Favorite from '../../../../components/controls/Favorite';
+
+export default React.createClass({
+ render() {
+ if (!this.props.canBeFavorite) {
+ return null;
+ }
+ return (
+ <div className="navbar-context-favorite">
+ <Favorite
+ component={this.props.component}
+ favorite={this.props.favorite}/>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-menu.js b/server/sonar-web/src/main/js/app/components/nav/component/component-nav-menu.js
new file mode 100644
index 00000000000..3321670d281
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/component-nav-menu.js
@@ -0,0 +1,262 @@
+/*
+ * 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 qs from 'querystring';
+import classNames from 'classnames';
+import React from 'react';
+import LinksMixin from '../links-mixin';
+import { translate } from '../../../../helpers/l10n';
+import { getComponentUrl } from '../../../../helpers/urls';
+
+const SETTINGS_URLS = [
+ '/project/settings',
+ '/project/quality_profiles',
+ '/project/quality_gate',
+ '/custom_measures',
+ '/project/links',
+ '/project_roles',
+ '/project/history',
+ 'background_tasks',
+ '/project/key',
+ '/project/deletion'
+];
+
+export default React.createClass({
+ mixins: [LinksMixin],
+
+ isDeveloper() {
+ return this.props.component.qualifier === 'DEV';
+ },
+
+ isView() {
+ const { qualifier } = this.props.component;
+ return qualifier === 'VW' || qualifier === 'SVW';
+ },
+
+ periodParameter() {
+ const params = qs.parse(window.location.search.substr(1));
+ return params.period ? `&period=${params.period}` : '';
+ },
+
+ getPeriod() {
+ const params = qs.parse(window.location.search.substr(1));
+ return params.period;
+ },
+
+ isFixedDashboardActive() {
+ const path = window.location.pathname;
+ return path.indexOf(window.baseUrl + '/dashboard') === 0 || path.indexOf(window.baseUrl + '/governance') === 0;
+ },
+
+ renderDashboardLink() {
+ const url = getComponentUrl(this.props.component.key);
+ const name = <i className="icon-home"/>;
+ const className = classNames({ active: this.isFixedDashboardActive() });
+ return (
+ <li key="overview" className={className}>
+ <a href={url}>{name}</a>
+ </li>
+ );
+ },
+
+ renderCodeLink() {
+ if (this.isDeveloper()) {
+ return null;
+ }
+
+ const url = `/code/?id=${encodeURIComponent(this.props.component.key)}`;
+ const header = this.isView() ? translate('view_projects.page') : translate('code.page');
+ return this.renderLink(url, header, '/code');
+ },
+
+ renderComponentIssuesLink() {
+ const url = `/component_issues?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('issues.page'), '/component_issues');
+ },
+
+ renderComponentMeasuresLink() {
+ const url = `/component_measures/?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('layout.measures'), '/component_measures');
+ },
+
+ renderAdministration() {
+ const shouldShowAdministration =
+ this.props.conf.showBackgroundTasks ||
+ this.props.conf.showHistory ||
+ this.props.conf.showLinks ||
+ this.props.conf.showManualMeasures ||
+ this.props.conf.showPermissions ||
+ this.props.conf.showQualityGates ||
+ this.props.conf.showQualityProfiles ||
+ this.props.conf.showSettings ||
+ this.props.conf.showUpdateKey;
+ if (!shouldShowAdministration) {
+ return null;
+ }
+ const isSettingsActive = SETTINGS_URLS.some(url => {
+ return window.location.href.indexOf(url) !== -1;
+ });
+ const className = 'dropdown' + (isSettingsActive ? ' active' : '');
+ return (
+ <li className={className}>
+ <a className="dropdown-toggle navbar-admin-link" data-toggle="dropdown" href="#">
+ {translate('layout.settings')}&nbsp;
+ <i className="icon-dropdown"/>
+ </a>
+ <ul className="dropdown-menu">
+ {this.renderSettingsLink()}
+ {this.renderProfilesLink()}
+ {this.renderQualityGateLink()}
+ {this.renderCustomMeasuresLink()}
+ {this.renderLinksLink()}
+ {this.renderPermissionsLink()}
+ {this.renderHistoryLink()}
+ {this.renderBackgroundTasksLink()}
+ {this.renderUpdateKeyLink()}
+ {this.renderExtensions()}
+ {this.renderDeletionLink()}
+ </ul>
+ </li>
+ );
+ },
+
+ renderSettingsLink() {
+ if (!this.props.conf.showSettings) {
+ return null;
+ }
+ const url = `/project/settings?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('project_settings.page'), '/project/settings');
+ },
+
+ renderProfilesLink() {
+ if (!this.props.conf.showQualityProfiles) {
+ return null;
+ }
+ const url = `/project/quality_profiles?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('project_quality_profiles.page'), '/project/quality_profiles');
+ },
+
+ renderQualityGateLink() {
+ if (!this.props.conf.showQualityGates) {
+ return null;
+ }
+ const url = `/project/quality_gate?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('project_quality_gate.page'), '/project/quality_gate');
+ },
+
+ renderCustomMeasuresLink() {
+ if (!this.props.conf.showManualMeasures) {
+ return null;
+ }
+ const url = `/custom_measures?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('custom_measures.page'), '/custom_measures');
+ },
+
+ renderLinksLink() {
+ if (!this.props.conf.showLinks) {
+ return null;
+ }
+ const url = `/project/links?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('project_links.page'), '/project/links');
+ },
+
+ renderPermissionsLink() {
+ if (!this.props.conf.showPermissions) {
+ return null;
+ }
+ const url = `/project_roles?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('permissions.page'), '/project_roles');
+ },
+
+ renderHistoryLink() {
+ if (!this.props.conf.showHistory) {
+ return null;
+ }
+ const url = `/project/history?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('project_history.page'), '/project/history');
+ },
+
+ renderBackgroundTasksLink() {
+ if (!this.props.conf.showBackgroundTasks) {
+ return null;
+ }
+ const url = `/project/background_tasks?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('background_tasks.page'), '/project/background_tasks');
+ },
+
+ renderUpdateKeyLink() {
+ if (!this.props.conf.showUpdateKey) {
+ return null;
+ }
+ const url = `/project/key?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('update_key.page'), '/project/key');
+ },
+
+ renderDeletionLink() {
+ const { qualifier } = this.props.component;
+
+ if (qualifier !== 'TRK' && qualifier !== 'VW') {
+ return null;
+ }
+
+ const url = `/project/deletion?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('deletion.page'), '/project/deletion');
+ },
+
+ renderExtensions() {
+ const extensions = this.props.conf.extensions || [];
+ return extensions.map(e => this.renderLink(e.url, e.name, e.url));
+ },
+
+ renderTools() {
+ const extensions = this.props.component.extensions || [];
+ const withoutGovernance = extensions.filter(ext => ext.name !== 'Governance');
+ const tools = withoutGovernance
+ .map(extension => this.renderLink(extension.url, extension.name));
+
+ if (!tools.length) {
+ return null;
+ }
+
+ return (
+ <li className="dropdown">
+ <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+ {translate('more')}&nbsp;
+ <i className="icon-dropdown"/>
+ </a>
+ <ul className="dropdown-menu">
+ {tools}
+ </ul>
+ </li>
+ );
+ },
+
+ render() {
+ return (
+ <ul className="nav navbar-nav nav-tabs">
+ {this.renderDashboardLink()}
+ {this.renderComponentIssuesLink()}
+ {this.renderComponentMeasuresLink()}
+ {this.renderCodeLink()}
+ {this.renderTools()}
+ {this.renderAdministration()}
+ </ul>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/component-nav-meta.js b/server/sonar-web/src/main/js/app/components/nav/component/component-nav-meta.js
new file mode 100644
index 00000000000..393564cd8a3
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/component-nav-meta.js
@@ -0,0 +1,77 @@
+/*
+ * 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 moment from 'moment';
+import React from 'react';
+import PendingIcon from '../../../../components/shared/pending-icon';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+
+export default React.createClass({
+ render() {
+ const metaList = [];
+ const canSeeBackgroundTasks = this.props.conf.showBackgroundTasks;
+ const backgroundTasksUrl =
+ `${window.baseUrl}/project/background_tasks?id=${encodeURIComponent(this.props.component.key)}`;
+
+ if (this.props.isInProgress) {
+ const tooltip = canSeeBackgroundTasks ?
+ translateWithParameters('component_navigation.status.in_progress.admin', backgroundTasksUrl) :
+ translate('component_navigation.status.in_progress');
+ metaList.push(
+ <li key="isInProgress" data-toggle="tooltip" title={tooltip}>
+ <i className="spinner" style={{ marginTop: '-1px' }}/>
+ {' '}
+ <span className="text-info">{translate('background_task.status.IN_PROGRESS')}</span>
+ </li>
+ );
+ } else if (this.props.isPending) {
+ const tooltip = canSeeBackgroundTasks ?
+ translateWithParameters('component_navigation.status.pending.admin', backgroundTasksUrl) :
+ translate('component_navigation.status.pending');
+ metaList.push(
+ <li key="isPending" data-toggle="tooltip" title={tooltip}>
+ <PendingIcon/> <span>{translate('background_task.status.PENDING')}</span>
+ </li>
+ );
+ } else if (this.props.isFailed) {
+ const tooltip = canSeeBackgroundTasks ?
+ translateWithParameters('component_navigation.status.failed.admin', backgroundTasksUrl) :
+ translate('component_navigation.status.failed');
+ metaList.push(
+ <li key="isFailed" data-toggle="tooltip" title={tooltip}>
+ <span className="badge badge-danger">{translate('background_task.status.FAILED')}</span>
+ </li>
+ );
+ }
+
+ if (this.props.snapshotDate) {
+ metaList.push(<li key="snapshotDate">{moment(this.props.snapshotDate).format('LLL')}</li>);
+ }
+
+ if (this.props.version) {
+ metaList.push(<li key="version">Version {this.props.version}</li>);
+ }
+
+ return (
+ <div className="navbar-right navbar-context-meta">
+ <ul className="list-inline">{metaList}</ul>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/component-nav.js b/server/sonar-web/src/main/js/app/components/nav/component/component-nav.js
new file mode 100644
index 00000000000..bb511a169a5
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/component-nav.js
@@ -0,0 +1,87 @@
+/*
+ * 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 $ from 'jquery';
+import _ from 'underscore';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { STATUSES } from '../../../../apps/background-tasks/constants';
+import { getTasksForComponent } from '../../../../api/ce';
+import ComponentNavFavorite from './component-nav-favorite';
+import ComponentNavBreadcrumbs from './component-nav-breadcrumbs';
+import ComponentNavMeta from './component-nav-meta';
+import ComponentNavMenu from './component-nav-menu';
+import RecentHistory from './RecentHistory';
+
+export default React.createClass({
+ componentDidMount() {
+ this.loadStatus();
+ this.populateRecentHistory();
+ },
+
+ loadStatus() {
+ getTasksForComponent(this.props.component.uuid).then(r => {
+ this.setState({
+ isPending: !!_.findWhere(r.queue, { status: STATUSES.PENDING }),
+ isInProgress: !!_.findWhere(r.queue, { status: STATUSES.IN_PROGRESS }),
+ isFailed: r.current && r.current.status === STATUSES.FAILED
+ }, this.initTooltips);
+ });
+ },
+
+ populateRecentHistory() {
+ const qualifier = _.last(this.props.component.breadcrumbs).qualifier;
+ if (['TRK', 'VW', 'DEV'].indexOf(qualifier) !== -1) {
+ RecentHistory.add(this.props.component.key, this.props.component.name, qualifier.toLowerCase());
+ }
+ },
+
+ initTooltips() {
+ $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this)).tooltip({
+ container: 'body',
+ placement: 'bottom',
+ delay: { show: 0, hide: 2000 },
+ html: true
+ });
+ },
+
+ render() {
+ return (
+ <div className="container">
+ <ComponentNavFavorite
+ component={this.props.component.key}
+ favorite={this.props.component.isFavorite}
+ canBeFavorite={this.props.component.canBeFavorite}/>
+
+ <ComponentNavBreadcrumbs
+ breadcrumbs={this.props.component.breadcrumbs}/>
+
+ <ComponentNavMeta
+ {...this.props}
+ {...this.state}
+ version={this.props.component.version}
+ snapshotDate={this.props.component.snapshotDate}/>
+
+ <ComponentNavMenu
+ component={this.props.component}
+ conf={this.props.conf}/>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/dashboard-name-mixin.js b/server/sonar-web/src/main/js/app/components/nav/dashboard-name-mixin.js
new file mode 100644
index 00000000000..2bc0227b02a
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/dashboard-name-mixin.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.
+ */
+import { translate } from '../../../helpers/l10n';
+
+export default {
+ getLocalizedDashboardName(baseName) {
+ const l10nKey = `dashboard.${baseName}.name`;
+ const l10nLabel = translate(l10nKey);
+ if (l10nLabel !== l10nKey) {
+ return l10nLabel;
+ } else {
+ return baseName;
+ }
+ }
+};
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-branding.js b/server/sonar-web/src/main/js/app/components/nav/global/global-nav-branding.js
new file mode 100644
index 00000000000..6241f7a4b63
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/global/global-nav-branding.js
@@ -0,0 +1,46 @@
+/*
+ * 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 { translate } from '../../../../helpers/l10n';
+
+export default React.createClass({
+ renderLogo() {
+ const url = this.props.logoUrl || `${window.baseUrl}/images/logo.svg`;
+ const width = this.props.logoWidth || 100;
+ const height = 30;
+ const title = translate('layout.sonar.slogan');
+ return <img src={url}
+ width={width}
+ height={height}
+ alt={title}
+ title={title}/>;
+ },
+
+ render() {
+ const homeController = window.SS.user ? '/projects/favorite' : '/about';
+ const homeUrl = window.baseUrl + homeController;
+ const homeLinkClassName = 'navbar-brand' + (this.props.logoUrl ? ' navbar-brand-custom' : '');
+ return (
+ <div className="navbar-header">
+ <a className={homeLinkClassName} href={homeUrl}>{this.renderLogo()}</a>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-menu.js b/server/sonar-web/src/main/js/app/components/nav/global/global-nav-menu.js
new file mode 100644
index 00000000000..6295431ffb1
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/global/global-nav-menu.js
@@ -0,0 +1,131 @@
+/*
+ * 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 DashboardNameMixin from '../dashboard-name-mixin';
+import LinksMixin from '../links-mixin';
+import { translate } from '../../../../helpers/l10n';
+
+export default React.createClass({
+ mixins: [DashboardNameMixin, LinksMixin],
+
+ getDefaultProps () {
+ return { globalDashboards: [], globalPages: [] };
+ },
+
+ renderProjects () {
+ const controller = window.SS.user ? '/projects/favorite' : '/projects';
+ const url = window.baseUrl + controller;
+ return (
+ <li className={this.activeLink('/projects')}>
+ <a href={url}>{translate('projects.page')}</a>
+ </li>
+ );
+ },
+
+ renderIssuesLink () {
+ const query = window.SS.user ? '#resolved=false|assigned_to_me=true' : '#resolved=false';
+ const url = window.baseUrl + '/issues' + query;
+ return (
+ <li className={this.activeLink('/issues')}>
+ <a href={url}>{translate('issues.page')}</a>
+ </li>
+ );
+ },
+
+ renderRulesLink () {
+ const url = window.baseUrl + '/coding_rules';
+ return (
+ <li className={this.activeLink('/coding_rules')}>
+ <a href={url}>{translate('coding_rules.page')}</a>
+ </li>
+ );
+ },
+
+ renderProfilesLink() {
+ const url = window.baseUrl + '/profiles';
+ return (
+ <li className={this.activeLink('/profiles')}>
+ <a href={url}>{translate('quality_profiles.page')}</a>
+ </li>
+ );
+ },
+
+ renderQualityGatesLink () {
+ const url = window.baseUrl + '/quality_gates';
+ return (
+ <li className={this.activeLink('/quality_gates')}>
+ <a href={url}>{translate('quality_gates.page')}</a>
+ </li>
+ );
+ },
+
+ renderAdministrationLink () {
+ if (!window.SS.isUserAdmin) {
+ return null;
+ }
+ const url = window.baseUrl + '/settings';
+ return (
+ <li className={this.activeLink('/settings')}>
+ <a className="navbar-admin-link" href={url}>{translate('layout.settings')}</a>
+ </li>
+ );
+ },
+
+ renderGlobalPageLink (globalPage, index) {
+ const url = window.baseUrl + globalPage.url;
+ return (
+ <li key={index}>
+ <a href={url}>{globalPage.name}</a>
+ </li>
+ );
+ },
+
+ renderMore () {
+ if (this.props.globalPages.length === 0) {
+ return null;
+ }
+ const globalPages = this.props.globalPages.map(this.renderGlobalPageLink);
+ return (
+ <li className="dropdown">
+ <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+ {translate('more')}&nbsp;
+ <span className="icon-dropdown"/>
+ </a>
+ <ul className="dropdown-menu">
+ {globalPages}
+ </ul>
+ </li>
+ );
+ },
+
+ render () {
+ return (
+ <ul className="nav navbar-nav">
+ {this.renderProjects()}
+ {this.renderIssuesLink()}
+ {this.renderRulesLink()}
+ {this.renderProfilesLink()}
+ {this.renderQualityGatesLink()}
+ {this.renderAdministrationLink()}
+ {this.renderMore()}
+ </ul>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-search.js b/server/sonar-web/src/main/js/app/components/nav/global/global-nav-search.js
new file mode 100644
index 00000000000..e84850bf365
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/global/global-nav-search.js
@@ -0,0 +1,106 @@
+/*
+ * 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 Backbone from 'backbone';
+import React from 'react';
+import SearchView from './search-view';
+
+function contains (root, node) {
+ while (node) {
+ if (node === root) {
+ return true;
+ }
+ node = node.parentNode;
+ }
+ return false;
+}
+
+export default React.createClass({
+ getInitialState() {
+ return { open: false };
+ },
+
+ componentDidMount() {
+ key('s', () => {
+ const isModalOpen = document.querySelector('html').classList.contains('modal-open');
+ if (!isModalOpen) {
+ this.openSearch();
+ }
+ return false;
+ });
+ },
+
+ componentWillUnmount() {
+ this.closeSearch();
+ key.unbind('s');
+ },
+
+ openSearch() {
+ document.addEventListener('click', this.onClickOutside);
+ this.setState({ open: true }, this.renderSearchView);
+ },
+
+ closeSearch() {
+ document.removeEventListener('click', this.onClickOutside);
+ this.resetSearchView();
+ this.setState({ open: false });
+ },
+
+ renderSearchView() {
+ const searchContainer = this.refs.container;
+ this.searchView = new SearchView({
+ model: new Backbone.Model(this.props),
+ hide: this.closeSearch
+ });
+ this.searchView.render().$el.appendTo(searchContainer);
+ },
+
+ resetSearchView() {
+ if (this.searchView) {
+ this.searchView.destroy();
+ }
+ },
+
+ onClick(e) {
+ e.preventDefault();
+ if (this.state.open) {
+ this.closeSearch();
+ } else {
+ this.openSearch();
+ }
+ },
+
+ onClickOutside(e) {
+ if (!contains(this.refs.dropdown, e.target)) {
+ this.closeSearch();
+ }
+ },
+
+ render() {
+ const dropdownClassName = 'dropdown' + (this.state.open ? ' open' : '');
+ return (
+ <li ref="dropdown" className={dropdownClassName}>
+ <a className="navbar-search-dropdown" href="#" onClick={this.onClick}>
+ <i className="icon-search navbar-icon"/>&nbsp;<i className="icon-dropdown"/>
+ </a>
+ <div ref="container" className="dropdown-menu dropdown-menu-right global-navbar-search-dropdown"></div>
+ </li>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/global-nav-user.js b/server/sonar-web/src/main/js/app/components/nav/global/global-nav-user.js
new file mode 100644
index 00000000000..8c2d0cc9949
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/global/global-nav-user.js
@@ -0,0 +1,69 @@
+/*
+ * 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 Avatar from '../../../../components/ui/Avatar';
+import RecentHistory from '../component/RecentHistory';
+import { translate } from '../../../../helpers/l10n';
+
+export default React.createClass({
+ renderAuthenticated() {
+ return (
+ <li className="dropdown js-user-authenticated">
+ <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+ <Avatar email={window.SS.userEmail} size={20}/>&nbsp;
+ {window.SS.userName}&nbsp;<i className="icon-dropdown"/>
+ </a>
+ <ul className="dropdown-menu dropdown-menu-right">
+ <li>
+ <a href={`${window.baseUrl}/account/`}>{translate('my_account.page')}</a>
+ </li>
+ <li>
+ <a onClick={this.handleLogout} href="#">{translate('layout.logout')}</a>
+ </li>
+ </ul>
+ </li>
+ );
+ },
+
+ renderAnonymous() {
+ return (
+ <li>
+ <a onClick={this.handleLogin} href="#">{translate('layout.login')}</a>
+ </li>
+ );
+ },
+
+ handleLogin(e) {
+ e.preventDefault();
+ const returnTo = window.location.pathname + window.location.search;
+ window.location = `${window.baseUrl}/sessions/new?return_to=${encodeURIComponent(returnTo)}${window.location.hash}`;
+ },
+
+ handleLogout(e) {
+ e.preventDefault();
+ RecentHistory.clear();
+ window.location = `${window.baseUrl}/sessions/logout`;
+ },
+
+ render() {
+ const isUserAuthenticated = !!window.SS.user;
+ return isUserAuthenticated ? this.renderAuthenticated() : this.renderAnonymous();
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/global-nav.js b/server/sonar-web/src/main/js/app/components/nav/global/global-nav.js
new file mode 100644
index 00000000000..1c2916c56de
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/global/global-nav.js
@@ -0,0 +1,73 @@
+/*
+ * 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 GlobalNavBranding from './global-nav-branding';
+import GlobalNavMenu from './global-nav-menu';
+import GlobalNavUser from './global-nav-user';
+import GlobalNavSearch from './global-nav-search';
+import ShortcutsHelpView from './shortcuts-help-view';
+
+export default React.createClass({
+ componentDidMount() {
+ window.addEventListener('keypress', this.onKeyPress);
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener('keypress', this.onKeyPress);
+ },
+
+ onKeyPress(e) {
+ const tagName = e.target.tagName;
+ const code = e.keyCode || e.which;
+ const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
+ const isTriggerKey = code === 63;
+ const isModalOpen = document.querySelector('html').classList.contains('modal-open');
+ if (!isInput && !isModalOpen && isTriggerKey) {
+ this.openHelp();
+ }
+ },
+
+ openHelp(e) {
+ if (e) {
+ e.preventDefault();
+ }
+ new ShortcutsHelpView().render();
+ },
+
+ render() {
+ return (
+ <div className="container">
+ <GlobalNavBranding {...this.props}/>
+
+ <GlobalNavMenu {...this.props}/>
+
+ <ul className="nav navbar-nav navbar-right">
+ <GlobalNavUser {...this.props}/>
+ <GlobalNavSearch {...this.props}/>
+ <li>
+ <a onClick={this.openHelp} href="#">
+ <i className="icon-help navbar-icon"/>
+ </a>
+ </li>
+ </ul>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/search-view.js b/server/sonar-web/src/main/js/app/components/nav/global/search-view.js
new file mode 100644
index 00000000000..9974ddcc284
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/global/search-view.js
@@ -0,0 +1,264 @@
+/*
+ * 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 $ from 'jquery';
+import _ from 'underscore';
+import Backbone from 'backbone';
+import Marionette from 'backbone.marionette';
+import SelectableCollectionView from '../../../../components/common/selectable-collection-view';
+import SearchItemTemplate from '../templates/nav-search-item.hbs';
+import EmptySearchTemplate from '../templates/nav-search-empty.hbs';
+import SearchTemplate from '../templates/nav-search.hbs';
+import RecentHistory from '../component/RecentHistory';
+import { translate } from '../../../../helpers/l10n';
+import { collapsedDirFromPath, fileFromPath } from '../../../../helpers/path';
+
+const SearchItemView = Marionette.ItemView.extend({
+ tagName: 'li',
+ template: SearchItemTemplate,
+
+ select () {
+ this.$el.addClass('active');
+ },
+
+ deselect () {
+ this.$el.removeClass('active');
+ },
+
+ submit () {
+ this.$('a')[0].click();
+ },
+
+ onRender () {
+ this.$('[data-toggle="tooltip"]').tooltip({
+ container: 'body',
+ html: true,
+ placement: 'left',
+ delay: { show: 500, hide: 0 }
+ });
+ },
+
+ onDestroy () {
+ this.$('[data-toggle="tooltip"]').tooltip('destroy');
+ },
+
+ serializeData () {
+ return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
+ index: this.options.index
+ });
+ }
+});
+
+const SearchEmptyView = Marionette.ItemView.extend({
+ tagName: 'li',
+ template: EmptySearchTemplate
+});
+
+const SearchResultsView = SelectableCollectionView.extend({
+ className: 'menu',
+ tagName: 'ul',
+ childView: SearchItemView,
+ emptyView: SearchEmptyView
+});
+
+export default Marionette.LayoutView.extend({
+ className: 'navbar-search',
+ tagName: 'form',
+ template: SearchTemplate,
+
+ regions: {
+ resultsRegion: '.js-search-results'
+ },
+
+ events: {
+ 'submit': 'onSubmit',
+ 'keydown .js-search-input': 'onKeyDown',
+ 'keyup .js-search-input': 'onKeyUp'
+ },
+
+ initialize () {
+ const that = this;
+ this.results = new Backbone.Collection();
+ this.favorite = [];
+ if (window.SS.user) {
+ this.fetchFavorite().always(function () {
+ that.resetResultsToDefault();
+ });
+ } else {
+ this.resetResultsToDefault();
+ }
+ this.resultsView = new SearchResultsView({ collection: this.results });
+ this.debouncedSearch = _.debounce(this.search, 250);
+ this._bufferedValue = '';
+ },
+
+ onRender () {
+ const that = this;
+ this.resultsRegion.show(this.resultsView);
+ setTimeout(function () {
+ that.$('.js-search-input').focus();
+ }, 0);
+ },
+
+ onKeyDown (e) {
+ if (e.keyCode === 38) {
+ this.resultsView.selectPrev();
+ return false;
+ }
+ if (e.keyCode === 40) {
+ this.resultsView.selectNext();
+ return false;
+ }
+ if (e.keyCode === 13) {
+ this.resultsView.submitCurrent();
+ this.destroy();
+ return false;
+ }
+ if (e.keyCode === 27) {
+ this.options.hide();
+ return false;
+ }
+ },
+
+ onKeyUp () {
+ const value = this.$('.js-search-input').val();
+ if (value === this._bufferedValue) {
+ return;
+ }
+ this._bufferedValue = this.$('.js-search-input').val();
+ this.searchRequest = this.debouncedSearch(value);
+ },
+
+ onSubmit () {
+ return false;
+ },
+
+ fetchFavorite () {
+ const that = this;
+ return $.get(window.baseUrl + '/api/favourites').done(function (r) {
+ that.favorite = r.map(function (f) {
+ const isFile = ['FIL', 'UTS'].indexOf(f.qualifier) !== -1;
+ return {
+ url: window.baseUrl + '/dashboard/index?id=' + encodeURIComponent(f.key) + window.dashboardParameters(true),
+ name: isFile ? collapsedDirFromPath(f.lname) + fileFromPath(f.lname) : f.name,
+ icon: 'favorite'
+ };
+ });
+ that.favorite = _.sortBy(that.favorite, 'name');
+ });
+ },
+
+ resetResultsToDefault () {
+ const recentHistory = RecentHistory.get();
+ const history = recentHistory.map(function (historyItem, index) {
+ const url = window.baseUrl + '/dashboard/index?id=' + encodeURIComponent(historyItem.key) +
+ window.dashboardParameters(true);
+ return {
+ url,
+ name: historyItem.name,
+ q: historyItem.icon,
+ extra: index === 0 ? translate('browsed_recently') : null
+ };
+ });
+ const favorite = _.first(this.favorite, 6).map(function (f, index) {
+ return _.extend(f, { extra: index === 0 ? translate('favorite') : null });
+ });
+ this.results.reset([].concat(history, favorite));
+ },
+
+ search (q) {
+ if (q.length < 2) {
+ this.resetResultsToDefault();
+ return;
+ }
+ const that = this;
+ const url = window.baseUrl + '/api/components/suggestions';
+ const options = { s: q };
+ return $.get(url, options).done(function (r) {
+ // if the input value has changed since we sent the request,
+ // just ignore the output, because another request already sent
+ if (q !== that._bufferedValue) {
+ return;
+ }
+
+ const collection = [];
+ r.results.forEach(function (domain) {
+ domain.items.forEach(function (item, index) {
+ collection.push(_.extend(item, {
+ q: domain.q,
+ extra: index === 0 ? domain.name : null,
+ url: window.baseUrl + '/dashboard/index?id=' + encodeURIComponent(item.key) +
+ window.dashboardParameters(true)
+ }));
+ });
+ });
+ that.results.reset([].concat(
+ that.getNavigationFindings(q),
+ that.getGlobalDashboardFindings(q),
+ that.getFavoriteFindings(q),
+ collection
+ ));
+ });
+ },
+
+ getNavigationFindings (q) {
+ const DEFAULT_ITEMS = [
+ { name: translate('issues.page'), url: window.baseUrl + '/issues/search' },
+ { name: translate('layout.measures'), url: window.baseUrl + '/measures/search?qualifiers[]=TRK' },
+ { name: translate('coding_rules.page'), url: window.baseUrl + '/coding_rules' },
+ { name: translate('quality_profiles.page'), url: window.baseUrl + '/profiles' },
+ { name: translate('quality_gates.page'), url: window.baseUrl + '/quality_gates' }
+ ];
+ const customItems = [];
+ if (window.SS.isUserAdmin) {
+ customItems.push({ name: translate('layout.settings'), url: window.baseUrl + '/settings' });
+ }
+ const findings = [].concat(DEFAULT_ITEMS, customItems).filter(function (f) {
+ return f.name.match(new RegExp(q, 'i'));
+ });
+ if (findings.length > 0) {
+ findings[0].extra = translate('navigation');
+ }
+ return _.first(findings, 6);
+ },
+
+ getGlobalDashboardFindings (q) {
+ const dashboards = this.model.get('globalDashboards') || [];
+ const items = dashboards.map(function (d) {
+ return { name: d.name, url: window.baseUrl + '/dashboard/index?did=' + encodeURIComponent(d.key) };
+ });
+ const findings = items.filter(function (f) {
+ return f.name.match(new RegExp(q, 'i'));
+ });
+ if (findings.length > 0) {
+ findings[0].extra = translate('dashboard.global_dashboards');
+ }
+ return _.first(findings, 6);
+ },
+
+ getFavoriteFindings (q) {
+ const findings = this.favorite.filter(function (f) {
+ return f.name.match(new RegExp(q, 'i'));
+ });
+ if (findings.length > 0) {
+ findings[0].extra = translate('favorite');
+ }
+ return _.first(findings, 6);
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/shortcuts-help-view.js b/server/sonar-web/src/main/js/app/components/nav/global/shortcuts-help-view.js
new file mode 100644
index 00000000000..ca4f2f5ac97
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/global/shortcuts-help-view.js
@@ -0,0 +1,27 @@
+/*
+ * 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 ModalView from '../../../../components/common/modals';
+import ShortcutsHelpTemplate from '../templates/nav-shortcuts-help.hbs';
+
+export default ModalView.extend({
+ className: 'modal modal-large',
+ template: ShortcutsHelpTemplate
+});
+
diff --git a/server/sonar-web/src/main/js/app/components/nav/links-mixin.js b/server/sonar-web/src/main/js/app/components/nav/links-mixin.js
new file mode 100644
index 00000000000..0027e2250df
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/links-mixin.js
@@ -0,0 +1,40 @@
+/*
+ * 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 classNames from 'classnames';
+
+export default {
+ activeLink(url) {
+ return window.location.pathname.indexOf(window.baseUrl + url) === 0 ? 'active' : null;
+ },
+
+ 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>
+ );
+ }
+};
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/settings-nav.js b/server/sonar-web/src/main/js/app/components/nav/settings/settings-nav.js
new file mode 100644
index 00000000000..087064ed84c
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/settings-nav.js
@@ -0,0 +1,133 @@
+/*
+ * 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 classNames from 'classnames';
+import some from 'lodash/some';
+import LinksMixin from '../links-mixin';
+import { translate } from '../../../../helpers/l10n';
+
+export default React.createClass({
+ mixins: [LinksMixin],
+
+ getDefaultProps() {
+ return { extensions: [] };
+ },
+
+ isSomethingActive(urls) {
+ const path = window.location.pathname;
+ return some(urls, url => path.indexOf(window.baseUrl + url) === 0);
+ },
+
+ isSecurityActive() {
+ const urls = ['/users', '/groups', '/roles/global', '/permission_templates'];
+ return this.isSomethingActive(urls);
+ },
+
+ isProjectsActive() {
+ const urls = ['/projects_admin', '/background_tasks'];
+ return this.isSomethingActive(urls);
+ },
+
+ isSystemActive() {
+ const urls = ['/updatecenter', '/system'];
+ return this.isSomethingActive(urls);
+ },
+
+ render() {
+ const isSecurity = this.isSecurityActive();
+ const isProjects = this.isProjectsActive();
+ const isSystem = this.isSystemActive();
+
+ const securityClassName = classNames('dropdown', { active: isSecurity });
+ const projectsClassName = classNames('dropdown', { active: isProjects });
+ const systemClassName = classNames('dropdown', { active: isSystem });
+ const configurationClassNames = classNames('dropdown', {
+ active: !isSecurity && !isProjects && !isSystem
+ });
+
+ return (
+ <div className="container">
+ <ul className="nav navbar-nav nav-crumbs">
+ {this.renderLink('/settings', translate('layout.settings'))}
+ </ul>
+
+ <ul className="nav navbar-nav nav-tabs">
+ <li className={configurationClassNames}>
+ <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+ {translate('sidebar.project_settings')}
+ {' '}
+ <i className="icon-dropdown"></i>
+ </a>
+ <ul className="dropdown-menu">
+ {this.renderLink('/settings', translate('settings.page'), url => window.location.pathname === url)}
+ {this.renderLink('/settings/licenses', translate('property.category.licenses'))}
+ {this.renderLink('/settings/encryption', translate('property.category.security.encryption'))}
+ {this.renderLink('/settings/server_id', translate('property.category.server_id'))}
+ {this.renderLink('/metrics', 'Custom Metrics')}
+ {this.props.extensions.map(e => this.renderLink(e.url, e.name))}
+ </ul>
+ </li>
+
+ <li className={securityClassName}>
+ <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+ {translate('sidebar.security')}
+ {' '}
+ <i className="icon-dropdown"></i>
+ </a>
+ <ul className="dropdown-menu">
+ {this.renderLink('/users', translate('users.page'))}
+ {this.renderLink('/groups', translate('user_groups.page'))}
+ {this.renderLink('/roles/global',
+ translate('global_permissions.page'))}
+ {this.renderLink('/permission_templates',
+ translate('permission_templates'))}
+ </ul>
+ </li>
+
+ <li className={projectsClassName}>
+ <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+ {translate('sidebar.projects')}
+ {' '}
+ <i className="icon-dropdown"></i>
+ </a>
+ <ul className="dropdown-menu">
+ {this.renderLink('/projects_admin', 'Management')}
+ {this.renderLink('/background_tasks',
+ translate('background_tasks.page'))}
+ </ul>
+ </li>
+
+ <li className={systemClassName}>
+ <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+ {translate('sidebar.system')}
+ {' '}
+ <i className="icon-dropdown"></i>
+ </a>
+ <ul className="dropdown-menu">
+ {this.renderLink('/updatecenter', translate('update_center.page'))}
+ {this.renderLink('/system', translate('system_info.page'))}
+ </ul>
+ </li>
+ </ul>
+ </div>
+
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/templates/nav-search-empty.hbs b/server/sonar-web/src/main/js/app/components/nav/templates/nav-search-empty.hbs
new file mode 100644
index 00000000000..fb76e686612
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/templates/nav-search-empty.hbs
@@ -0,0 +1 @@
+<span class="note">{{t 'no_results'}}</span>
diff --git a/server/sonar-web/src/main/js/app/components/nav/templates/nav-search-item.hbs b/server/sonar-web/src/main/js/app/components/nav/templates/nav-search-item.hbs
new file mode 100644
index 00000000000..25128ddfc20
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/templates/nav-search-item.hbs
@@ -0,0 +1,22 @@
+{{#notNull extra}}
+ {{#gt index 0}}
+ <div class="divider"></div>
+ {{/gt}}
+ {{#if extra}}
+ <div class="dropdown-header">{{extra}}</div>
+ {{/if}}
+{{/notNull}}
+
+<a href="{{this.url}}" data-title="{{name}}<br>{{key}}" data-toggle="tooltip">
+ {{#if icon}}<i class="icon-{{icon}} text-text-bottom"></i>{{/if}}
+ {{#if q}}{{qualifierIcon q}}{{/if}}
+ {{#eq q 'FIL'}}
+ {{collapsedDirFromPath name}}{{fileFromPath name}}
+ {{else}}
+ {{#eq q 'UTS'}}
+ {{collapsedDirFromPath name}}{{fileFromPath name}}
+ {{else}}
+ {{name}}
+ {{/eq}}
+ {{/eq}}
+</a>
diff --git a/server/sonar-web/src/main/js/app/components/nav/templates/nav-search.hbs b/server/sonar-web/src/main/js/app/components/nav/templates/nav-search.hbs
new file mode 100644
index 00000000000..68e1f3ad168
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/templates/nav-search.hbs
@@ -0,0 +1,8 @@
+<i class="navbar-search-icon icon-search"></i>
+
+<input class="navbar-search-input js-search-input" type="search" name="q" placeholder="{{t 'search_verb'}}"
+ maxlength="30" autocomplete="off">
+
+<div class="js-search-results"></div>
+
+<div class="note navbar-search-subtitle">{{t 'search.shortcut'}}</div>
diff --git a/server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs b/server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs
new file mode 100644
index 00000000000..ae4f9967233
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs
@@ -0,0 +1,61 @@
+<div class="modal-head">
+ <h2>{{t 'help'}}</h2>
+</div>
+
+<div class="modal-body modal-container">
+ <div class="spacer-bottom">
+ <a href="http://www.sonarqube.org" target="sonar">Community</a> -
+ <a href="http://www.sonarqube.org/documentation" target="sonar_doc">Documentation</a> -
+ <a href="http://www.sonarqube.org/support" target="support">Get Support</a> -
+ <a href="http://redirect.sonarsource.com/doc/plugin-library.html" target="plugins">Plugins</a> -
+ <a href="{{link '/web_api'}}">Web API</a> -
+ <a href="{{link '/about'}}">About</a>
+ </div>
+
+ <h2 class="spacer-top spacer-bottom">{{t 'shortcuts.modal_title'}}</h2>
+
+ <div class="columns">
+ <div class="column-half">
+ <div class="spacer-bottom">
+ <h3 class="shortcuts-section-title">{{t 'shortcuts.section.global'}}</h3>
+ <ul class="shortcuts-list">
+ <li><span class="shortcut-button">s</span> &nbsp;&nbsp; {{t 'shortcuts.section.global.search'}}</li>
+ <li><span class="shortcut-button">?</span> &nbsp;&nbsp; {{t 'shortcuts.section.global.shortcuts'}}</li>
+ </ul>
+ </div>
+
+ <h3 class="shortcuts-section-title">{{t 'shortcuts.section.rules'}}</h3>
+ <ul class="shortcuts-list">
+ <li><span class="shortcut-button">&uarr;</span> <span
+ class="shortcut-button">&darr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.navigate_between_rules'}}</li>
+ <li><span class="shortcut-button">&rarr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.open_details'}}</li>
+ <li><span class="shortcut-button">&larr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.return_to_list'}}</li>
+ <li><span class="shortcut-button">a</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.activate'}}</li>
+ <li><span class="shortcut-button">d</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.deactivate'}}</li>
+ </ul>
+ </div>
+
+ <div class="column-half">
+ <h3 class="shortcuts-section-title">{{t 'shortcuts.section.issues'}}</h3>
+ <ul class="shortcuts-list">
+ <li><span class="shortcut-button">&uarr;</span> <span
+ class="shortcut-button">&darr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.navigate_between_issues'}}
+ </li>
+ <li><span class="shortcut-button">&rarr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.open_details'}}</li>
+ <li><span class="shortcut-button">&larr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.return_to_list'}}</li>
+ <li><span class="shortcut-button">⎵</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.select'}}</li>
+ <li><span class="shortcut-button">f</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.do_transition'}}</li>
+ <li><span class="shortcut-button">a</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.assign'}}</li>
+ <li><span class="shortcut-button">m</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.assign_to_me'}}</li>
+ <li><span class="shortcut-button">i</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.change_severity'}}</li>
+ <li><span class="shortcut-button">c</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.comment'}}</li>
+ <li><span class="shortcut-button">ctrl</span> + <span class="shortcut-button">enter</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.submit_comment'}}</li>
+ <li><span class="shortcut-button">t</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.change_tags'}}</li>
+ </ul>
+ </div>
+ </div>
+</div>
+
+<div class="modal-foot">
+ <a class="js-modal-close" href="#">{{t 'close'}}</a>
+</div>
diff --git a/server/sonar-web/src/main/js/app/index.js b/server/sonar-web/src/main/js/app/index.js
index 3ed7485fed3..65145e45906 100644
--- a/server/sonar-web/src/main/js/app/index.js
+++ b/server/sonar-web/src/main/js/app/index.js
@@ -17,41 +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 React from 'react';
-import { render } from 'react-dom';
-import { Router, Route, useRouterHistory } from 'react-router';
-import { createHistory } from 'history';
-import { Provider } from 'react-redux';
-import App from './components/App';
-import aboutRoutes from '../apps/about/routes';
-import accountRoutes from '../apps/account/routes';
-import projectsRoutes from '../apps/projects/routes';
-import qualityGatesRoutes from '../apps/quality-gates/routes';
-import qualityProfilesRoutes from '../apps/quality-profiles/routes';
-import configureStore from '../components/store/configureStore';
-import rootReducer from './store/rootReducer';
+import configureLocale from './utils/configureLocale';
+import exposeLibraries from './utils/exposeLibraries';
+import startAjaxMonitoring from './utils/startAjaxMonitoring';
+import startApp from './utils/startApp';
+import startReactApp from './utils/startReactApp';
import './styles/index';
-window.sonarqube.appStarted.then(options => {
- const el = document.querySelector(options.el);
-
- const history = useRouterHistory(createHistory)({
- basename: window.baseUrl + '/'
- });
-
- const store = configureStore(rootReducer);
-
- render((
- <Provider store={store}>
- <Router history={history}>
- <Route path="/" component={App}>
- <Route path="about">{aboutRoutes}</Route>
- <Route path="account">{accountRoutes}</Route>
- {projectsRoutes}
- {qualityGatesRoutes}
- {qualityProfilesRoutes}
- </Route>
- </Router>
- </Provider>
- ), el);
-});
+configureLocale();
+startAjaxMonitoring();
+startApp();
+startReactApp();
+exposeLibraries();
diff --git a/server/sonar-web/src/main/js/app/store/rootReducer.js b/server/sonar-web/src/main/js/app/store/rootReducer.js
index 40c02dafbf7..396c2b5c5f8 100644
--- a/server/sonar-web/src/main/js/app/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/app/store/rootReducer.js
@@ -25,8 +25,12 @@ import languages, * as fromLanguages from './languages/reducer';
import measures, * as fromMeasures from './measures/reducer';
import globalMessages, * as fromGlobalMessages from '../../components/store/globalMessages';
+import measuresApp, * as fromMeasuresApp from '../../apps/component-measures/store/rootReducer';
+import permissionsApp, * as fromPermissionsApp from '../../apps/permissions/shared/store/rootReducer';
+import projectAdminApp, * as fromProjectAdminApp from '../../apps/project-admin/store/rootReducer';
import projectsApp, * as fromProjectsApp from '../../apps/projects/store/reducer';
import qualityGatesApp from '../../apps/quality-gates/store/rootReducer';
+import settingsApp, * as fromSettingsApp from '../../apps/settings/store/rootReducer';
export default combineReducers({
components,
@@ -37,8 +41,12 @@ export default combineReducers({
users,
// apps
+ measuresApp,
+ permissionsApp,
+ projectAdminApp,
projectsApp,
- qualityGatesApp
+ qualityGatesApp,
+ settingsApp
});
export const getComponent = (state, key) => (
@@ -88,3 +96,199 @@ export const getProjectsAppMaxFacetValue = state => (
export const getQualityGatesAppState = state => (
state.qualityGatesApp
);
+
+export const getPermissionsAppUsers = state => (
+ fromPermissionsApp.getUsers(state.permissionsApp)
+);
+
+export const getPermissionsAppGroups = state => (
+ fromPermissionsApp.getGroups(state.permissionsApp)
+);
+
+export const isPermissionsAppLoading = state => (
+ fromPermissionsApp.isLoading(state.permissionsApp)
+);
+
+export const getPermissionsAppQuery = state => (
+ fromPermissionsApp.getQuery(state.permissionsApp)
+);
+
+export const getPermissionsAppFilter = state => (
+ fromPermissionsApp.getFilter(state.permissionsApp)
+);
+
+export const getPermissionsAppSelectedPermission = state => (
+ fromPermissionsApp.getSelectedPermission(state.permissionsApp)
+);
+
+export const getPermissionsAppError = state => (
+ fromPermissionsApp.getError(state.permissionsApp)
+);
+
+export const getSettingsAppDefinition = (state, key) => (
+ fromSettingsApp.getDefinition(state.settingsApp, key)
+);
+
+export const getSettingsAppAllCategories = state => (
+ fromSettingsApp.getAllCategories(state.settingsApp)
+);
+
+export const getSettingsAppDefaultCategory = state => (
+ fromSettingsApp.getDefaultCategory(state.settingsApp)
+);
+
+export const getSettingsAppSettingsForCategory = (state, category) => (
+ fromSettingsApp.getSettingsForCategory(state.settingsApp, category)
+);
+
+export const getSettingsAppChangedValue = (state, key) => (
+ fromSettingsApp.getChangedValue(state.settingsApp, key)
+);
+
+export const isSettingsAppLoading = (state, key) => (
+ fromSettingsApp.isLoading(state.settingsApp, key)
+);
+
+export const getSettingsAppLicenseByKey = (state, key) => (
+ fromSettingsApp.getLicenseByKey(state.settingsApp, key)
+);
+
+export const getSettingsAppAllLicenseKeys = state => (
+ fromSettingsApp.getAllLicenseKeys(state.settingsApp)
+);
+
+export const getSettingsAppValidationMessage = (state, key) => (
+ fromSettingsApp.getValidationMessage(state.settingsApp, key)
+);
+
+export const getSettingsAppEncryptionState = state => (
+ fromSettingsApp.getEncryptionState(state.settingsApp)
+);
+
+export const getSettingsAppGlobalMessages = state => (
+ fromSettingsApp.getGlobalMessages(state.settingsApp)
+);
+
+export const getProjectAdminProfileByKey = (state, profileKey) => (
+ fromProjectAdminApp.getProfileByKey(state.projectAdminApp, profileKey)
+);
+
+export const getProjectAdminAllProfiles = state => (
+ fromProjectAdminApp.getAllProfiles(state.projectAdminApp)
+);
+
+export const getProjectAdminProjectProfiles = (state, projectKey) => (
+ fromProjectAdminApp.getProjectProfiles(state.projectAdminApp, projectKey)
+);
+
+export const getProjectAdminGateById = (state, gateId) => (
+ fromProjectAdminApp.getGateById(state.projectAdminApp, gateId)
+);
+
+export const getProjectAdminAllGates = state => (
+ fromProjectAdminApp.getAllGates(state.projectAdminApp)
+);
+
+export const getProjectAdminProjectGate = (state, projectKey) => (
+ fromProjectAdminApp.getProjectGate(state.projectAdminApp, projectKey)
+);
+
+export const getProjectAdminLinkById = (state, linkId) => (
+ fromProjectAdminApp.getLinkById(state.projectAdminApp, linkId)
+);
+
+export const getProjectAdminProjectLinks = (state, projectKey) => (
+ fromProjectAdminApp.getProjectLinks(state.projectAdminApp, projectKey)
+);
+
+export const getProjectAdminComponentByKey = (state, componentKey) => (
+ fromProjectAdminApp.getComponentByKey(state.projectAdminApp, componentKey)
+);
+
+export const getProjectAdminProjectModules = (state, projectKey) => (
+ fromProjectAdminApp.getProjectModules(state.projectAdminApp, projectKey)
+);
+
+export const getProjectAdminGlobalMessages = state => (
+ fromProjectAdminApp.getGlobalMessages(state.projectAdminApp)
+);
+
+export const getMeasuresAppComponent = state => (
+ fromMeasuresApp.getComponent(state.measuresApp)
+);
+
+export const getMeasuresAppAllMetrics = state => (
+ fromMeasuresApp.getAllMetrics(state.measuresApp)
+);
+
+export const getMeasuresAppDetailsMetric = state => (
+ fromMeasuresApp.getDetailsMetric(state.measuresApp)
+);
+
+export const getMeasuresAppDetailsMeasure = state => (
+ fromMeasuresApp.getDetailsMeasure(state.measuresApp)
+);
+
+export const getMeasuresAppDetailsSecondaryMeasure = state => (
+ fromMeasuresApp.getDetailsSecondaryMeasure(state.measuresApp)
+);
+
+export const getMeasuresAppDetailsPeriods = state => (
+ fromMeasuresApp.getDetailsPeriods(state.measuresApp)
+);
+
+export const isMeasuresAppFetching = state => (
+ fromMeasuresApp.isFetching(state.measuresApp)
+);
+
+export const getMeasuresAppList = state => (
+ fromMeasuresApp.getList(state.measuresApp)
+);
+
+export const getMeasuresAppListComponents = state => (
+ fromMeasuresApp.getListComponents(state.measuresApp)
+);
+
+export const getMeasuresAppListSelected = state => (
+ fromMeasuresApp.getListSelected(state.measuresApp)
+);
+
+export const getMeasuresAppListTotal = state => (
+ fromMeasuresApp.getListTotal(state.measuresApp)
+);
+
+export const getMeasuresAppListPageIndex = state => (
+ fromMeasuresApp.getListPageIndex(state.measuresApp)
+);
+
+export const getMeasuresAppTree = state => (
+ fromMeasuresApp.getTree(state.measuresApp)
+);
+
+export const getMeasuresAppTreeComponents = state => (
+ fromMeasuresApp.getTreeComponents(state.measuresApp)
+);
+
+export const getMeasuresAppTreeBreadcrumbs = state => (
+ fromMeasuresApp.getTreeBreadcrumbs(state.measuresApp)
+);
+
+export const getMeasuresAppTreeSelected = state => (
+ fromMeasuresApp.getTreeSelected(state.measuresApp)
+);
+
+export const getMeasuresAppTreeTotal = state => (
+ fromMeasuresApp.getTreeTotal(state.measuresApp)
+);
+
+export const getMeasuresAppTreePageIndex = state => (
+ fromMeasuresApp.getTreePageIndex(state.measuresApp)
+);
+
+export const getMeasuresAppHomeDomains = state => (
+ fromMeasuresApp.getHomeDomains(state.measuresApp)
+);
+
+export const getMeasuresAppHomePeriods = state => (
+ fromMeasuresApp.getHomePeriods(state.measuresApp)
+);
diff --git a/server/sonar-web/src/main/js/app/styles/index.js b/server/sonar-web/src/main/js/app/styles/index.js
index 0f7e68f8ed3..24defb8b3f1 100644
--- a/server/sonar-web/src/main/js/app/styles/index.js
+++ b/server/sonar-web/src/main/js/app/styles/index.js
@@ -17,5 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import '../../components/ui/Level.css';
+import '../../components/ui/Rating.css';
import './boxed-group.css';
import './page.css';
+
+// these styles are extracted to a separate file
+import '../../../less/sonar.less';
diff --git a/server/sonar-web/src/main/js/app/utils/configureLocale.js b/server/sonar-web/src/main/js/app/utils/configureLocale.js
new file mode 100644
index 00000000000..d035798783b
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/utils/configureLocale.js
@@ -0,0 +1,30 @@
+/*
+ * 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 moment from 'moment';
+
+const getPreferredLanguage = () => (
+ window.navigator.languages ? window.navigator.languages[0] : window.navigator.language
+);
+
+const configureLocale = () => {
+ moment.locale(getPreferredLanguage());
+};
+
+export default configureLocale;
diff --git a/server/sonar-web/src/main/js/app/utils/exposeLibraries.js b/server/sonar-web/src/main/js/app/utils/exposeLibraries.js
new file mode 100644
index 00000000000..92580bae1c0
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/utils/exposeLibraries.js
@@ -0,0 +1,28 @@
+/*
+ * 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 * as measures from '../../helpers/measures';
+import * as request from '../../helpers/request';
+
+const exposeLibraries = () => {
+ window.SonarMeasures = measures;
+ window.SonarRequest = request;
+};
+
+export default exposeLibraries;
diff --git a/server/sonar-web/src/main/js/app/utils/isCurrentPathKnown.js b/server/sonar-web/src/main/js/app/utils/isCurrentPathKnown.js
new file mode 100644
index 00000000000..37468ca94f4
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/utils/isCurrentPathKnown.js
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+const knownPaths = [
+ 'about',
+ 'account',
+ 'background_tasks',
+ 'coding_rules',
+ 'dashboard',
+ 'groups',
+ 'issues',
+ 'maintenance',
+ 'metrics',
+ 'permission_templates',
+ 'projects',
+ 'projects_admin',
+ 'roles/global',
+ 'settings',
+ 'setup',
+ 'system',
+ 'quality_gates',
+ 'profiles',
+ 'updatecenter',
+ 'users',
+ 'web_api',
+ 'code',
+ 'component_issues',
+ 'component_measures',
+ 'custom_measures',
+ 'project/background_tasks',
+ 'project/settings',
+ 'project/deletion',
+ 'project/quality_profiles',
+ 'project/quality_gate',
+ 'project/links',
+ 'project/key',
+ 'project_roles'
+];
+
+const ignoredPaths = [
+ 'users/new'
+];
+
+export default function () {
+ const currentPath = window.location.pathname;
+
+ const isIgnored = ignoredPaths.some(path => currentPath.indexOf(`${window.baseUrl}/${path}`) === 0);
+ if (isIgnored) {
+ return false;
+ }
+
+ return knownPaths.some(path => currentPath.indexOf(`${window.baseUrl}/${path}`) === 0);
+}
diff --git a/server/sonar-web/src/main/js/app/utils/startAjaxMonitoring.js b/server/sonar-web/src/main/js/app/utils/startAjaxMonitoring.js
new file mode 100644
index 00000000000..cc3f75823db
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/utils/startAjaxMonitoring.js
@@ -0,0 +1,192 @@
+/*
+ * 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 $ from 'jquery';
+import _ from 'underscore';
+import Backbone from 'backbone';
+import Marionette from 'backbone.marionette';
+import escapeHtml from 'escape-html';
+import { translate } from '../../helpers/l10n';
+import { getCSRFTokenName, getCSRFTokenValue } from '../../helpers/request';
+
+const defaults = {
+ queue: {},
+ timeout: 300,
+ fadeTimeout: 100
+};
+
+const Process = Backbone.Model.extend({
+ defaults: {
+ state: 'ok'
+ },
+
+ timeout () {
+ this.set({
+ state: 'timeout',
+ message: 'Still Working...'
+ });
+ },
+
+ finish (options) {
+ options = _.defaults(options || {}, { force: false });
+ if (this.get('state') !== 'failed' || !!options.force) {
+ this.trigger('destroy', this, this.collection, options);
+ }
+ },
+
+ fail (message) {
+ const that = this;
+ let msg = message || translate('process.fail');
+ if (msg === 'process.fail') {
+ // no translation
+ msg = 'An error happened, some parts of the page might not render correctly. ' +
+ 'Please contact the administrator if you keep on experiencing this error.';
+ }
+ clearInterval(this.get('timer'));
+ this.set({
+ state: 'failed',
+ message: msg
+ });
+ this.set('state', 'failed');
+ setTimeout(function () {
+ that.finish({ force: true });
+ }, 5000);
+ }
+});
+
+const Processes = Backbone.Collection.extend({
+ model: Process
+});
+
+const ProcessesView = Marionette.ItemView.extend({
+ tagName: 'ul',
+ className: 'processes-container',
+
+ collectionEvents: {
+ 'all': 'render'
+ },
+
+ render () {
+ const failed = this.collection.findWhere({ state: 'failed' });
+ const timeout = this.collection.findWhere({ state: 'timeout' });
+ let el;
+ this.$el.empty();
+ if (failed != null) {
+ el = $('<li></li>')
+ .html(failed.get('message'))
+ .addClass('process-spinner process-spinner-failed shown');
+ const close = $('<button></button>').html('<i class="icon-close"></i>').addClass('process-spinner-close');
+ close.appendTo(el);
+ close.on('click', function () {
+ failed.finish({ force: true });
+ });
+ el.appendTo(this.$el);
+ } else if (timeout != null) {
+ el = $('<li></li>')
+ .html(timeout.get('message'))
+ .addClass('process-spinner shown');
+ el.appendTo(this.$el);
+ }
+ return this;
+ }
+});
+
+const processes = new Processes();
+const processesView = new ProcessesView({
+ collection: processes
+});
+
+/**
+ * Add background process
+ * @returns {number}
+ */
+function addBackgroundProcess () {
+ const uid = _.uniqueId('process');
+ const process = new Process({
+ id: uid,
+ timer: setTimeout(function () {
+ process.timeout();
+ }, defaults.timeout)
+ });
+ processes.add(process);
+ return uid;
+}
+
+/**
+ * Finish background process
+ * @param {number} uid
+ */
+function finishBackgroundProcess (uid) {
+ const process = processes.get(uid);
+ if (process != null) {
+ process.finish();
+ }
+}
+
+/**
+ * Fail background process
+ * @param {number} uid
+ * @param {string} message
+ */
+function failBackgroundProcess (uid, message) {
+ const process = processes.get(uid);
+ if (process != null) {
+ process.fail(message);
+ }
+}
+
+/**
+ * Handle ajax error
+ * @param jqXHR
+ */
+function handleAjaxError (jqXHR) {
+ if (jqXHR != null && jqXHR.processId != null) {
+ let message = null;
+ if (jqXHR.responseJSON != null && jqXHR.responseJSON.errors != null) {
+ message = _.pluck(jqXHR.responseJSON.errors, 'msg').join('. ');
+ }
+ failBackgroundProcess(jqXHR.processId, message ? escapeHtml(message) : null);
+ }
+}
+
+$.ajaxSetup({
+ beforeSend (jqXHR) {
+ jqXHR.setRequestHeader(getCSRFTokenName(), getCSRFTokenValue());
+ jqXHR.processId = addBackgroundProcess();
+ },
+ complete (jqXHR) {
+ if (jqXHR.processId != null) {
+ finishBackgroundProcess(jqXHR.processId);
+ }
+ },
+ statusCode: {
+ 400: handleAjaxError,
+ 403: handleAjaxError,
+ 500: handleAjaxError,
+ 502: handleAjaxError,
+ 503: handleAjaxError,
+ 504: handleAjaxError
+ }
+});
+
+const startAjaxMonitoring = () => {
+ processesView.render().$el.insertBefore('#footer');
+};
+
+export default startAjaxMonitoring;
diff --git a/server/sonar-web/src/main/js/app/utils/startApp.js b/server/sonar-web/src/main/js/app/utils/startApp.js
new file mode 100644
index 00000000000..9229702f16d
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/utils/startApp.js
@@ -0,0 +1,69 @@
+/*
+ * 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 _ from 'underscore';
+import Navigation from '../components/nav/app';
+import { installGlobal, requestMessages } from '../../helpers/l10n';
+
+function requestLocalizationBundle () {
+ if (!window.sonarqube.bannedNavigation) {
+ installGlobal();
+ return requestMessages();
+ } else {
+ return Promise.resolve();
+ }
+}
+
+function startNavigation () {
+ if (!window.sonarqube.bannedNavigation) {
+ return new Navigation().start();
+ } else {
+ return Promise.resolve();
+ }
+}
+
+function prepareAppOptions (navResponse) {
+ const appOptions = { el: '#content' };
+ if (navResponse) {
+ appOptions.rootQualifiers = navResponse.global.qualifiers;
+ appOptions.logoUrl = navResponse.global.logoUrl;
+ appOptions.logoWidth = navResponse.global.logoWidth;
+ if (navResponse.component) {
+ appOptions.component = {
+ id: navResponse.component.uuid,
+ key: navResponse.component.key,
+ name: navResponse.component.name,
+ qualifier: _.last(navResponse.component.breadcrumbs).qualifier,
+ breadcrumbs: navResponse.component.breadcrumbs,
+ snapshotDate: navResponse.component.snapshotDate
+ };
+ }
+ }
+ return appOptions;
+}
+
+const startApp = () => {
+ window.sonarqube.appStarted = Promise.resolve()
+ .then(requestLocalizationBundle)
+ .then(startNavigation)
+ .then(prepareAppOptions);
+};
+
+export default startApp;
+
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js
new file mode 100644
index 00000000000..31b5aac4197
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js
@@ -0,0 +1,116 @@
+/*
+ * 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 { render } from 'react-dom';
+import { Router, Route, useRouterHistory } from 'react-router';
+import { createHistory } from 'history';
+import { Provider } from 'react-redux';
+import App from '../components/App';
+import ComponentContainer from '../components/ComponentContainer';
+import NullComponent from '../components/NullComponent';
+import aboutRoutes from '../../apps/about/routes';
+import accountRoutes from '../../apps/account/routes';
+import backgroundTasksRoutes from '../../apps/background-tasks/routes';
+import codeRoutes from '../../apps/code/routes';
+import codingRulesRoutes from '../../apps/coding-rules/routes';
+import componentIssuesRoutes from '../../apps/component-issues/routes';
+import componentMeasuresRoutes from '../../apps/component-measures/routes';
+import customMeasuresRoutes from '../../apps/custom-measures/routes';
+import groupsRoutes from '../../apps/groups/routes';
+import issuesRoutes from '../../apps/issues/routes';
+import metricsRoutes from '../../apps/metrics/routes';
+import overviewRoutes from '../../apps/overview/routes';
+import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
+import projectAdminRoutes from '../../apps/project-admin/routes';
+import projectsRoutes from '../../apps/projects/routes';
+import projectsAdminRoutes from '../../apps/projects-admin/routes';
+import qualityGatesRoutes from '../../apps/quality-gates/routes';
+import qualityProfilesRoutes from '../../apps/quality-profiles/routes';
+import settingsRoutes from '../../apps/settings/routes';
+import systemRoutes from '../../apps/system/routes';
+import updateCenterRoutes from '../../apps/update-center/routes';
+import usersRoutes from '../../apps/users/routes';
+import webAPIRoutes from '../../apps/web-api/routes';
+import { maintenanceRoutes, setupRoutes } from '../../apps/maintenance/routes';
+import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/permissions/routes';
+import configureStore from '../../components/store/configureStore';
+import rootReducer from '../store/rootReducer';
+import isCurrentPathKnown from './isCurrentPathKnown';
+
+const startReactApp = () => {
+ if (isCurrentPathKnown()) {
+ window.sonarqube.appStarted.then(options => {
+ const el = document.querySelector(options.el);
+
+ const history = useRouterHistory(createHistory)({
+ basename: window.baseUrl + '/'
+ });
+
+ const store = configureStore(rootReducer);
+
+ render((
+ <Provider store={store}>
+ <Router history={history}>
+ <Route path="/" component={App}>
+ <Route path="about">{aboutRoutes}</Route>
+ <Route path="account">{accountRoutes}</Route>
+ <Route path="background_tasks">{backgroundTasksRoutes}</Route>
+ <Route path="coding_rules">{codingRulesRoutes}</Route>
+ <Route path="dashboard">{overviewRoutes}</Route>
+ <Route path="groups">{groupsRoutes}</Route>
+ <Route path="issues">{issuesRoutes}</Route>
+ <Route path="maintenance">{maintenanceRoutes}</Route>
+ <Route path="metrics">{metricsRoutes}</Route>
+ <Route path="permission_templates">{permissionTemplatesRoutes}</Route>
+ <Route path="projects">{projectsRoutes}</Route>
+ <Route path="projects_admin">{projectsAdminRoutes}</Route>
+ <Route path="roles/global">{globalPermissionsRoutes}</Route>
+ <Route path="settings">{settingsRoutes}</Route>
+ <Route path="setup">{setupRoutes}</Route>
+ <Route path="system">{systemRoutes}</Route>
+ <Route path="quality_gates">{qualityGatesRoutes}</Route>
+ <Route path="profiles">{qualityProfilesRoutes}</Route>
+ <Route path="updatecenter">{updateCenterRoutes}</Route>
+ <Route path="users">{usersRoutes}</Route>
+ <Route path="web_api">{webAPIRoutes}</Route>
+
+ <Route component={ComponentContainer}>
+ <Route path="code">{codeRoutes}</Route>
+ <Route path="component_issues">{componentIssuesRoutes}</Route>
+ <Route path="component_measures">{componentMeasuresRoutes}</Route>
+ <Route path="custom_measures">{customMeasuresRoutes}</Route>
+ <Route path="project">
+ <Route path="background_tasks">{backgroundTasksRoutes}</Route>
+ <Route path="settings">{settingsRoutes}</Route>
+ {projectAdminRoutes}
+ </Route>
+ <Route path="project_roles">{projectPermissionsRoutes}</Route>
+ </Route>
+ </Route>
+
+ <Route path="*" component={NullComponent}/>
+ </Router>
+ </Provider>
+ ), el);
+ });
+ }
+};
+
+export default startReactApp;