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 /server/sonar-web/src/main/js | |
parent | a3df8c53bec6e8e61cac61a41c8e7489b5bfb098 (diff) | |
download | sonarqube-cc871d7b8c4af1f66234830719d27e45d4136334.tar.gz sonarqube-cc871d7b8c4af1f66234830719d27e45d4136334.zip |
SONAR-6331 add project overview
Diffstat (limited to 'server/sonar-web/src/main/js')
41 files changed, 1026 insertions, 19 deletions
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', |