aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
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 /server/sonar-web
parent26817f2f1b31d777b681cc08f3a06db201521007 (diff)
downloadsonarqube-187f501d31cba9ff220c44bbcd222768a4eee40e.tar.gz
sonarqube-187f501d31cba9ff220c44bbcd222768a4eee40e.zip
SONAR-6331 add project overview page
Diffstat (limited to 'server/sonar-web')
-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
41 files changed, 1386 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 %>