diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-08-04 17:53:08 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-08-10 17:11:54 +0200 |
commit | cc871d7b8c4af1f66234830719d27e45d4136334 (patch) | |
tree | 4a0e191441ba2fcd706853a74d6288436640bc85 | |
parent | a3df8c53bec6e8e61cac61a41c8e7489b5bfb098 (diff) | |
download | sonarqube-cc871d7b8c4af1f66234830719d27e45d4136334.tar.gz sonarqube-cc871d7b8c4af1f66234830719d27e45d4136334.zip |
SONAR-6331 add project overview
51 files changed, 1469 insertions, 45 deletions
diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee index d324e5b63fc..a74b6f86171 100644 --- a/server/sonar-web/Gruntfile.coffee +++ b/server/sonar-web/Gruntfile.coffee @@ -38,6 +38,8 @@ module.exports = (grunt) -> babel: build: + options: + modules: 'amd' files: [ expand: true cwd: '<%= SOURCE_PATH %>/js' @@ -142,6 +144,7 @@ module.exports = (grunt) -> 'build-app:measures' 'build-app:metrics' 'build-app:nav' + 'build-app:overview' 'build-app:provisioning' 'build-app:quality-gates' 'build-app:quality-profiles' diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js index e25e8be81de..182e3b988d6 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js @@ -30,12 +30,12 @@ define([ var that = this; this.total = null; this.projects = []; - this.requestIssues().done(function () { + this.requestNutshellIssues().done(function () { that.render(); }); }, - requestIssues: function () { + requestNutshellIssues: function () { var that = this, url = baseUrl + '/api/issues/search', options = { diff --git a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js index 1e79d92e45d..5eabc8b76fd 100644 --- a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js +++ b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js @@ -158,7 +158,7 @@ define([ }); }, - requestIssues: function () { + requestNutshellIssues: function () { var that = this; var r; if (this.options.app.list.last().get('component') === this.model.get('key')) { diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-home-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-home-view.js index 942e86e4ba7..dac3eb0bc5f 100644 --- a/server/sonar-web/src/main/js/apps/issues/workspace-home-view.js +++ b/server/sonar-web/src/main/js/apps/issues/workspace-home-view.js @@ -32,7 +32,7 @@ define([ initialize: function () { this.model = new Backbone.Model(); - this.requestIssues(); + this.requestNutshellIssues(); this.requestMyIssues(); }, @@ -75,7 +75,7 @@ define([ } }, - requestIssues: function () { + requestNutshellIssues: function () { var that = this; var url = baseUrl + '/api/issues/search', options = { diff --git a/server/sonar-web/src/main/js/apps/nav/context-navbar-view.js b/server/sonar-web/src/main/js/apps/nav/context-navbar-view.js index 95351c828ba..960599e1d21 100644 --- a/server/sonar-web/src/main/js/apps/nav/context-navbar-view.js +++ b/server/sonar-web/src/main/js/apps/nav/context-navbar-view.js @@ -23,7 +23,7 @@ define([ var $ = jQuery, MORE_URLS = [ - '/dashboards', '/plugins/resource' + '/dashboards', '/dashboard', '/plugins/resource' ], SETTINGS_URLS = [ '/project/settings', '/project/profile', '/project/qualitygate', '/manual_measures/index', @@ -72,13 +72,11 @@ define([ }) || (href.indexOf('/dashboard') !== -1 && search.indexOf('did=') !== -1), isSettingsActive = _.some(SETTINGS_URLS, function (url) { return href.indexOf(url) !== -1; - }), - isOverviewActive = !isMoreActive && href.indexOf('/dashboard') !== -1 && search.indexOf('did=') === -1; + }); return _.extend(Marionette.LayoutView.prototype.serializeData.apply(this, arguments), { canManageContextDashboards: !!window.SS.user, contextKeyEncoded: encodeURIComponent(this.model.get('componentKey')), - isOverviewActive: isOverviewActive, isSettingsActive: isSettingsActive, isMoreActive: isMoreActive }); diff --git a/server/sonar-web/src/main/js/apps/nav/templates/nav-context-navbar.hbs b/server/sonar-web/src/main/js/apps/nav/templates/nav-context-navbar.hbs index e6eeb5cf547..a3504814f63 100644 --- a/server/sonar-web/src/main/js/apps/nav/templates/nav-context-navbar.hbs +++ b/server/sonar-web/src/main/js/apps/nav/templates/nav-context-navbar.hbs @@ -21,8 +21,8 @@ </div> <ul class="nav navbar-nav nav-tabs"> - <li {{#if isOverviewActive}}class="active"{{/if}}> - <a href="{{componentPermalink component.key}}">{{t 'overview.page'}}</a> + <li {{#isActiveLink '/overview'}}class="active"{{/isActiveLink}}> + <a href="{{componentOverviewPermalink component.key}}">{{t 'overview.page'}}</a> </li> <li {{#isActiveLink '/components'}}class="active"{{/isActiveLink}}> <a href="{{componentBrowsePermalink component.key}}">{{t 'components.page'}}</a> @@ -95,11 +95,11 @@ <a class="dropdown-toggle" data-toggle="dropdown" href="#">{{t 'more'}} <i class="icon-dropdown"></i></a> <ul class="dropdown-menu"> <li class="dropdown-header">{{t 'layout.dashboards'}}</li> - {{#withoutFirst component.dashboards}} + {{#each component.dashboards}} <li> <a href="{{componentDashboardPermalink ../component.key key}}">{{dashboardL10n name}}</a> </li> - {{/withoutFirst}} + {{/each}} {{#if canManageContextDashboards}} <li class="small-divider"></li> <li> diff --git a/server/sonar-web/src/main/js/apps/overview/app.jsx b/server/sonar-web/src/main/js/apps/overview/app.jsx new file mode 100644 index 00000000000..eca47e5cb2b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/app.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import Main from './main'; + +const $ = jQuery; + +export default { + start: function (options) { + $('html').addClass('dashboard-page'); + window.requestMessages().done(() => { + var el = document.querySelector(options.el); + React.render(<Main + component={options.component} + gate={options.gate} + measures={options.measures} + leak={options.leak}/>, el); + }); + } +}; diff --git a/server/sonar-web/src/main/js/apps/overview/card.jsx b/server/sonar-web/src/main/js/apps/overview/card.jsx new file mode 100644 index 00000000000..a22146246d3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/card.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export default React.createClass({ + render() { + return <li className="overview-card">{this.props.children}</li>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/cards.jsx b/server/sonar-web/src/main/js/apps/overview/cards.jsx new file mode 100644 index 00000000000..3d69cf8bf3a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/cards.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export default React.createClass({ + render() { + return <ul className="overview-cards">{this.props.children}</ul>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/gate-condition.jsx b/server/sonar-web/src/main/js/apps/overview/gate-condition.jsx new file mode 100644 index 00000000000..cd2c4ec4f18 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/gate-condition.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import {periodLabel, getPeriodDate} from './helpers/period-label'; +import DrilldownLink from './helpers/drilldown-link'; + +export default React.createClass({ + render() { + const + metricName = window.t('metric', this.props.condition.metric.name, 'name'), + threshold = this.props.condition.level === 'ERROR' ? + this.props.condition.error : this.props.condition.warning, + iconClassName = 'icon-alert-' + this.props.condition.level.toLowerCase(), + period = this.props.condition.period ? + `(${periodLabel(this.props.component.periods, this.props.condition.period)})` : null, + periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period); + + return ( + <div> + <h4 className="overview-gate-condition-metric">{metricName} {period}</h4> + <div className="overview-gate-condition-value"> + <i className={iconClassName}></i> + <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name} + period={this.props.condition.period} periodDate={periodDate}> + <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/> + </DrilldownLink> + <span className="overview-gate-condition-itself"> + {window.t('quality_gates.operator', this.props.condition.op, 'short')} + <Measure value={threshold} type={this.props.condition.metric.type}/> + </span> + </div> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/gate-conditions.jsx b/server/sonar-web/src/main/js/apps/overview/gate-conditions.jsx new file mode 100644 index 00000000000..dddac46e6e4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/gate-conditions.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Cards from './cards'; +import Card from './card'; +import GateCondition from './gate-condition'; + +export default React.createClass({ + render() { + const conditions = this.props.gate.conditions + .filter((c) => { + return c.level !== 'OK'; + }) + .map((c) => { + return ( + <Card key={c.metric.name}> + <GateCondition condition={c} component={this.props.component}/> + </Card> + ); + }); + return <Cards>{conditions}</Cards>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/gate.jsx b/server/sonar-web/src/main/js/apps/overview/gate.jsx new file mode 100644 index 00000000000..af924bd60d7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/gate.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import GateConditions from './gate-conditions'; + +export default React.createClass({ + render: function () { + if (!this.props.gate || !this.props.gate.level) { + return null; + } + + const + badgeClassName = 'badge badge-' + this.props.gate.level.toLowerCase(), + badgeText = window.t('overview.gate', this.props.gate.level); + + return ( + <div className="overview-gate"> + <div className="overview-title"> + {window.t('overview.quality_gate')} + <span className={badgeClassName}>{badgeText}</span> + </div> + <GateConditions gate={this.props.gate} component={this.props.component}/> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/donut.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/donut.jsx new file mode 100644 index 00000000000..58ba8a36736 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/donut.jsx @@ -0,0 +1,41 @@ +import React from 'react'; + +const Sector = React.createClass({ + render() { + const arc = d3.svg.arc() + .outerRadius(this.props.radius) + .innerRadius(this.props.radius - this.props.thickness); + return <path d={arc(this.props.data)} style={{ fill: this.props.fill }}></path>; + } +}); + +export default React.createClass({ + getDefaultProps() { + return { + size: 30, + thickness: 6 + }; + }, + + render() { + const radius = this.props.size / 2; + const pie = d3.layout.pie().sort(null) + .value(d => { + return d.value + }); + const data = this.props.data; + const sectors = pie(data).map((d, i) => { + return <Sector + key={i} + data={d} + fill={data[i].fill} + radius={radius} + thickness={this.props.thickness}/>; + }); + return ( + <svg width={this.props.size} height={this.props.size}> + <g transform={`translate(${radius}, ${radius})`}>{sectors}</g> + </svg> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.jsx new file mode 100644 index 00000000000..d9e9f996be8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import IssuesLink from './issues-link'; + +export default React.createClass({ + render() { + if (this.isIssueMeasure()) { + return this.renderIssuesLink(); + } + + let params = { id: this.props.component, metric: this.props.metric }; + if (this.props.period) { + params.period = this.props.period; + } + + const + query = Object.keys(params).map(function (key) { + return `${key}=${encodeURIComponent(params[key])}`; + }).join('&'), + url = `${baseUrl}/drilldown/measures?${query}`; + + return <a href={url}>{this.props.children}</a>; + }, + + isIssueMeasure() { + const ISSUE_MEASURES = [ + 'violations', + 'blocker_violations', + 'critical_violations', + 'major_violations', + 'minor_violations', + 'info_violations', + 'new_blocker_violations', + 'new_critical_violations', + 'new_major_violations', + 'new_minor_violations', + 'new_info_violations', + 'open_issues', + 'reopened_issues', + 'confirmed_issues', + 'false_positive_issues' + ]; + return ISSUE_MEASURES.indexOf(this.props.metric) !== -1; + }, + + propsToIssueParams() { + let params = {}; + if (this.props.periodDate) { + params.createdAfter = moment(this.props.periodDate).format('YYYY-MM-DDTHH:mm:ssZZ'); + } + switch (this.props.metric) { + case 'blocker_violations': + case 'new_blocker_violations': + _.extend(params, { resolved: 'false', severities: 'BLOCKER' }); + break; + case 'critical_violations': + case 'new_critical_violations': + _.extend(params, { resolved: 'false', severities: 'CRITICAL' }); + break; + case 'major_violations': + case 'new_major_violations': + _.extend(params, { resolved: 'false', severities: 'MAJOR' }); + break; + case 'minor_violations': + case 'new_minor_violations': + _.extend(params, { resolved: 'false', severities: 'MINOR' }); + break; + case 'info_violations': + case 'new_info_violations': + _.extend(params, { resolved: 'false', severities: 'INFO' }); + break; + case 'open_issues': + _.extend(params, { resolved: 'false', statuses: 'OPEN' }); + break; + case 'reopened_issues': + _.extend(params, { resolved: 'false', statuses: 'REOPENED' }); + break; + case 'confirmed_issues': + _.extend(params, { resolved: 'false', statuses: 'CONFIRMED' }); + break; + case 'false_positive_issues': + _.extend(params, { resolutions: 'FALSE-POSITIVE' }); + break; + default: + _.extend(params, { resolved: 'false' }); + } + return params; + }, + + renderIssuesLink() { + return ( + <IssuesLink component={this.props.component} params={this.propsToIssueParams()}> + {this.props.children} + </IssuesLink> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.jsx new file mode 100644 index 00000000000..82831d8c1ab --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.jsx @@ -0,0 +1,8 @@ +import React from 'react'; + +export default React.createClass({ + render: function () { + const url = `${baseUrl}/quality_gates/show/${this.props.gate}`; + return <a href={url}>{this.props.children}</a>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.jsx new file mode 100644 index 00000000000..edfaede9b68 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default React.createClass({ + render: function () { + var params = Object.keys(this.props.params).map((key) => { + return `${key}=${encodeURIComponent(this.props.params[key])}`; + }).join('|'); + var url = `${baseUrl}/component_issues/index?id=${encodeURIComponent(this.props.component)}#${params}`; + return <a href={url}>{this.props.children}</a>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.jsx new file mode 100644 index 00000000000..18ca9270a4b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default React.createClass({ + render: function () { + if (this.props.value == null || isNaN(this.props.value)) { + return null; + } + var formatted = window.formatMeasureVariation(this.props.value, this.props.type); + return <span>{formatted}</span>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/measure.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/measure.jsx new file mode 100644 index 00000000000..b2f398bb666 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/measure.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default React.createClass({ + render: function () { + if (this.props.value == null || isNaN(this.props.value)) { + return null; + } + const formatted = window.formatMeasure(this.props.value, this.props.type); + return <span>{formatted}</span>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/period-label.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.jsx new file mode 100644 index 00000000000..996ea01f96b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.jsx @@ -0,0 +1,15 @@ +export let periodLabel = (periods, periodIndex) => { + let period = _.findWhere(periods, { index: periodIndex }); + if (!period) { + return null; + } + return window.tp(`overview.period.${period.mode}`, period.modeParam); +}; + +export let getPeriodDate = (periods, periodIndex) => { + let period = _.findWhere(periods, { index: periodIndex }); + if (!period) { + return null; + } + return moment(period.date).toDate(); +}; diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.jsx new file mode 100644 index 00000000000..c4f12bf07bd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.jsx @@ -0,0 +1,8 @@ +import React from 'react'; + +export default React.createClass({ + render: function () { + const url = `${baseUrl}/profiles/show?key=${encodeURIComponent(this.props.profile)}`; + return <a href={url}>{this.props.children}</a>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/rating.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/rating.jsx new file mode 100644 index 00000000000..a5337ec828f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/helpers/rating.jsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export default React.createClass({ + render: function () { + if (this.props.value == null || isNaN(this.props.value)) { + return null; + } + const formatted = window.formatMeasure(this.props.value, 'RATING'); + const className = 'rating rating-' + formatted; + return <span className={className}>{formatted}</span>; + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/leak-coverage.jsx b/server/sonar-web/src/main/js/apps/overview/leak-coverage.jsx new file mode 100644 index 00000000000..db5b992cada --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/leak-coverage.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import MeasureVariation from './helpers/measure-variation'; +import DrilldownLink from './helpers/drilldown-link'; +import Donut from './helpers/donut'; + +export default React.createClass({ + render: function () { + const + newCoverage = parseInt(this.props.leak.newCoverage, 10), + tests = this.props.leak.tests, + donutData = [ + { value: newCoverage, fill: '#85bb43' }, + { value: 100 - newCoverage, fill: '#d4333f' } + ]; + + if (newCoverage == null || isNaN(newCoverage)) { + return null; + } + + return ( + <Card> + <div className="measures"> + <div className="measures-chart"> + <Donut data={donutData} size="47"/> + </div> + <div className="measure measure-big" data-metric="new_coverage"> + <span className="measure-value"> + <DrilldownLink component={this.props.component.key} metric="new_coverage" period="3"> + <Measure value={newCoverage} type="PERCENT"/> + </DrilldownLink> + </span> + <span className="measure-name">{window.t('overview.metric.new_coverage')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top measures-chart-indent"> + <li> + <span><MeasureVariation value={tests} type="SHORT_INT"/></span> + <span>{window.t('overview.metric.tests')}</span> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/leak-dups.jsx b/server/sonar-web/src/main/js/apps/overview/leak-dups.jsx new file mode 100644 index 00000000000..20cf2be1eb8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/leak-dups.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import Card from './card'; +import MeasureVariation from './helpers/measure-variation'; +import DrilldownLink from './helpers/drilldown-link'; +import Donut from './helpers/donut'; + +export default React.createClass({ + render: function () { + const + density = this.props.leak.duplications, + lines = this.props.leak.duplicatedLines, + donutData = [ + { value: density, fill: '#f3ca8e' }, + { value: 100 - density, fill: '#e6e6e6' } + ]; + + if (density == null) { + return null; + } + + return ( + <Card> + <div className="measures"> + <div className="measures-chart"> + <Donut data={donutData} size="47"/> + </div> + <div className="measure measure-big" data-metric="duplicated_lines_density"> + <span className="measure-value"> + <MeasureVariation value={density} type="PERCENT"/> + </span> + <span className="measure-name">{window.t('overview.metric.duplications')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top measures-chart-indent"> + <li> + <span><MeasureVariation value={lines} type="SHORT_INT"/></span> + <span>{window.t('overview.metric.duplicated_lines')}</span> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/leak-issues.jsx b/server/sonar-web/src/main/js/apps/overview/leak-issues.jsx new file mode 100644 index 00000000000..3264dd65f26 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/leak-issues.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import MeasureVariation from './helpers/measure-variation'; +import IssuesLink from './helpers/issues-link'; +import DrilldownLink from './helpers/drilldown-link'; +import SeverityIcon from 'components/shared/severity-icon'; +import StatusIcon from 'components/shared/status-icon'; +import SeverityHelper from 'components/shared/severity-helper'; +import {getPeriodDate} from './helpers/period-label'; + +export default React.createClass({ + render: function () { + const + newDebt = this.props.leak.newDebt, + issues = this.props.leak.newIssues, + blockerIssues = this.props.leak.newBlockerIssues, + criticalIssues = this.props.leak.newCriticalIssues, + issuesToReview = this.props.leak.newOpenIssues + this.props.leak.newReopenedIssues, + periodDate = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ'); + + return ( + <Card> + <div className="measures"> + <div className="measure measure-big" data-metric="sqale_index"> + <span className="measure-value"> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: periodDate, facetMode: 'debt' }}> + <Measure value={newDebt} type="SHORT_WORK_DUR"/> + </IssuesLink> + </span> + <span className="measure-name">{window.t('overview.metric.new_debt')}</span> + </div> + <div className="measure measure-big" data-metric="violations"> + <span className="measure-value"> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: periodDate }}> + <Measure value={issues} type="SHORT_INT"/> + </IssuesLink> + </span> + <span className="measure-name">{window.t('overview.metric.new_issues')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top"> + <li> + <span><SeverityIcon severity="BLOCKER"/></span> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: periodDate, severities: 'BLOCKER' }}> + <MeasureVariation value={blockerIssues} type="SHORT_INT"/> + </IssuesLink> + </li> + <li> + <span><SeverityIcon severity="CRITICAL"/></span> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: periodDate, severities: 'CRITICAL' }}> + <MeasureVariation value={criticalIssues} type="SHORT_INT"/> + </IssuesLink> + </li> + <li> + <span><StatusIcon status="OPEN"/></span> + <IssuesLink component={this.props.component.key} + params={{ resolved: 'false', createdAfter: periodDate, statuses: 'OPEN,REOPENED' }}> + <MeasureVariation value={issuesToReview} type="SHORT_INT"/> + </IssuesLink> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/leak-size.jsx b/server/sonar-web/src/main/js/apps/overview/leak-size.jsx new file mode 100644 index 00000000000..d97b0a3f093 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/leak-size.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import Card from './card'; +import MeasureVariation from './helpers/measure-variation'; +import DrilldownLink from './helpers/drilldown-link'; + +export default React.createClass({ + render: function () { + const + lines = this.props.leak.lines, + files = this.props.leak.files; + + return ( + <Card> + <div className="measures"> + <div className="measure measure-big" data-metric="lines"> + <span className="measure-value"> + <MeasureVariation value={lines} type="SHORT_INT"/> + </span> + <span className="measure-name">{window.t('overview.metric.lines')}</span> + </div> + <div className="measure measure-big" data-metric="files"> + <span className="measure-value"> + <MeasureVariation value={files} type="SHORT_INT"/> + </span> + <span className="measure-name">{window.t('overview.metric.files')}</span> + </div> + </div> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/leak.jsx b/server/sonar-web/src/main/js/apps/overview/leak.jsx new file mode 100644 index 00000000000..da44c8f380b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/leak.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import Cards from './cards'; +import LeakIssues from './leak-issues'; +import LeakCoverage from './leak-coverage'; +import LeakSize from './leak-size'; +import LeakDups from './leak-dups'; +import {periodLabel} from './helpers/period-label'; + +export default React.createClass({ + render: function () { + if (_.size(this.props.component.periods) < 3) { + return null; + } + + const period = periodLabel(this.props.component.periods, '3'); + + return ( + <div className="overview-leak"> + <div className="overview-title"> + {window.t('overview.water_leak')} + <span className="overview-leak-period">{period}</span> + </div> + <Cards> + <LeakIssues component={this.props.component} leak={this.props.leak} measures={this.props.measures}/> + <LeakCoverage component={this.props.component} leak={this.props.leak}/> + <LeakDups component={this.props.component} leak={this.props.leak}/> + <LeakSize component={this.props.component} leak={this.props.leak}/> + </Cards> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/main.jsx b/server/sonar-web/src/main/js/apps/overview/main.jsx new file mode 100644 index 00000000000..5a9408f0821 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/main.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import Gate from './gate'; +import Leak from './leak'; +import Nutshell from './nutshell'; +import Meta from './meta'; +import {getPeriodDate} from './helpers/period-label'; + +const $ = jQuery; + +export default React.createClass({ + getInitialState() { + return { leak: this.props.leak, measures: this.props.measures }; + }, + + componentDidMount() { + if (this._hasWaterLeak()) { + this.requestLeakIssues(); + this.requestLeakDebt(); + } + this.requestNutshellIssues(); + this.requestNutshellDebt(); + }, + + _hasWaterLeak() { + return !!_.findWhere(this.props.component.periods, { index: '3' }); + }, + + _requestIssues(data) { + const url = `${baseUrl}/api/issues/search`; + data.ps = 1; + data.componentUuids = this.props.component.id; + return $.get(url, data); + }, + + requestLeakIssues() { + const createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ'); + this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities,statuses' }).done(r => { + const + severitiesFacet = _.findWhere(r.facets, { property: 'severities' }).values, + statusesFacet = _.findWhere(r.facets, { property: 'statuses' }).values; + + this.setState({ + leak: _.extend({}, this.state.leak, { + newIssues: r.total, + newBlockerIssues: _.findWhere(severitiesFacet, { val: 'BLOCKER' }).count, + newCriticalIssues: _.findWhere(severitiesFacet, { val: 'CRITICAL' }).count, + newOpenIssues: _.findWhere(statusesFacet, { val: 'OPEN' }).count, + newReopenedIssues: _.findWhere(statusesFacet, { val: 'REOPENED' }).count + }) + }); + }); + }, + + requestNutshellIssues() { + this._requestIssues({ resolved: 'false', facets: 'severities,statuses' }).done(r => { + const + severitiesFacet = _.findWhere(r.facets, { property: 'severities' }).values, + statusesFacet = _.findWhere(r.facets, { property: 'statuses' }).values; + + this.setState({ + measures: _.extend({}, this.state.measures, { + issues: r.total, + blockerIssues: _.findWhere(severitiesFacet, { val: 'BLOCKER' }).count, + criticalIssues: _.findWhere(severitiesFacet, { val: 'CRITICAL' }).count, + openIssues: _.findWhere(statusesFacet, { val: 'OPEN' }).count, + reopenedIssues: _.findWhere(statusesFacet, { val: 'REOPENED' }).count + }) + }); + }); + }, + + requestLeakDebt() { + const createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ'); + this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities', facetMode: 'debt' }).done(r => { + this.setState({ + leak: _.extend({}, this.state.leak, { newDebt: r.debtTotal }) + }); + }); + }, + + requestNutshellDebt() { + this._requestIssues({ resolved: 'false', facets: 'severities', facetMode: 'debt' }).done(r => { + this.setState({ + measures: _.extend({}, this.state.measures, { debt: r.debtTotal }) + }); + }); + }, + + render() { + return ( + <div className="overview"> + <div className="overview-main"> + <Gate component={this.props.component} gate={this.props.gate}/> + <Leak component={this.props.component} leak={this.state.leak} measures={this.state.measures}/> + <Nutshell component={this.props.component} measures={this.state.measures}/> + </div> + <Meta component={this.props.component}/> + </div> + ); + } +}) diff --git a/server/sonar-web/src/main/js/apps/overview/meta.jsx b/server/sonar-web/src/main/js/apps/overview/meta.jsx new file mode 100644 index 00000000000..7971125ca6b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import ProfileLink from './helpers/profile-link'; +import GateLink from './helpers/gate-link'; + +export default React.createClass({ + render: function () { + const + profiles = (this.props.component.profiles || []).map(function (profile) { + return ( + <li key={profile.key}> + <span className="note little-spacer-right">({profile.language})</span> + <ProfileLink profile={profile.key}>{profile.name}</ProfileLink> + </li> + ); + }), + links = (this.props.component.links || []).map(function (link) { + const iconClassName = `little-spacer-right icon-color-link icon-${link.type}`; + return ( + <li key={link.type}> + <i className={iconClassName}></i> + <a href={link.href} target="_blank">{link.name}</a> + </li> + ); + }); + + const descriptionCard = this.props.component.description ? ( + <div className="overview-card"> + <div className="overview-meta-description">{this.props.component.description}</div> + </div> + ) : null, + + linksCard = _.size(this.props.component.links) > 0 ? ( + <div className="overview-card"> + <ul className="overview-meta-list">{links}</ul> + </div> + ) : null, + + profilesCard = _.size(this.props.component.profiles) > 0 ? ( + <div className="overview-card"> + <h4 className="overview-meta-header">{window.t('overview.quality_profiles')}</h4> + <ul className="overview-meta-list">{profiles}</ul> + </div> + ) : null, + + gateCard = this.props.component.gate ? ( + <div className="overview-card"> + <h4 className="overview-meta-header">{window.t('overview.quality_gate')}</h4> + <ul className="overview-meta-list"> + <li> + {this.props.component.gate.isDefault ? + <span className="note little-spacer-right">(Default)</span> : null} + <GateLink gate={this.props.component.gate.key}>{this.props.component.gate.name}</GateLink> + </li> + </ul> + </div> + ) : null; + + return ( + <div className="overview-meta"> + {descriptionCard} + {linksCard} + {profilesCard} + {gateCard} + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.jsx b/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.jsx new file mode 100644 index 00000000000..a7880041ec5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import DrilldownLink from './helpers/drilldown-link'; +import Donut from './helpers/donut'; + +export default React.createClass({ + render: function () { + const + coverage = this.props.measures.coverage, + tests = this.props.measures.tests, + donutData = [ + { value: coverage, fill: '#85bb43' }, + { value: 100 - coverage, fill: '#d4333f' } + ]; + + if (coverage == null) { + return null; + } + + return ( + <Card> + <div className="measures"> + <div className="measures-chart"> + <Donut data={donutData} size="47"/> + </div> + <div className="measure measure-big"> + <span className="measure-value"> + <DrilldownLink component={this.props.component.key} metric="overall_coverage"> + <Measure value={coverage} type="PERCENT"/> + </DrilldownLink> + </span> + <span className="measure-name">{window.t('overview.metric.coverage')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top measures-chart-indent"> + <li> + <DrilldownLink component={this.props.component.key} metric="tests"> + <Measure value={tests} type="SHORT_INT"/> + </DrilldownLink> + <span>{window.t('overview.metric.tests')}</span> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-dups.jsx b/server/sonar-web/src/main/js/apps/overview/nutshell-dups.jsx new file mode 100644 index 00000000000..b0c1df3f3ba --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/nutshell-dups.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import DrilldownLink from './helpers/drilldown-link'; +import Donut from './helpers/donut'; + +export default React.createClass({ + render: function () { + const + density = this.props.measures.duplications, + lines = this.props.measures.duplicatedLines, + donutData = [ + { value: density, fill: '#f3ca8e' }, + { value: 100 - density, fill: '#e6e6e6' } + ]; + + if (density == null) { + return null; + } + + return ( + <Card> + <div className="measures"> + <div className="measures-chart"> + <Donut data={donutData} size="47"/> + </div> + <div className="measure measure-big"> + <span className="measure-value"> + <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density"> + <Measure value={density} type="PERCENT"/> + </DrilldownLink> + </span> + <span className="measure-name">{window.t('overview.metric.duplications')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top measures-chart-indent"> + <li> + <DrilldownLink component={this.props.component.key} metric="duplicated_lines"> + <Measure value={lines} type="SHORT_INT"/> + </DrilldownLink> + <span>{window.t('overview.metric.duplicated_lines')}</span> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-issues.jsx b/server/sonar-web/src/main/js/apps/overview/nutshell-issues.jsx new file mode 100644 index 00000000000..d1c242b620d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/nutshell-issues.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import Rating from './helpers/rating'; +import IssuesLink from './helpers/issues-link'; +import DrilldownLink from './helpers/drilldown-link'; +import SeverityIcon from 'components/shared/severity-icon'; +import SeverityHelper from 'components/shared/severity-helper'; +import StatusIcon from 'components/shared/status-icon'; + +export default React.createClass({ + render: function () { + const + debt = this.props.measures.debt, + rating = this.props.measures.sqaleRating, + issues = this.props.measures.issues, + blockerIssues = this.props.measures.blockerIssues, + criticalIssues = this.props.measures.criticalIssues, + issuesToReview = this.props.measures.openIssues + this.props.measures.reopenedIssues; + + return ( + <Card> + <div className="measures"> + <div className="measure measure-big" data-metric="sqale_rating"> + <DrilldownLink component={this.props.component.key} metric="sqale_rating"> + <Rating value={rating}/> + </DrilldownLink> + </div> + <div className="measure measure-big" data-metric="sqale_index"> + <span className="measure-value"> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}> + <Measure value={debt} type="SHORT_WORK_DUR"/> + </IssuesLink> + </span> + <span className="measure-name">{window.t('overview.metric.debt')}</span> + </div> + <div className="measure measure-big" data-metric="violations"> + <span className="measure-value"> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}> + <Measure value={issues} type="SHORT_INT"/> + </IssuesLink> + </span> + <span className="measure-name">{window.t('overview.metric.issues')}</span> + </div> + </div> + <ul className="list-inline big-spacer-top"> + <li> + <span><SeverityIcon severity="BLOCKER"/></span> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'BLOCKER' }}> + <Measure value={blockerIssues} type="SHORT_INT"/> + </IssuesLink> + </li> + <li> + <span><SeverityIcon severity="CRITICAL"/></span> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'CRITICAL' }}> + <Measure value={criticalIssues} type="SHORT_INT"/> + </IssuesLink> + </li> + <li> + <span><StatusIcon status="OPEN"/></span> + <IssuesLink component={this.props.component.key} params={{ resolved: 'false', statuses: 'OPEN,REOPENED' }}> + <Measure value={issuesToReview} type="SHORT_INT"/> + </IssuesLink> + </li> + </ul> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-size.jsx b/server/sonar-web/src/main/js/apps/overview/nutshell-size.jsx new file mode 100644 index 00000000000..65911191227 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/nutshell-size.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import Card from './card'; +import Measure from './helpers/measure'; +import DrilldownLink from './helpers/drilldown-link'; + +export default React.createClass({ + render: function () { + const + lines = this.props.measures['lines'], + files = this.props.measures['files']; + + return ( + <Card> + <div className="measures"> + <div className="measure measure-big" data-metric="lines"> + <span className="measure-value"> + <DrilldownLink component={this.props.component.key} metric="lines"> + <Measure value={lines} type="SHORT_INT"/> + </DrilldownLink> + </span> + <span className="measure-name">{window.t('overview.metric.lines')}</span> + </div> + <div className="measure measure-big" data-metric="files"> + <span className="measure-value"> + <DrilldownLink component={this.props.component.key} metric="files"> + <Measure value={files} type="SHORT_INT"/> + </DrilldownLink> + </span> + <span className="measure-name">{window.t('overview.metric.files')}</span> + </div> + </div> + </Card> + ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell.jsx b/server/sonar-web/src/main/js/apps/overview/nutshell.jsx new file mode 100644 index 00000000000..f15d3b8db5d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/nutshell.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import Cards from './cards'; +import NutshellIssues from './nutshell-issues'; +import NutshellCoverage from './nutshell-coverage'; +import NutshellSize from './nutshell-size'; +import NutshellDups from './nutshell-dups'; + +export default React.createClass({ + render: function () { + const props = { measures: this.props.measures, component: this.props.component }; + return ( + <div className="overview-nutshell"> + <div className="overview-title">{window.t('overview.project_in_a_nutshell')}</div> + <Cards> + <NutshellIssues {...props}/> + <NutshellCoverage {...props}/> + <NutshellDups {...props}/> + <NutshellSize {...props}/> + </Cards> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/components/common/handlebars-extensions.js b/server/sonar-web/src/main/js/components/common/handlebars-extensions.js index e8e8c13f3e9..c85329a0535 100644 --- a/server/sonar-web/src/main/js/components/common/handlebars-extensions.js +++ b/server/sonar-web/src/main/js/components/common/handlebars-extensions.js @@ -32,6 +32,10 @@ return baseUrl + '/dashboard/index?id=' + encodeURIComponent(componentKey); }); + Handlebars.registerHelper('componentOverviewPermalink', function (componentKey) { + return baseUrl + '/overview/index?id=' + encodeURIComponent(componentKey); + }); + Handlebars.registerHelper('componentDashboardPermalink', function (componentKey, dashboardKey) { var params = [ { key: 'id', value: componentKey }, diff --git a/server/sonar-web/src/main/js/components/shared/severity-helper.jsx b/server/sonar-web/src/main/js/components/shared/severity-helper.jsx new file mode 100644 index 00000000000..a0e931aca2f --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/severity-helper.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import SeverityIcon from './severity-icon'; + +export default React.createClass({ + render() { + if (!this.props.severity) { + return null; + } + return ( + <span> + <SeverityIcon severity={this.props.severity}/> + + {window.t('severity', this.props.severity)} + </span> + ); + } +}); diff --git a/server/sonar-web/src/main/js/components/shared/severity-icon.jsx b/server/sonar-web/src/main/js/components/shared/severity-icon.jsx new file mode 100644 index 00000000000..87aec17ea7b --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/severity-icon.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default React.createClass({ + render() { + if (!this.props.severity) { + return null; + } + var className = 'icon-severity-' + this.props.severity.toLowerCase(); + return <i className={className}></i>; + } +}); diff --git a/server/sonar-web/src/main/js/components/shared/status-helper.jsx b/server/sonar-web/src/main/js/components/shared/status-helper.jsx new file mode 100644 index 00000000000..033fd3ff786 --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/status-helper.jsx @@ -0,0 +1,26 @@ +define([ + 'libs/third-party/react', + './status-icon' +], function (React, StatusIcon) { + + return React.createClass({ + render: function () { + if (!this.props.status) { + return null; + } + var resolution; + if (this.props.resolution) { + resolution = ' (' + window.t('issue.resolution', this.props.resolution) + ')'; + } + return ( + <span> + <StatusIcon status={this.props.status}/> + + {window.t('issue.status', this.props.status)} + {resolution} + </span> + ); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/components/shared/status-icon.jsx b/server/sonar-web/src/main/js/components/shared/status-icon.jsx new file mode 100644 index 00000000000..5acd210745e --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/status-icon.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default React.createClass({ + render() { + if (!this.props.status) { + return null; + } + var className = 'icon-status-' + this.props.status.toLowerCase(); + return <i className={className}></i>; + } +}); diff --git a/server/sonar-web/src/main/js/components/source-viewer/main.js b/server/sonar-web/src/main/js/components/source-viewer/main.js index 34df4821849..1a87855f087 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/main.js +++ b/server/sonar-web/src/main/js/components/source-viewer/main.js @@ -125,7 +125,7 @@ define([ var that = this, opts = typeof options === 'object' ? options : {}, finalize = function () { - that.requestIssues().done(function () { + that.requestNutshellIssues().done(function () { that.render(); that.trigger('loaded'); }); @@ -269,7 +269,7 @@ define([ }); }, - requestIssues: function () { + requestNutshellIssues: function () { var that = this, options = { data: { diff --git a/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js b/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js index 0341d42b883..54ec351b29b 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js +++ b/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js @@ -31,7 +31,7 @@ define([ initialize: function () { var that = this, - requests = [this.requestMeasures(), this.requestIssues()]; + requests = [this.requestMeasures(), this.requestNutshellIssues()]; if (this.model.get('isUnitTest')) { requests.push(this.requestTests()); } @@ -132,7 +132,7 @@ define([ }); }, - requestIssues: function () { + requestNutshellIssues: function () { var that = this, url = baseUrl + '/api/issues/search', options = { diff --git a/server/sonar-web/src/main/js/libs/application.js b/server/sonar-web/src/main/js/libs/application.js index e26addbfd1b..7d141563f49 100644 --- a/server/sonar-web/src/main/js/libs/application.js +++ b/server/sonar-web/src/main/js/libs/application.js @@ -465,6 +465,7 @@ function closeModalWindow () { * @returns {string} */ var shortDurationFormatter = function (value) { + value = parseInt(value, 10); if (value === 0) { return '0'; } @@ -518,6 +519,9 @@ function closeModalWindow () { 'PERCENT': function (value) { return numeral(+value / 100).format('0,0.0%'); }, + 'SHORT_PERCENT': function (value) { + return numeral(+value / 100).format('0,0%'); + }, 'WORK_DUR': durationFormatter, 'SHORT_WORK_DUR': shortDurationFormatter, 'RATING': ratingFormatter diff --git a/server/sonar-web/src/main/js/widgets/issue-filter/widget.js b/server/sonar-web/src/main/js/widgets/issue-filter/widget.js index 97c72ca75a1..e46c6377815 100644 --- a/server/sonar-web/src/main/js/widgets/issue-filter/widget.js +++ b/server/sonar-web/src/main/js/widgets/issue-filter/widget.js @@ -258,7 +258,7 @@ define(['./templates'], function () { this.listenTo(this.model, 'change', this.render); this.conf = byDistributionConf[this.options.distributionAxis]; this.query = this.getParsedQuery(); - this.requestIssues(); + this.requestNutshellIssues(); }, getParsedQuery: function () { @@ -317,7 +317,7 @@ define(['./templates'], function () { }); }, - requestIssues: function () { + requestNutshellIssues: function () { var that = this, facetMode = this.options.displayMode, url = baseUrl + '/api/issues/search', diff --git a/server/sonar-web/src/main/less/components/badges.less b/server/sonar-web/src/main/less/components/badges.less index 756b3d8c0bf..6ed82633d46 100644 --- a/server/sonar-web/src/main/less/components/badges.less +++ b/server/sonar-web/src/main/less/components/badges.less @@ -59,10 +59,17 @@ &:hover, &:focus, &:active { color: @blue; } } -.badge-success { +.badge-success, +.badge-ok { background-color: @green; } -.badge-warning { +.badge-warning, +.badge-warn { background-color: @orange; } + +.badge-danger, +.badge-error { + background-color: @red +} diff --git a/server/sonar-web/src/main/less/components/measures.less b/server/sonar-web/src/main/less/components/measures.less index c7ef0a761a6..eecb0d2e63f 100644 --- a/server/sonar-web/src/main/less/components/measures.less +++ b/server/sonar-web/src/main/less/components/measures.less @@ -57,6 +57,10 @@ } } +.measures-chart-indent { + padding-left: 90px; +} + .measure { line-height: 1.3333333333333; } @@ -108,7 +112,8 @@ vertical-align: middle; .measure-name { - font-size: 16px; + margin-top: 2px; + font-size: 15px; font-weight: 300; } @@ -127,15 +132,14 @@ } .measure-one-line { - .justify; + .clearfix; .measure-name { - display: inline-block; + float: left; } .measure-value { - display: inline-block; - text-align: right; + float: right; } } diff --git a/server/sonar-web/src/main/less/init/icons.less b/server/sonar-web/src/main/less/init/icons.less index 42a0c0e682d..ca4a8b2b49f 100644 --- a/server/sonar-web/src/main/less/init/icons.less +++ b/server/sonar-web/src/main/less/init/icons.less @@ -64,6 +64,7 @@ a[class^="icon-"], a[class*=" icon-"] { .icon-black { color: @baseFontColor; } .icon-red { color: @red; } .icon-green { color: @green; } +.icon-color-link { color: @darkBlue; } /* diff --git a/server/sonar-web/src/main/less/pages.less b/server/sonar-web/src/main/less/pages.less index bbd9f119eaf..b0e1b6ce99a 100644 --- a/server/sonar-web/src/main/less/pages.less +++ b/server/sonar-web/src/main/less/pages.less @@ -1,22 +1,3 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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 "pages/analysis-reports"; @import "pages/coding-rules"; @import "pages/dashboard"; @@ -26,3 +7,4 @@ @import "pages/quality-gates"; @import "pages/maintenance"; @import "pages/login"; +@import "pages/overview"; diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less new file mode 100644 index 00000000000..e4ec6a5bb44 --- /dev/null +++ b/server/sonar-web/src/main/less/pages/overview.less @@ -0,0 +1,190 @@ +@import (reference) "../variables"; +@import (reference) "../mixins"; +@import (reference) "../init/type"; + +.overview { + display: table; + width: 100%; + min-height: ~"calc(100vh - @{navbarGlobalHeight} - @{navbarContextHeight} - @{pageFooterHeight})"; +} + +.overview-main { + display: table-cell; + vertical-align: top; + .box-sizing(border-box); + background-color: #fff; +} + +.overview-gate { + .clearfix; + padding: 50px 30px; +} + +.overview-gate-box { + float: left; + .size(120px, 70px); + padding: 10px; + .box-sizing(border-box); + line-height: 24px; + color: #fff; + font-size: 16px; + font-weight: 300; +} + +.overview-gate-box-error { background-color: @red; } + +.overview-gate-box-warn { background-color: @orange; } + +.overview-gate-box-ok { background-color: @green; } + +.overview-gate-conditions { + line-height: 70px; + font-size: 0; + white-space: nowrap; + overflow: hidden; + + & > li { + display: inline-block; + vertical-align: middle; + padding: 0 20px; + .box-sizing(border-box); + font-size: @baseFontSize; + line-height: 1; + } +} + +.overview-gate-condition-metric { + //color: mix(@baseFontColor, @barBackgroundColor, 70%); + font-weight: 300; + font-size: 15px; + //letter-spacing: 0.03em; +} + +.overview-gate-condition-value { + margin-top: 8px; + font-weight: 300; + font-size: 20px; + + i { + position: relative; + top: -1px; + } +} + +.overview-gate-condition-itself { + padding-left: 4px; + color: mix(@baseFontColor, @barBackgroundColor, 70%); + font-size: 13px; +} + +.overview-gate-condition-level { + margin-top: 8px; +} + +.overview-leak { + padding: 50px 30px; + border-top: 1px solid @barBorderColor; + border-bottom: 1px solid @barBorderColor; +} + +.overview-title { + margin-bottom: 20px; + font-size: 24px; + font-weight: 300; + + & > .badge { + position: relative; + top: -2px; + margin-left: 15px; + padding: 8px 15px; + font-size: 16px; + } +} + +.overview-leak-period { + margin-left: 10px; + font-size: 16px; +} + +.overview-nutshell { + padding: 50px 30px; +} + +.overview-cards { +} + +.overview-card { + display: inline-block; + vertical-align: top; + width: 200px; + margin-right: 30px; + .box-sizing(border-box); + + &:last-child { margin-right: 0; } + + .overview-main & { + font-weight: 300; + font-size: 14px; + } +} + +.overview-measure { + font-size: 28px; +} + +.overview-measure-label { + font-size: 16px; +} + +.overview-meta { + display: table-cell; + vertical-align: top; + width: 180px; + padding: 30px; + border-left: 1px solid @barBorderColor; + background-color: @barBackgroundColor; + + .panel { + border: none !important; + } +} + +.overview-meta-description { + line-height: 1.5; +} + +.overview-meta-header { + color: #797979; +} + +.overview-meta-list { + & > li { + padding-bottom: 4px; + .text-ellipsis; + } +} + +@media (max-width: 1200px) { + .overview { + display: block; + } + + .overview-main { + display: block; + } + + .overview-meta { + display: block; + width: auto; + border-top: 1px solid @barBorderColor; + border-left: none; + } +} + +@media (min-width: 1201px) { + .overview-meta .overview-card { + width: 180px; + margin-right: 0; + margin-bottom: 30px; + } +} diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb index 9ec1e477893..63b7a4c7636 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb @@ -17,6 +17,7 @@ # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # +include ERB::Util class DashboardController < ApplicationController @@ -26,6 +27,10 @@ class DashboardController < ApplicationController def index load_resource() + if @resource && !params[:did] + overview_url = url_for({:controller => 'overview', :action => :index}) + '?id=' + url_encode(@resource.key) + redirect_to overview_url + end if !@resource || @resource.display_dashboard? redirect_if_bad_component() load_dashboard() diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb new file mode 100644 index 00000000000..746bc651be8 --- /dev/null +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb @@ -0,0 +1,29 @@ +# +# SonarQube, open source software quality management tool. +# Copyright (C) 2008-2014 SonarSource +# mailto:contact AT sonarsource DOT com +# +# SonarQube 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. +# +# SonarQube 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. +# +class OverviewController < ApplicationController + before_filter :init_resource_for_user_role + + SECTION=Navigation::SECTION_RESOURCE + + def index + + end + +end diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb new file mode 100644 index 00000000000..b3848046382 --- /dev/null +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb @@ -0,0 +1,164 @@ +<% + links_size = @resource.project_links.size + + profiles = [] + qprofiles_measure = @snapshot.measure(Metric::QUALITY_PROFILES) + if qprofiles_measure && !qprofiles_measure.data.blank? + profiles = JSON.parse qprofiles_measure.data + end + profiles_size = profiles.size + + is_gate_default = false + gate = nil + gate_id = Property.value('sonar.qualitygate', @resource && @resource.id, nil) + unless gate_id + gate_id=Property.value('sonar.qualitygate', nil, nil) + is_gate_default = false || gate_id + end + if gate_id + gate = Internal.quality_gates.get(gate_id.to_i) + end +%> + +<% m = @snapshot.measure(Metric::QUALITY_GATE_DETAILS) + if m && !m.data.blank? + details = JSON.parse m.data + m.alert_status = details['level'] + raw_conditions = details['conditions'] + conditions = [] + missing_metric = false + raw_conditions.each do |condition| + if metric(condition['metric']).nil? + missing_metric = true + else + conditions << condition + end + end + alert_metric = metric(Metric::ALERT_STATUS) + end +%> + +<% content_for :extra_script do %> + <script> + (function () { + var component = { + id: '<%= @resource.uuid %>', + key: '<%= @resource.key %>', + description: '<%= @resource.description %>', + periods: [ + <% + if @snapshot.project_snapshot.periods? + (1..5).each do |index| + if @snapshot.period_mode(index) + %> + { + index: '<%= index -%>', + mode: '<%= @snapshot.period_mode(index) -%>', + modeParam: '<%= @snapshot.period_param(index) -%>', + date: '<%= @snapshot.period_datetime(index).to_date.strftime('%FT%T%z') -%>' + }, + <% end %> + <% end %> + <% end %> + ], + links: [ + <% @resource.project_links.sort.each_with_index do |link, index| %> + { + name: '<%= escape_javascript link.name -%>', + type: '<%= escape_javascript link.link_type -%>', + href: '<%= escape_javascript link.href -%>' + }<% if index < links_size - 1 %>, <% end -%> + <% end %> + ], + profiles: [ + <% profiles.each_with_index do |profile, index| %> + { + name: '<%= escape_javascript profile['name'] -%>', + key: '<%= escape_javascript profile['key']-%>', + language: '<%= escape_javascript Api::Utils.language_name(profile['language']) -%>' + }<% if index < profiles_size - 1 %>, <% end -%> + <% end %> + ], + <% if gate %> + gate: { + name: '<%= escape_javascript gate.getName() -%>', + key: <%= escape_javascript gate_id -%>, + isDefault: <%= is_gate_default -%> + } + <% end %> + }; + + <% if m %> + var gate = { + level: '<%= m.alert_status -%>', + conditions: [ + <% conditions.sort_by {|condition| [ -condition['level'].length, metric(condition['metric']).short_name] }.each do |condition| %> + <% metric = metric(condition['metric']) %> + { + level: '<%= escape_javascript condition['level'] %>', + metric: { + name: '<%= escape_javascript metric.name %>', + type: '<%= escape_javascript metric.value_type %>' + }, + op: '<%= escape_javascript condition['op'] %>', + period: '<%= condition['period'] %>', + warning: '<%= escape_javascript condition['warning'] %>', + error: '<%= escape_javascript condition['error'] %>', + actual: '<%= escape_javascript condition['actual'] %>', + }, + <% end %> + ] + }; + <% else %> + var gate = null; + <% end %> + + var measures = { + + // issues + sqaleRating: '<%= @snapshot.measure('sqale_rating').value -%>', + + // coverage + <% if @snapshot.measure('overall_coverage') %> + coverage: '<%= @snapshot.measure('overall_coverage').value -%>', + <% end %> + <% if @snapshot.measure('tests') %> + tests: '<%= @snapshot.measure('tests').value -%>', + <% end %> + + // duplications + duplications: '<%= @snapshot.measure('duplicated_lines_density').value -%>', + duplicatedLines: '<%= @snapshot.measure('duplicated_lines').value -%>', + duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').value -%>', + + // size + lines: '<%= @snapshot.measure('lines').value -%>', + files: '<%= @snapshot.measure('files').value -%>' + }; + + var leak = { + + // coverage + <% if @snapshot.measure('new_overall_coverage') %> + newCoverage: '<%= @snapshot.measure('new_overall_coverage').variation(3) -%>', + <% end %> + <% if @snapshot.measure('tests') %> + tests: '<%= @snapshot.measure('tests').variation(3) -%>', + <% end %> + + // duplications + duplications: '<%= @snapshot.measure('duplicated_lines_density').variation(3) -%>', + duplicatedLines: '<%= @snapshot.measure('duplicated_lines').variation(3) -%>', + duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').variation(3) -%>', + + // size + lines: '<%= @snapshot.measure('lines').variation(3) -%>', + files: '<%= @snapshot.measure('files').variation(3) -%>' + }; + + require(['apps/overview/app'], function (App) { + App.start({ el: '#content', component: component, gate: gate, measures: measures, leak: leak }); + }); + })(); + </script> +<% end %> diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index e7bcba4207d..18dfdf16d5b 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3020,3 +3020,35 @@ update_center.status.COMPATIBLE=Compatible update_center.status.INCOMPATIBLE=Incompatible update_center.status.REQUIRES_SYSTEM_UPGRADE=Requires system update update_center.status.DEPS_REQUIRE_SYSTEM_UPGRADE=Some of dependencies requires system update + + + +#------------------------------------------------------------------------------ +# +# OVERVIEW +# +#------------------------------------------------------------------------------ +overview.quality_gate=Quality Gate +overview.quality_profiles=Quality Profiles +overview.water_leak=Water Leak +overview.project_in_a_nutshell=Project In a Nutshell + +overview.metric.new_coverage=New Coverage +overview.metric.tests=tests +overview.metric.duplications=Duplications +overview.metric.duplicated_lines=lines +overview.metric.debt=Debt +overview.metric.issues=Issues +overview.metric.new_debt=New Debt +overview.metric.new_issues=New Issues +overview.metric.lines=Lines +overview.metric.files=Files +overview.metric.coverage=Coverage + +overview.period.previous_version=since {0} +overview.period.previous_analysis=since previous analysis +overview.period.days=last {0} days + +overview.gate.ERROR=Failed +overview.gate.WARN=Warning +overview.gate.OK=Passed |