diff options
Diffstat (limited to 'server/sonar-web/src/main/js/app')
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}/> {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')} + <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')} + <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')} + <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"/> <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}/> + {window.SS.userName} <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> {{t 'shortcuts.section.global.search'}}</li> + <li><span class="shortcut-button">?</span> {{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">↑</span> <span + class="shortcut-button">↓</span> {{t 'shortcuts.section.rules.navigate_between_rules'}}</li> + <li><span class="shortcut-button">→</span> {{t 'shortcuts.section.rules.open_details'}}</li> + <li><span class="shortcut-button">←</span> {{t 'shortcuts.section.rules.return_to_list'}}</li> + <li><span class="shortcut-button">a</span> {{t 'shortcuts.section.rules.activate'}}</li> + <li><span class="shortcut-button">d</span> {{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">↑</span> <span + class="shortcut-button">↓</span> {{t 'shortcuts.section.issues.navigate_between_issues'}} + </li> + <li><span class="shortcut-button">→</span> {{t 'shortcuts.section.issues.open_details'}}</li> + <li><span class="shortcut-button">←</span> {{t 'shortcuts.section.issues.return_to_list'}}</li> + <li><span class="shortcut-button">⎵</span> {{t 'shortcuts.section.issue.select'}}</li> + <li><span class="shortcut-button">f</span> {{t 'shortcuts.section.issue.do_transition'}}</li> + <li><span class="shortcut-button">a</span> {{t 'shortcuts.section.issue.assign'}}</li> + <li><span class="shortcut-button">m</span> {{t 'shortcuts.section.issue.assign_to_me'}}</li> + <li><span class="shortcut-button">i</span> {{t 'shortcuts.section.issue.change_severity'}}</li> + <li><span class="shortcut-button">c</span> {{t 'shortcuts.section.issue.comment'}}</li> + <li><span class="shortcut-button">ctrl</span> + <span class="shortcut-button">enter</span> {{t 'shortcuts.section.issue.submit_comment'}}</li> + <li><span class="shortcut-button">t</span> {{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; |