aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-10-19 16:40:37 +0200
committerStas Vilchik <vilchiks@gmail.com>2015-10-20 11:03:41 +0200
commit187f501d31cba9ff220c44bbcd222768a4eee40e (patch)
tree51e71412637be96d9458c761f3d09dc236570d22
parent26817f2f1b31d777b681cc08f3a06db201521007 (diff)
downloadsonarqube-187f501d31cba9ff220c44bbcd222768a4eee40e.tar.gz
sonarqube-187f501d31cba9ff220c44bbcd222768a4eee40e.zip
SONAR-6331 add project overview page
-rw-r--r--server/sonar-web/package.json3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/app.js26
-rw-r--r--server/sonar-web/src/main/js/apps/overview/card.js7
-rw-r--r--server/sonar-web/src/main/js/apps/overview/cards.js7
-rw-r--r--server/sonar-web/src/main/js/apps/overview/empty.js13
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate-condition.js33
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate-conditions.js21
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate-empty.js14
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate.js25
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/donut.js34
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js95
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js8
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/measure.js11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/period-label.js18
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js8
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/rating.js12
-rw-r--r--server/sonar-web/src/main/js/apps/overview/leak-coverage.js46
-rw-r--r--server/sonar-web/src/main/js/apps/overview/leak-dups.js42
-rw-r--r--server/sonar-web/src/main/js/apps/overview/leak-issues.js69
-rw-r--r--server/sonar-web/src/main/js/apps/overview/leak-size.js30
-rw-r--r--server/sonar-web/src/main/js/apps/overview/leak.js33
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main.js102
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta.js68
-rw-r--r--server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js47
-rw-r--r--server/sonar-web/src/main/js/apps/overview/nutshell-dups.js47
-rw-r--r--server/sonar-web/src/main/js/apps/overview/nutshell-issues.js68
-rw-r--r--server/sonar-web/src/main/js/apps/overview/nutshell-size.js35
-rw-r--r--server/sonar-web/src/main/js/apps/overview/nutshell.js23
-rw-r--r--server/sonar-web/src/main/js/helpers/Url.js2
-rw-r--r--server/sonar-web/src/main/js/libs/application.js2
-rw-r--r--server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js8
-rw-r--r--server/sonar-web/src/main/less/components/measures.less4
-rw-r--r--server/sonar-web/src/main/less/components/navbar.less1
-rw-r--r--server/sonar-web/src/main/less/components/ui.less2
-rw-r--r--server/sonar-web/src/main/less/pages.less1
-rw-r--r--server/sonar-web/src/main/less/pages/overview.less195
-rw-r--r--server/sonar-web/src/main/less/variables.less2
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb29
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb181
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties32
42 files changed, 1418 insertions, 8 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json
index 0b9f0251293..2d21537cd6d 100644
--- a/server/sonar-web/package.json
+++ b/server/sonar-web/package.json
@@ -52,7 +52,8 @@
},
"browserify-shim": {
"jquery": "global:jQuery",
- "underscore": "global:_"
+ "underscore": "global:_",
+ "d3": "global:d3"
},
"browserify": {
"transform": [
diff --git a/server/sonar-web/src/main/js/apps/overview/app.js b/server/sonar-web/src/main/js/apps/overview/app.js
new file mode 100644
index 00000000000..80bc990a965
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/app.js
@@ -0,0 +1,26 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import React from 'react';
+import Main from './main';
+import Empty from './empty';
+
+class App {
+ start(options) {
+ let opts = _.extend({}, options, window.sonarqube.overview);
+ _.extend(opts.component, options.component);
+ $('html').toggleClass('dashboard-page', opts.component.hasSnapshot);
+ window.requestMessages().done(() => {
+ let el = document.querySelector(opts.el);
+ let inner = opts.component.hasSnapshot ? (
+ <Main
+ component={opts.component}
+ gate={opts.gate}
+ measures={opts.measures}
+ leak={opts.leak}/>
+ ) : <Empty/>;
+ React.render(inner, el);
+ });
+ }
+}
+
+window.sonarqube.appStarted.then(options => new App().start(options));
diff --git a/server/sonar-web/src/main/js/apps/overview/card.js b/server/sonar-web/src/main/js/apps/overview/card.js
new file mode 100644
index 00000000000..a22146246d3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/card.js
@@ -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.js b/server/sonar-web/src/main/js/apps/overview/cards.js
new file mode 100644
index 00000000000..3d69cf8bf3a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/cards.js
@@ -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/empty.js b/server/sonar-web/src/main/js/apps/overview/empty.js
new file mode 100644
index 00000000000..78c5320a1f8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/empty.js
@@ -0,0 +1,13 @@
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ return (
+ <div className="panel">
+ <div className="alert alert-warning">
+ {window.t('provisioning.no_analysis')}
+ </div>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/gate-condition.js
new file mode 100644
index 00000000000..2cc428253e6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/gate-condition.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import Measure from './helpers/measure';
+import { periodLabel, getPeriodDate } from './helpers/period-label';
+import DrilldownLink from './helpers/drilldown-link';
+
+export default React.createClass({
+ render() {
+ let 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}<br/><span className="nowrap">{period}</span></h4>
+ <div className="overview-gate-condition-value">
+ <i className={iconClassName}/>&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.js b/server/sonar-web/src/main/js/apps/overview/gate-conditions.js
new file mode 100644
index 00000000000..65b92e42a71
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/gate-conditions.js
@@ -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() {
+ let 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-empty.js b/server/sonar-web/src/main/js/apps/overview/gate-empty.js
new file mode 100644
index 00000000000..56cdb552a05
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/gate-empty.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ let qualityGatesUrl = window.baseUrl + '/quality_gates';
+
+ return (
+ <div className="overview-gate">
+ <h2 className="overview-title">{window.t('overview.quality_gate')}</h2>
+ <p className="big-spacer-top">You should <a href={qualityGatesUrl}>define</a> a quality gate on this project.</p>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate.js b/server/sonar-web/src/main/js/apps/overview/gate.js
new file mode 100644
index 00000000000..2436d2d468e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/gate.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import GateConditions from './gate-conditions';
+import GateEmpty from './gate-empty';
+
+export default React.createClass({
+ render() {
+ if (!this.props.gate || !this.props.gate.level) {
+ return this.props.component.qualifier === 'TRK' ? <GateEmpty/> : null;
+ }
+
+ let
+ badgeClassName = 'badge badge-' + this.props.gate.level.toLowerCase(),
+ badgeText = window.t('overview.gate', this.props.gate.level);
+
+ return (
+ <div className="overview-gate">
+ <h2 className="overview-title">
+ {window.t('overview.quality_gate')}
+ <span className={badgeClassName}>{badgeText}</span>
+ </h2>
+ <GateConditions gate={this.props.gate} component={this.props.component}/>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/donut.js b/server/sonar-web/src/main/js/apps/overview/helpers/donut.js
new file mode 100644
index 00000000000..368a5222c64
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/helpers/donut.js
@@ -0,0 +1,34 @@
+import d3 from 'd3';
+import React from 'react';
+
+let Sector = React.createClass({
+ render() {
+ let 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 }}/>;
+ }
+});
+
+export default React.createClass({
+ getDefaultProps() {
+ return {
+ size: 30,
+ thickness: 6
+ };
+ },
+
+ render() {
+ let radius = this.props.size / 2;
+ let pie = d3.layout.pie()
+ .sort(null)
+ .value(d => d.value);
+ let data = this.props.data;
+ let 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.js b/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js
new file mode 100644
index 00000000000..338a34bc016
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js
@@ -0,0 +1,95 @@
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+import IssuesLink from './issues-link';
+
+export default React.createClass({
+ 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>;
+ },
+
+ 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;
+ }
+
+ let query = Object.keys(params).map(key => {
+ return `${key}=${encodeURIComponent(params[key])}`;
+ }).join('&'),
+ url = `${baseUrl}/drilldown/measures?${query}`;
+
+ return <a href={url}>{this.props.children}</a>;
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js
new file mode 100644
index 00000000000..79878ac9fd6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js
@@ -0,0 +1,8 @@
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ let 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.js b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js
new file mode 100644
index 00000000000..d3118579036
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ let params = Object.keys(this.props.params).map((key) => {
+ return `${key}=${encodeURIComponent(this.props.params[key])}`;
+ }).join('|'),
+ 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.js b/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js
new file mode 100644
index 00000000000..0e2e1fa75c4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ if (this.props.value == null || isNaN(this.props.value)) {
+ return null;
+ }
+ let 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.js b/server/sonar-web/src/main/js/apps/overview/helpers/measure.js
new file mode 100644
index 00000000000..dafc98a82c4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/helpers/measure.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ if (this.props.value == null || isNaN(this.props.value)) {
+ return null;
+ }
+ let 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.js b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js
new file mode 100644
index 00000000000..109a9df9a57
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js
@@ -0,0 +1,18 @@
+import _ from 'underscore';
+import moment from 'moment';
+
+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.js b/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js
new file mode 100644
index 00000000000..22065abad56
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js
@@ -0,0 +1,8 @@
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ let 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.js b/server/sonar-web/src/main/js/apps/overview/helpers/rating.js
new file mode 100644
index 00000000000..e160019e9c6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/helpers/rating.js
@@ -0,0 +1,12 @@
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ if (this.props.value == null || isNaN(this.props.value)) {
+ return null;
+ }
+ let formatted = window.formatMeasure(this.props.value, 'RATING');
+ let className = 'rating rating-' + formatted;
+ return <span className={className}>{formatted}</span>;
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/leak-coverage.js b/server/sonar-web/src/main/js/apps/overview/leak-coverage.js
new file mode 100644
index 00000000000..0d502439796
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/leak-coverage.js
@@ -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() {
+ let
+ 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.js b/server/sonar-web/src/main/js/apps/overview/leak-dups.js
new file mode 100644
index 00000000000..a34555fd892
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/leak-dups.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import Card from './card';
+import MeasureVariation from './helpers/measure-variation';
+import Donut from './helpers/donut';
+
+export default React.createClass({
+ render() {
+ let
+ 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.js b/server/sonar-web/src/main/js/apps/overview/leak-issues.js
new file mode 100644
index 00000000000..61443d6f1ce
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/leak-issues.js
@@ -0,0 +1,69 @@
+import moment from 'moment';
+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 SeverityIcon from '../../components/shared/severity-icon';
+import StatusIcon from '../../components/shared/status-icon';
+import {getPeriodDate} from './helpers/period-label';
+
+export default React.createClass({
+ render() {
+ let
+ 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.js b/server/sonar-web/src/main/js/apps/overview/leak-size.js
new file mode 100644
index 00000000000..d7df01caf33
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/leak-size.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import Card from './card';
+import MeasureVariation from './helpers/measure-variation';
+
+export default React.createClass({
+ render() {
+ let
+ 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.js b/server/sonar-web/src/main/js/apps/overview/leak.js
new file mode 100644
index 00000000000..f629153f2ff
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/leak.js
@@ -0,0 +1,33 @@
+import _ from 'underscore';
+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() {
+ if (_.size(this.props.component.periods) < 3) {
+ return null;
+ }
+
+ let period = periodLabel(this.props.component.periods, '3');
+
+ return (
+ <div className="overview-leak">
+ <h2 className="overview-title">
+ {window.t('overview.water_leak')}
+ <span className="overview-leak-period">{period}</span>
+ </h2>
+ <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.js b/server/sonar-web/src/main/js/apps/overview/main.js
new file mode 100644
index 00000000000..dc1025a8911
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/main.js
@@ -0,0 +1,102 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import moment from 'moment';
+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';
+
+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) {
+ let url = `${baseUrl}/api/issues/search`;
+ data.ps = 1;
+ data.componentUuids = this.props.component.id;
+ return $.get(url, data);
+ },
+
+ requestLeakIssues() {
+ let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ');
+ this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities,statuses' }).done(r => {
+ let
+ 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 => {
+ let
+ 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() {
+ let 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.js b/server/sonar-web/src/main/js/apps/overview/meta.js
new file mode 100644
index 00000000000..310aadd8ae6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/meta.js
@@ -0,0 +1,68 @@
+import _ from 'underscore';
+import React from 'react';
+import ProfileLink from './helpers/profile-link';
+import GateLink from './helpers/gate-link';
+
+export default React.createClass({
+ render() {
+ let
+ profiles = (this.props.component.profiles || []).map(profile => {
+ return (
+ <li key={profile.key}>
+ <span className="note spacer-right">({profile.language})</span>
+ <ProfileLink profile={profile.key}>{profile.name}</ProfileLink>
+ </li>
+ );
+ }),
+ links = (this.props.component.links || []).map(link => {
+ let iconClassName = `spacer-right icon-color-link icon-${link.type}`;
+ return (
+ <li key={link.type}>
+ <i className={iconClassName}/>
+ <a href={link.href} target="_blank">{link.name}</a>
+ </li>
+ );
+ });
+
+ let 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 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.js b/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js
new file mode 100644
index 00000000000..e478a715259
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js
@@ -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() {
+ let
+ 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.js b/server/sonar-web/src/main/js/apps/overview/nutshell-dups.js
new file mode 100644
index 00000000000..fd93f144b0a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/nutshell-dups.js
@@ -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() {
+ let
+ 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.js b/server/sonar-web/src/main/js/apps/overview/nutshell-issues.js
new file mode 100644
index 00000000000..f9ae3340dbf
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/nutshell-issues.js
@@ -0,0 +1,68 @@
+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 StatusIcon from '../../components/shared/status-icon';
+
+export default React.createClass({
+ render() {
+ let
+ 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.js b/server/sonar-web/src/main/js/apps/overview/nutshell-size.js
new file mode 100644
index 00000000000..5d41e291c80
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/nutshell-size.js
@@ -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() {
+ let
+ 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.js b/server/sonar-web/src/main/js/apps/overview/nutshell.js
new file mode 100644
index 00000000000..f8c7dc39f9e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/nutshell.js
@@ -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() {
+ let props = { measures: this.props.measures, component: this.props.component };
+ return (
+ <div className="overview-nutshell">
+ <h2 className="overview-title">{window.t('overview.project_in_a_nutshell')}</h2>
+ <Cards>
+ <NutshellIssues {...props}/>
+ <NutshellCoverage {...props}/>
+ <NutshellDups {...props}/>
+ <NutshellSize {...props}/>
+ </Cards>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/helpers/Url.js b/server/sonar-web/src/main/js/helpers/Url.js
index 80e24f0fc28..6a57b3537f7 100644
--- a/server/sonar-web/src/main/js/helpers/Url.js
+++ b/server/sonar-web/src/main/js/helpers/Url.js
@@ -2,5 +2,5 @@ export function getProjectUrl(project) {
if (typeof project !== 'string') {
throw new TypeError('Project ID or KEY should be passed');
}
- return `${window.baseUrl}/dashboard?id=${encodeURIComponent(project)}`;
+ return `${window.baseUrl}/overview?id=${encodeURIComponent(project)}`;
}
diff --git a/server/sonar-web/src/main/js/libs/application.js b/server/sonar-web/src/main/js/libs/application.js
index 255af4f496c..5338ae64886 100644
--- a/server/sonar-web/src/main/js/libs/application.js
+++ b/server/sonar-web/src/main/js/libs/application.js
@@ -362,7 +362,7 @@ function closeModalWindow () {
*/
function shouldDisplayAbout (days, hours, minutes) {
var hasDays = days > 0,
- fewDays = days < 1000,
+ fewDays = days < 5,
hasHours = hours > 0,
hasMinutes = minutes > 0;
return (hasDays && fewDays && hasHours) || (!hasDays && hasHours && hasMinutes);
diff --git a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
index b7db14235b9..be59409f008 100644
--- a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
+++ b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
@@ -17,7 +17,7 @@ export default React.createClass({
return params.period ? `&period=${params.period}` : '';
},
- renderOverviewLink() {
+ renderMainDashboardLink() {
if (_.size(this.props.component.dashboards) === 0) {
return null;
}
@@ -34,6 +34,11 @@ export default React.createClass({
});
},
+ renderOverviewLink() {
+ let url = `/overview?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('overview.page'), '/overview');
+ },
+
renderComponentsLink() {
const url = `/components/index?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, window.t('components.page'), '/components');
@@ -239,6 +244,7 @@ export default React.createClass({
render() {
return (
<ul className="nav navbar-nav nav-tabs">
+ {this.renderMainDashboardLink()}
{this.renderOverviewLink()}
{this.renderComponentsLink()}
{this.renderComponentIssuesLink()}
diff --git a/server/sonar-web/src/main/less/components/measures.less b/server/sonar-web/src/main/less/components/measures.less
index a112b884fc4..17292f6eabc 100644
--- a/server/sonar-web/src/main/less/components/measures.less
+++ b/server/sonar-web/src/main/less/components/measures.less
@@ -58,7 +58,7 @@
.measure-value {
color: @darkBlue;
font-size: @bigFontSize;
- font-weight: 300;
+ font-weight: 400;
}
.measure-bar {
@@ -95,7 +95,7 @@
.measure-name {
margin-top: 2px;
font-size: 15px;
- font-weight: 300;
+ font-weight: 400;
}
.measure-value {
diff --git a/server/sonar-web/src/main/less/components/navbar.less b/server/sonar-web/src/main/less/components/navbar.less
index 141c2b08f31..f6e613f933e 100644
--- a/server/sonar-web/src/main/less/components/navbar.less
+++ b/server/sonar-web/src/main/less/components/navbar.less
@@ -189,6 +189,7 @@
top: @navbarGlobalHeight;
z-index: @navbar-context-z-index;
height: @navbarContextHeight;
+ padding-top: 5px;
background-color: @navbarContextBackground;
.nav-tabs {
diff --git a/server/sonar-web/src/main/less/components/ui.less b/server/sonar-web/src/main/less/components/ui.less
index 6e8ebe364ef..1af0fed2673 100644
--- a/server/sonar-web/src/main/less/components/ui.less
+++ b/server/sonar-web/src/main/less/components/ui.less
@@ -168,7 +168,7 @@
> li:first-child {
font-size: 18px;
- font-weight: 300;
+ font-weight: 400;
> a {
color: @baseFontColor;
diff --git a/server/sonar-web/src/main/less/pages.less b/server/sonar-web/src/main/less/pages.less
index 4eee2d92015..0dce505a7a8 100644
--- a/server/sonar-web/src/main/less/pages.less
+++ b/server/sonar-web/src/main/less/pages.less
@@ -9,3 +9,4 @@
@import "pages/maintenance";
@import "pages/login";
@import "pages/api-documentation";
+@import "pages/overview";
diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less
new file mode 100644
index 00000000000..da3055a4485
--- /dev/null
+++ b/server/sonar-web/src/main/less/pages/overview.less
@@ -0,0 +1,195 @@
+@import (reference) "../variables";
+@import (reference) "../mixins";
+@import (reference) "../init/type";
+
+.overview {
+ display: flex;
+ width: 100%;
+ min-height: ~"calc(100vh - @{navbarGlobalHeight} - @{navbarContextHeight} - @{pageFooterHeight})";
+}
+
+.overview-main {
+ flex: 1;
+ box-sizing: border-box;
+ background-color: #fff;
+}
+
+.overview-gate {
+ .clearfix;
+ padding: 50px 30px;
+}
+
+.overview-gate-box {
+ float: left;
+ .size(120px, 70px);
+ padding: 10px;
+ .box-sizing(border-box);
+ line-height: 24px;
+ color: #fff;
+ font-size: 16px;
+ font-weight: 300;
+}
+
+.overview-gate-box-error { background-color: @red; }
+
+.overview-gate-box-warn { background-color: @orange; }
+
+.overview-gate-box-ok { background-color: @green; }
+
+.overview-gate-conditions {
+ line-height: 70px;
+ font-size: 0;
+ white-space: nowrap;
+ overflow: hidden;
+
+ & > li {
+ display: inline-block;
+ vertical-align: middle;
+ padding: 0 20px;
+ .box-sizing(border-box);
+ font-size: @baseFontSize;
+ line-height: 1;
+ }
+}
+
+.overview-gate-condition-metric {
+ //color: mix(@baseFontColor, @barBackgroundColor, 70%);
+ font-size: 15px;
+ font-weight: 400;
+ //letter-spacing: 0.03em;
+}
+
+.overview-gate-condition-value {
+ margin-top: 8px;
+ font-weight: 300;
+ font-size: 22px;
+
+ i {
+ position: relative;
+ top: -1px;
+ }
+}
+
+.overview-gate-condition-itself {
+ padding-left: 4px;
+ color: mix(@baseFontColor, @barBackgroundColor, 70%);
+ font-size: 13px;
+ font-weight: 400;
+}
+
+.overview-gate-condition-level {
+ margin-top: 8px;
+}
+
+.overview-leak {
+ padding: 50px 30px;
+ border-top: 1px solid @barBorderColor;
+ border-bottom: 1px solid @barBorderColor;
+}
+
+.overview-title {
+ font-size: 18px;
+ font-weight: 400;
+
+ & > .badge {
+ position: relative;
+ top: -2px;
+ margin-left: 15px;
+ padding: 8px 15px;
+ font-size: 16px;
+ letter-spacing: 0.04em;
+ }
+}
+
+.overview-title + .overview-cards:not(:empty) {
+ margin-top: 20px;
+}
+
+.overview-leak-period {
+ margin-left: 10px;
+ font-size: 14px;
+}
+
+.overview-nutshell {
+ padding: 50px 30px;
+}
+
+.overview-cards {
+ display: flex;
+}
+
+.overview-card {
+ flex: 1 0 25%;
+ box-sizing: border-box;
+
+ .overview-gate & {
+ flex-grow: 0;
+ }
+
+ .overview-main & {
+ font-size: 14px;
+ }
+
+ .measures-chart {
+ width: auto;
+ text-align: left;
+ }
+
+ .measures-chart-indent {
+ padding-left: 67px;
+ }
+
+ .measure-big + .measure-big {
+ margin-left: 30px;
+ }
+
+ .list-inline {
+ margin-left: -10px;
+ margin-right: -10px;
+
+ & > li {
+ padding-left: 10px;
+ padding-right: 10px;
+ }
+ }
+}
+
+.overview-measure {
+ font-size: 28px;
+}
+
+.overview-measure-label {
+ font-size: 16px;
+}
+
+.overview-meta {
+ width: 240px;
+ padding: 50px 30px;
+ border-left: 1px solid @barBorderColor;
+ box-sizing: border-box;
+ background-color: @barBackgroundColor;
+
+ .panel {
+ border: none !important;
+ }
+}
+
+.overview-meta .overview-card {
+ width: auto;
+ margin-bottom: 30px;
+}
+
+.overview-meta-description {
+ line-height: 1.5;
+}
+
+.overview-meta-header {
+ color: #797979;
+}
+
+.overview-meta-list {
+ & > li {
+ padding-bottom: 4px;
+ .text-ellipsis;
+ }
+}
diff --git a/server/sonar-web/src/main/less/variables.less b/server/sonar-web/src/main/less/variables.less
index a92cdd372f2..b21f2353e3c 100644
--- a/server/sonar-web/src/main/less/variables.less
+++ b/server/sonar-web/src/main/less/variables.less
@@ -155,7 +155,7 @@
*/
@navbarGlobalHeight: 30px;
-@navbarContextHeight: 60px;
+@navbarContextHeight: 65px;
@pageFooterHeight: 60px;
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb
new file mode 100644
index 00000000000..746bc651be8
--- /dev/null
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb
@@ -0,0 +1,29 @@
+#
+# SonarQube, open source software quality management tool.
+# Copyright (C) 2008-2014 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# SonarQube is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# SonarQube is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+class OverviewController < ApplicationController
+ before_filter :init_resource_for_user_role
+
+ SECTION=Navigation::SECTION_RESOURCE
+
+ def index
+
+ end
+
+end
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb
new file mode 100644
index 00000000000..41fe8714401
--- /dev/null
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb
@@ -0,0 +1,181 @@
+<%
+ links_size = @resource.project_links.size
+
+ profiles = []
+ if @snapshot
+ qprofiles_measure = @snapshot.measure(Metric::QUALITY_PROFILES)
+ if qprofiles_measure && !qprofiles_measure.data.blank?
+ profiles = JSON.parse qprofiles_measure.data
+ end
+ end
+ profiles_size = profiles.size
+
+ is_gate_default = false
+ gate = nil
+ gate_id = Property.value('sonar.qualitygate', @resource && @resource.id, nil)
+ unless gate_id
+ gate_id=Property.value('sonar.qualitygate', nil, nil)
+ is_gate_default = false || gate_id
+ end
+ if gate_id
+ gate = Internal.quality_gates.get(gate_id.to_i)
+ end
+%>
+
+<%
+ if @snapshot
+ m = @snapshot.measure(Metric::QUALITY_GATE_DETAILS)
+ if m && !m.data.blank?
+ details = JSON.parse m.data
+ m.alert_status = details['level']
+ raw_conditions = details['conditions']
+ conditions = []
+ missing_metric = false
+ raw_conditions.each do |condition|
+ if metric(condition['metric']).nil?
+ missing_metric = true
+ else
+ conditions << condition
+ end
+ end
+ alert_metric = metric(Metric::ALERT_STATUS)
+ end
+ end
+%>
+
+<% content_for :extra_script do %>
+ <script>
+ (function () {
+ var component = {
+ id: '<%= escape_javascript @resource.uuid %>',
+ key: '<%= escape_javascript @resource.key %>',
+ description: '<%= escape_javascript @resource.description %>',
+ hasSnapshot: <%= @snapshot ? true : false %>,
+ periods: [
+ <%
+ if @snapshot && @snapshot.project_snapshot.periods?
+ (1..5).each do |index|
+ if @snapshot.period_mode(index)
+ %>
+ {
+ index: '<%= index -%>',
+ mode: '<%= @snapshot.period_mode(index) -%>',
+ modeParam: '<%= @snapshot.period_param(index) -%>',
+ date: '<%= @snapshot.period_datetime(index).to_date.strftime('%FT%T%z') -%>'
+ },
+ <% end %>
+ <% end %>
+ <% end %>
+ ],
+ links: [
+ <% @resource.project_links.sort.each_with_index do |link, index| %>
+ {
+ name: '<%= escape_javascript link.name -%>',
+ type: '<%= escape_javascript link.link_type -%>',
+ href: '<%= escape_javascript link.href -%>'
+ }<% if index < links_size - 1 %>, <% end -%>
+ <% end %>
+ ],
+ profiles: [
+ <% profiles.each_with_index do |profile, index| %>
+ {
+ name: '<%= escape_javascript profile['name'] -%>',
+ key: '<%= escape_javascript profile['key']-%>',
+ language: '<%= escape_javascript Api::Utils.language_name(profile['language']) -%>'
+ }<% if index < profiles_size - 1 %>, <% end -%>
+ <% end %>
+ ],
+ <% if gate %>
+ gate: {
+ name: '<%= escape_javascript gate.getName() -%>',
+ key: <%= escape_javascript gate_id -%>,
+ isDefault: <%= is_gate_default -%>
+ }
+ <% end %>
+ };
+
+ <% if m %>
+ var gate = {
+ level: '<%= m.alert_status -%>',
+ conditions: [
+ <% conditions.sort_by {|condition| [ -condition['level'].length, metric(condition['metric']).short_name] }.each do |condition| %>
+ <% metric = metric(condition['metric']) %>
+ {
+ level: '<%= escape_javascript condition['level'] %>',
+ metric: {
+ name: '<%= escape_javascript metric.name %>',
+ type: '<%= escape_javascript metric.value_type %>'
+ },
+ op: '<%= escape_javascript condition['op'] %>',
+ period: '<%= condition['period'] %>',
+ warning: '<%= escape_javascript condition['warning'] %>',
+ error: '<%= escape_javascript condition['error'] %>',
+ actual: '<%= escape_javascript condition['actual'] %>',
+ },
+ <% end %>
+ ]
+ };
+ <% else %>
+ var gate = null;
+ <% end %>
+
+ var measures = {
+ <% if @snapshot %>
+
+ // issues
+ <% if @snapshot.measure('sqale_rating') %>
+ sqaleRating: '<%= @snapshot.measure('sqale_rating').value -%>',
+ <% else %>
+ sqaleRating: 'A',
+ <% end %>
+
+ // coverage
+ <% if @snapshot.measure('overall_coverage') %>
+ coverage: '<%= @snapshot.measure('overall_coverage').value -%>',
+ <% end %>
+ <% if @snapshot.measure('tests') %>
+ tests: '<%= @snapshot.measure('tests').value -%>',
+ <% end %>
+
+ // duplications
+ duplications: '<%= @snapshot.measure('duplicated_lines_density').value -%>',
+ duplicatedLines: '<%= @snapshot.measure('duplicated_lines').value -%>',
+ duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').value -%>',
+
+ // size
+ lines: '<%= @snapshot.measure('lines').value -%>',
+ files: '<%= @snapshot.measure('files').value -%>'
+ <% end %>
+ };
+
+ var leak = {
+ <% if @snapshot %>
+ // coverage
+ <% if @snapshot.measure('new_overall_coverage') %>
+ newCoverage: '<%= @snapshot.measure('new_overall_coverage').variation(3) -%>',
+ <% end %>
+ <% if @snapshot.measure('tests') %>
+ tests: '<%= @snapshot.measure('tests').variation(3) -%>',
+ <% end %>
+
+ // duplications
+ duplications: '<%= @snapshot.measure('duplicated_lines_density').variation(3) -%>',
+ duplicatedLines: '<%= @snapshot.measure('duplicated_lines').variation(3) -%>',
+ duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').variation(3) -%>',
+
+ // size
+ lines: '<%= @snapshot.measure('lines').variation(3) -%>',
+ files: '<%= @snapshot.measure('files').variation(3) -%>'
+ <% end %>
+ };
+
+ window.sonarqube.overview = {
+ component: component,
+ gate: gate,
+ measures: measures,
+ leak: leak
+ };
+ })();
+ </script>
+ <script src="<%= ApplicationController.root_context -%>/js/bundles/overview.js?v=<%= sonar_version -%>"></script>
+<% end %>
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index ebec9a8157b..7760e5320be 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -3092,3 +3092,35 @@ background_tasks.in_progress_duration=Duration of the current task in progress.
#
#------------------------------------------------------------------------------
system.log_level.warning=Current level has performance impacts, please make sure to get back to INFO level once your investigation is done. Please note that when you restart the server, the level will automatically be reset to INFO.
+
+
+
+#------------------------------------------------------------------------------
+#
+# OVERVIEW
+#
+#------------------------------------------------------------------------------
+overview.quality_gate=Quality Gate
+overview.quality_profiles=Quality Profiles
+overview.water_leak=Water Leak
+overview.project_in_a_nutshell=Project In a Nutshell
+
+overview.metric.new_coverage=New Coverage
+overview.metric.tests=tests
+overview.metric.duplications=Duplications
+overview.metric.duplicated_lines=lines
+overview.metric.debt=Debt
+overview.metric.issues=Issues
+overview.metric.new_debt=New Debt
+overview.metric.new_issues=New Issues
+overview.metric.lines=Lines
+overview.metric.files=Files
+overview.metric.coverage=Coverage
+
+overview.period.previous_version=since {0}
+overview.period.previous_analysis=since previous analysis
+overview.period.days=last {0} days
+
+overview.gate.ERROR=Failed
+overview.gate.WARN=Warning
+overview.gate.OK=Passed