diff options
13 files changed, 248 insertions, 57 deletions
diff --git a/server/sonar-web/npm-debug.log.d92601cb81ea7493b0878a7dda333587 b/server/sonar-web/npm-debug.log.d92601cb81ea7493b0878a7dda333587 deleted file mode 100644 index e69de29bb2d..00000000000 --- a/server/sonar-web/npm-debug.log.d92601cb81ea7493b0878a7dda333587 +++ /dev/null diff --git a/server/sonar-web/src/main/js/components/dashboards/dashboard-sidebar.js b/server/sonar-web/src/main/js/components/dashboards/dashboard-sidebar.js new file mode 100644 index 00000000000..f68aa7b9c89 --- /dev/null +++ b/server/sonar-web/src/main/js/components/dashboards/dashboard-sidebar.js @@ -0,0 +1,124 @@ +import qs from 'querystring'; +import _ from 'underscore'; +import classNames from 'classnames'; +import React from 'react'; + +import { getLocalizedDashboardName } from '../../helpers/l10n'; +import { getComponentDashboardUrl, getComponentFixedDashboardUrl, getComponentDashboardManagementUrl } from '../../helpers/urls'; + + +const FIXED_DASHBOARDS = [ + { link: '', name: 'overview.page' }, + { link: '/issues', name: 'overview.domain.debt' }, + { link: '/tests', name: 'overview.domain.coverage' }, + { link: '/duplications', name: 'overview.domain.duplications' }, + { link: '/size', name: 'overview.domain.size' } +]; + +const CUSTOM_DASHBOARDS_LIMIT = 1; + + +export const DashboardSidebar = React.createClass({ + propTypes: { + component: React.PropTypes.object.isRequired, + customDashboards: React.PropTypes.arrayOf(React.PropTypes.object).isRequired + }, + + periodParameter() { + let params = qs.parse(window.location.search.substr(1)); + return params.period ? `&period=${params.period}` : ''; + }, + + getPeriod() { + let params = qs.parse(window.location.search.substr(1)); + return params.period; + }, + + isFixedDashboardActive(fixedDashboard) { + let path = window.location.pathname; + return path === `${window.baseUrl}/overview${fixedDashboard.link}`; + }, + + isCustomDashboardActive(customDashboard) { + let path = window.location.pathname, + params = qs.parse(window.location.search.substr(1)); + return path.indexOf(`${window.baseUrl}/dashboard`) === 0 && params['did'] === `${customDashboard.key}`; + }, + + isMoreCustomDashboardsActive () { + let dashboards = _.rest(this.props.customDashboards, CUSTOM_DASHBOARDS_LIMIT); + return _.any(dashboards, this.isCustomDashboardActive); + }, + + isDashboardManagementActive () { + let path = window.location.pathname; + return path.indexOf(`${window.baseUrl}/dashboards`) === 0; + }, + + renderFixedDashboards() { + return FIXED_DASHBOARDS.map(fixedDashboard => { + let key = 'fixed-dashboard-' + fixedDashboard.link.substr(1); + let url = getComponentFixedDashboardUrl(this.props.component.key, fixedDashboard.link); + let name = window.t(fixedDashboard.name); + let className = classNames({ active: this.isFixedDashboardActive(fixedDashboard) }); + return <li key={key} className={className}> + <a href={url}>{name}</a> + </li>; + }); + }, + + renderCustomDashboards() { + let dashboards = _.first(this.props.customDashboards, CUSTOM_DASHBOARDS_LIMIT); + return dashboards.map(this.renderCustomDashboard); + }, + + renderCustomDashboard(customDashboard) { + let key = 'custom-dashboard-' + customDashboard.key; + let url = getComponentDashboardUrl(this.props.component.key, customDashboard.key, this.getPeriod()); + let name = getLocalizedDashboardName(customDashboard.name); + let className = classNames({ active: this.isCustomDashboardActive(customDashboard) }); + return <li key={key} className={className}> + <a href={url}>{name}</a> + </li>; + }, + + renderMoreCustomDashboards() { + if (this.props.customDashboards.length <= CUSTOM_DASHBOARDS_LIMIT) { + return null; + } + let dashboards = _.rest(this.props.customDashboards, CUSTOM_DASHBOARDS_LIMIT) + .map(this.renderCustomDashboard); + let className = classNames('dropdown', { active: this.isMoreCustomDashboardsActive() }); + return <li className={className}> + <a className="dropdown-toggle" data-toggle="dropdown" href="#"> + More + <i className="icon-dropdown"/> + </a> + <ul className="dropdown-menu">{dashboards}</ul> + </li>; + }, + + renderDashboardsManagementLink() { + if (!window.SS.user) { + return null; + } + let key = 'dashboard-management'; + let url = getComponentDashboardManagementUrl(this.props.component.key); + let name = window.t('dashboard.manage_dashboards'); + let className = classNames('pill-right', { active: this.isDashboardManagementActive() }); + return <li key={key} className={className}> + <a className="note" href={url}>{name}</a> + </li>; + }, + + render() { + return <nav className="navbar-side"> + <ul className="pills"> + {this.renderFixedDashboards()} + {this.renderCustomDashboards()} + {this.renderMoreCustomDashboards()} + {this.renderDashboardsManagementLink()} + </ul> + </nav>; + } +}); diff --git a/server/sonar-web/src/main/js/helpers/l10n.js b/server/sonar-web/src/main/js/helpers/l10n.js new file mode 100644 index 00000000000..de950a94b93 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/l10n.js @@ -0,0 +1,5 @@ +export function getLocalizedDashboardName (baseName) { + var l10nKey = 'dashboard.' + baseName + '.name'; + var l10nLabel = window.t(l10nKey); + return l10nLabel !== l10nKey ? l10nLabel : baseName; +} diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js index 8280af1c6ea..2dc10eacb31 100644 --- a/server/sonar-web/src/main/js/helpers/urls.js +++ b/server/sonar-web/src/main/js/helpers/urls.js @@ -41,3 +41,41 @@ export function getComponentDrilldownUrl (componentKey, metric, period, highligh } return url; } + + +/** + * Generate URL for a component's dashboard + * @param {string} componentKey + * @param {string} dashboardKey + * @param {string} [period] + * @returns {string} + */ +export function getComponentDashboardUrl (componentKey, dashboardKey, period) { + let url = window.baseUrl + '/dashboard?id=' + encodeURIComponent(componentKey) + + '&did=' + encodeURIComponent(dashboardKey); + if (period) { + url += '&period=' + period; + } + return url; +} + + +/** + * Generate URL for a fixed component's dashboard (overview) + * @param {string} componentKey + * @param {string} dashboardKey + * @returns {string} + */ +export function getComponentFixedDashboardUrl (componentKey, dashboardKey) { + return window.baseUrl + '/overview' + dashboardKey + '?id=' + encodeURIComponent(componentKey); +} + + +/** + * Generate URL for a component's dashboards management page + * @param {string} componentKey + * @returns {string} + */ +export function getComponentDashboardManagementUrl (componentKey) { + return window.baseUrl + '/dashboards?resource=' + encodeURIComponent(componentKey); +} diff --git a/server/sonar-web/src/main/js/main/nav/app.js b/server/sonar-web/src/main/js/main/nav/app.js index af45c03ff02..f4f9644be13 100644 --- a/server/sonar-web/src/main/js/main/nav/app.js +++ b/server/sonar-web/src/main/js/main/nav/app.js @@ -1,9 +1,11 @@ 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'; +import { getGlobalNavigation, getComponentNavigation, getSettingsNavigation } from '../../api/nav'; +import { DashboardSidebar } from '../../components/dashboards/dashboard-sidebar'; import '../../components/workspace/main'; import '../../helpers/handlebars-helpers'; @@ -44,15 +46,26 @@ export default class App { } static renderComponentNav (options) { - return getComponentNavigation(options.componentKey).then(r => { + return getComponentNavigation(options.componentKey).then(component => { const el = document.getElementById('context-navigation'); if (el) { - ReactDOM.render(<ComponentNav component={r} conf={r.configuration || {}}/>, el); + ReactDOM.render(<ComponentNav component={component} conf={component.configuration || {}}/>, el); } - return r; + this.renderSidebarNav(component); + return component; }); } + static renderSidebarNav (component) { + let shouldRender = + window.location.pathname.indexOf(window.baseUrl + '/overview') === 0 || + window.location.pathname.indexOf(window.baseUrl + '/dashboard') === 0; + let el = document.getElementById('sidebar'); + if (shouldRender && el) { + ReactDOM.render(<DashboardSidebar component={component} customDashboards={component.dashboards}/>, el); + } + } + static renderSettingsNav (options) { return getSettingsNavigation().then(r => { let el = document.getElementById('context-navigation'); diff --git a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js index 28818b16c54..a73e466e35a 100644 --- a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js +++ b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js @@ -17,9 +17,14 @@ export default React.createClass({ return params.period ? `&period=${params.period}` : ''; }, - renderOverviewLink() { + renderDashboardLink() { let url = `/overview?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, window.t('overview.page'), '/overview'); + return this.renderLink(url, window.t('layout.dashboards'), () => { + let cond = + window.location.pathname.indexOf(window.baseUrl + '/overview') === 0 || + window.location.pathname.indexOf(window.baseUrl + '/dashboard') === 0; + return cond ? 'active' : null; + }); }, renderComponentsLink() { @@ -55,7 +60,9 @@ export default React.createClass({ return ( <li className={className}> <a className="dropdown-toggle navbar-admin-link" data-toggle="dropdown" href="#"> - {window.t('layout.settings')} <i className="icon-dropdown"/></a> + {window.t('layout.settings')} + <i className="icon-dropdown"/> + </a> <ul className="dropdown-menu"> {this.renderSettingsLink()} {this.renderProfilesLink()} @@ -169,69 +176,30 @@ export default React.createClass({ }); }, - renderMore() { - return ( - <li className="dropdown"> - <a className="dropdown-toggle" data-toggle="dropdown" href="#"> - {window.t('more')} <i className="icon-dropdown"></i> - </a> - <ul className="dropdown-menu"> - {this.renderDashboards()} - {this.renderDashboardManagementLink()} - {this.renderTools()} - </ul> - </li> - ); - }, - - renderDashboards() { - let dashboards = (this.props.component.dashboards || []).map(d => { - let url = `/dashboard?id=${encodeURIComponent(this.props.component.key)}&did=${d.key}${this.periodParameter()}`; - let name = this.getLocalizedDashboardName(d.name); - return this.renderLink(url, name); - }); - return [<li key="0" className="dropdown-header">{window.t('layout.dashboards')}</li>].concat(dashboards); - }, - - renderDashboardManagementLink() { - if (!window.SS.user) { - return null; - } - let url = `/dashboards?resource=${encodeURIComponent(this.props.component.key)}`; - let name = window.t('dashboard.manage_dashboards'); - return [ - <li key="dashboard-divider" className="small-divider"></li>, - this.renderLink(url, name, '/dashboards') - ]; - }, - renderTools() { let component = this.props.component; if (!component.isComparable && !_.size(component.extensions)) { return null; } - let tools = [ - <li key="tools-divider" className="divider"></li>, - <li key="tools" className="dropdown-header">Tools</li> - ]; + let tools = []; + (component.extensions || []).forEach(e => { + tools.push(this.renderLink(e.url, e.name)); + }); if (component.isComparable) { let compareUrl = `/comparison/index?resource=${component.key}`; tools.push(this.renderLink(compareUrl, window.t('comparison.page'))); } - (component.extensions || []).forEach(e => { - tools.push(this.renderLink(e.url, e.name)); - }); return tools; }, render() { return ( <ul className="nav navbar-nav nav-tabs"> - {this.renderOverviewLink()} + {this.renderDashboardLink()} {this.renderComponentsLink()} {this.renderComponentIssuesLink()} {this.renderAdministration()} - {this.renderMore()} + {this.renderTools()} </ul> ); } diff --git a/server/sonar-web/src/main/js/main/nav/links-mixin.js b/server/sonar-web/src/main/js/main/nav/links-mixin.js index d2f07300261..73b2a69ba5f 100644 --- a/server/sonar-web/src/main/js/main/nav/links-mixin.js +++ b/server/sonar-web/src/main/js/main/nav/links-mixin.js @@ -11,7 +11,7 @@ export default { let fullUrl = window.baseUrl + url; let check = _.isFunction(highlightUrl) ? highlightUrl : this.activeLink; return ( - <li key={highlightUrl} className={check(highlightUrl)}> + <li key={url} className={check(highlightUrl)}> <a href={fullUrl}>{title}</a> </li> ); diff --git a/server/sonar-web/src/main/less/components.less b/server/sonar-web/src/main/less/components.less index 34d748dc806..464b4260cdf 100644 --- a/server/sonar-web/src/main/less/components.less +++ b/server/sonar-web/src/main/less/components.less @@ -23,3 +23,4 @@ @import "components/columns"; @import "components/workspace"; @import "components/search"; +@import "components/pills"; diff --git a/server/sonar-web/src/main/less/components/navbar.less b/server/sonar-web/src/main/less/components/navbar.less index 25f4552bb7e..1685c6635d9 100644 --- a/server/sonar-web/src/main/less/components/navbar.less +++ b/server/sonar-web/src/main/less/components/navbar.less @@ -219,3 +219,9 @@ color: @secondFontColor; font-size: @smallFontSize; } + +.navbar-side { + padding: 10px; + border-bottom: 1px solid @barBorderColor; + background-color: #e5f1f9; +} diff --git a/server/sonar-web/src/main/less/components/page.less b/server/sonar-web/src/main/less/components/page.less index 77d560efb23..bb510b2ff85 100644 --- a/server/sonar-web/src/main/less/components/page.less +++ b/server/sonar-web/src/main/less/components/page.less @@ -12,7 +12,7 @@ body { .page { .clearfix; position: relative; - padding: 10px; + padding: 10px 20px; } .page-container { diff --git a/server/sonar-web/src/main/less/components/pills.less b/server/sonar-web/src/main/less/components/pills.less new file mode 100644 index 00000000000..88cc5a3c619 --- /dev/null +++ b/server/sonar-web/src/main/less/components/pills.less @@ -0,0 +1,38 @@ +@import (reference) '../variables'; + +.pills { + display: flex; +} + +.pills > li { + +} + +.pills > li + li { + margin-left: 2px; +} + +.pills > li > a { + display: block; + height: @formControlHeight; + max-width: 135px; + line-height: @formControlHeight; + padding: 0 10px; + border: none; + border-radius: @formControlHeight; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + transition: none; +} + +.pills > li.active > a, +.pills > li > a:hover, +.pills > li > a:focus { + background-color: @darkBlue; + color: #fff; +} + +.pill-right { + margin-left: auto !important; +} diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/dashboards/_my_dashboards.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/dashboards/_my_dashboards.html.erb index b055bb15f90..3c2425053b5 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/dashboards/_my_dashboards.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/dashboards/_my_dashboards.html.erb @@ -21,9 +21,6 @@ <%= link_to h(dashboard.name(true)), {:controller => :dashboard, :action => :index, :did => dashboard.id, :id => (resource_id unless dashboard.global?)}, :id => "view-#{u dashboard.name}" %> <div class="description"><%= h dashboard.description -%></div> - <% if index == 0 %> - <div class="note spacer-top"><%= h message('dashboard.default_dashboard') -%></div> - <% end %> </td> <td class="shared"> <% if (dashboard.shared) %><i class="icon-check" id='<%= "dashboard-#{index}-shared" -%>'></i><% end %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb index b793d31198e..2af9eb9a60c 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb @@ -17,6 +17,7 @@ <%= yield :header -%> <div id="body" class="page-container"> + <div id="sidebar"></div> <div id="content"> <div class="panel hidden" id="messages-panel"> <div class="alert alert-danger hidden" id="error"> |