aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-08-04 17:53:08 +0200
committerStas Vilchik <vilchiks@gmail.com>2015-08-10 17:11:54 +0200
commitcc871d7b8c4af1f66234830719d27e45d4136334 (patch)
tree4a0e191441ba2fcd706853a74d6288436640bc85 /server/sonar-web/src/main/js
parenta3df8c53bec6e8e61cac61a41c8e7489b5bfb098 (diff)
downloadsonarqube-cc871d7b8c4af1f66234830719d27e45d4136334.tar.gz
sonarqube-cc871d7b8c4af1f66234830719d27e45d4136334.zip
SONAR-6331 add project overview
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/component-viewer/main.js2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-home-view.js4
-rw-r--r--server/sonar-web/src/main/js/apps/nav/context-navbar-view.js6
-rw-r--r--server/sonar-web/src/main/js/apps/nav/templates/nav-context-navbar.hbs8
-rw-r--r--server/sonar-web/src/main/js/apps/overview/app.jsx18
-rw-r--r--server/sonar-web/src/main/js/apps/overview/card.jsx7
-rw-r--r--server/sonar-web/src/main/js/apps/overview/cards.jsx7
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate-condition.jsx35
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate-conditions.jsx21
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate.jsx24
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/donut.jsx41
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.jsx96
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/gate-link.jsx8
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/issues-link.jsx11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.jsx11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/measure.jsx11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/period-label.jsx15
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/profile-link.jsx8
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/rating.jsx12
-rw-r--r--server/sonar-web/src/main/js/apps/overview/leak-coverage.jsx46
-rw-r--r--server/sonar-web/src/main/js/apps/overview/leak-dups.jsx43
-rw-r--r--server/sonar-web/src/main/js/apps/overview/leak-issues.jsx70
-rw-r--r--server/sonar-web/src/main/js/apps/overview/leak-size.jsx31
-rw-r--r--server/sonar-web/src/main/js/apps/overview/leak.jsx32
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main.jsx101
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta.jsx67
-rw-r--r--server/sonar-web/src/main/js/apps/overview/nutshell-coverage.jsx47
-rw-r--r--server/sonar-web/src/main/js/apps/overview/nutshell-dups.jsx47
-rw-r--r--server/sonar-web/src/main/js/apps/overview/nutshell-issues.jsx69
-rw-r--r--server/sonar-web/src/main/js/apps/overview/nutshell-size.jsx35
-rw-r--r--server/sonar-web/src/main/js/apps/overview/nutshell.jsx23
-rw-r--r--server/sonar-web/src/main/js/components/common/handlebars-extensions.js4
-rw-r--r--server/sonar-web/src/main/js/components/shared/severity-helper.jsx17
-rw-r--r--server/sonar-web/src/main/js/components/shared/severity-icon.jsx11
-rw-r--r--server/sonar-web/src/main/js/components/shared/status-helper.jsx26
-rw-r--r--server/sonar-web/src/main/js/components/shared/status-icon.jsx11
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/main.js4
-rw-r--r--server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js4
-rw-r--r--server/sonar-web/src/main/js/libs/application.js4
-rw-r--r--server/sonar-web/src/main/js/widgets/issue-filter/widget.js4
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'}}&nbsp;<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>&nbsp;
+ <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>&nbsp;
+ <span className="overview-gate-condition-itself">
+ {window.t('quality_gates.operator', this.props.condition.op, 'short')}&nbsp;
+ <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>&nbsp;
+ <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>&nbsp;
+ <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>&nbsp;
+ <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>&nbsp;
+ <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>&nbsp;
+ <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>&nbsp;
+ <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>&nbsp;
+ <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>&nbsp;
+ <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>&nbsp;
+ <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>&nbsp;
+ <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}/>
+ &nbsp;
+ {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}/>
+ &nbsp;
+ {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',