aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-10-28 10:25:13 +0100
committerStas Vilchik <vilchiks@gmail.com>2015-10-30 10:46:02 +0100
commit1e5830fb652fda2da6f18460c92f82d93c52130c (patch)
tree8383c08cc17ed71555b1cd8468292968f9bf07f1 /server/sonar-web/src
parenta3528799883487e180ce90985e96cf87281e645f (diff)
downloadsonarqube-1e5830fb652fda2da6f18460c92f82d93c52130c.tar.gz
sonarqube-1e5830fb652fda2da6f18460c92f82d93c52130c.zip
SONAR-6331 improve UX of the project overview page
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/api/issues.js16
-rw-r--r--server/sonar-web/src/main/js/api/measures.js20
-rw-r--r--server/sonar-web/src/main/js/apps/overview/app.js15
-rw-r--r--server/sonar-web/src/main/js/apps/overview/coverage/main.js9
-rw-r--r--server/sonar-web/src/main/js/apps/overview/duplications/main.js10
-rw-r--r--server/sonar-web/src/main/js/apps/overview/formatting.js78
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js41
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js16
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js (renamed from server/sonar-web/src/main/js/apps/overview/general/gate-empty.js)2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/gate/gate.js (renamed from server/sonar-web/src/main/js/apps/overview/general/gate.js)6
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/card.js8
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/cards.js7
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/components.js126
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/coverage.js64
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/details-link.js18
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/details.js34
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/duplications.js56
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/empty.js13
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/gate-condition.js37
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js21
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/issues.js129
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js46
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/leak-dups.js42
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/leak-issues.js69
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/leak-size.js30
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/leak.js37
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/main.js224
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js47
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js47
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js68
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js35
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/nutshell.js26
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/size.js60
-rw-r--r--server/sonar-web/src/main/js/apps/overview/general/timeline.js47
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/metrics.js5
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/period-label.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/issues/assignees.js3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/issues/main.js12
-rw-r--r--server/sonar-web/src/main/js/apps/overview/issues/severities.js3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main.js95
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta.js10
-rw-r--r--server/sonar-web/src/main/js/apps/overview/overview.js54
-rw-r--r--server/sonar-web/src/main/js/apps/overview/size/main.js10
-rw-r--r--server/sonar-web/src/main/js/components/charts/bar-chart.js2
-rw-r--r--server/sonar-web/src/main/js/components/charts/bubble-chart.js2
-rw-r--r--server/sonar-web/src/main/js/components/charts/line-chart.js13
-rw-r--r--server/sonar-web/src/main/js/components/charts/treemap.js2
-rw-r--r--server/sonar-web/src/main/js/components/charts/word-cloud.js2
-rw-r--r--server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js (renamed from server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js)0
-rw-r--r--server/sonar-web/src/main/js/helpers/constants.js2
-rw-r--r--server/sonar-web/src/main/js/libs/application.js43
-rw-r--r--server/sonar-web/src/main/less/pages/overview.less406
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb54
54 files changed, 1029 insertions, 1197 deletions
diff --git a/server/sonar-web/src/main/js/api/issues.js b/server/sonar-web/src/main/js/api/issues.js
index 5d2259e6855..76a54463b7d 100644
--- a/server/sonar-web/src/main/js/api/issues.js
+++ b/server/sonar-web/src/main/js/api/issues.js
@@ -1,7 +1,9 @@
import _ from 'underscore';
+
import { getJSON } from '../helpers/request.js';
-function getFacet (query, facet) {
+
+export function getFacet (query, facet) {
let url = baseUrl + '/api/issues/search';
let data = _.extend({}, query, { facets: facet, ps: 1, additionalFields: '_all' });
return getJSON(url, data).then(r => {
@@ -9,14 +11,17 @@ function getFacet (query, facet) {
});
}
+
export function getSeverities (query) {
return getFacet(query, 'severities').then(r => r.facet);
}
+
export function getTags (query) {
return getFacet(query, 'tags').then(r => r.facet);
}
+
export function getAssignees (query) {
return getFacet(query, 'assignees').then(r => {
return r.facet.map(item => {
@@ -25,3 +30,12 @@ export function getAssignees (query) {
});
});
}
+
+
+export function getIssuesCount (query) {
+ let url = baseUrl + '/api/issues/search';
+ let data = _.extend({}, query, { ps: 1, facetMode: 'debt' });
+ return getJSON(url, data).then(r => {
+ return { issues: r.total, debt: r.debtTotal };
+ });
+}
diff --git a/server/sonar-web/src/main/js/api/measures.js b/server/sonar-web/src/main/js/api/measures.js
index e416e3a8882..0916c4998c1 100644
--- a/server/sonar-web/src/main/js/api/measures.js
+++ b/server/sonar-web/src/main/js/api/measures.js
@@ -1,5 +1,6 @@
import { getJSON } from '../helpers/request.js';
+
export function getMeasures (componentKey, metrics) {
let url = baseUrl + '/api/resources/index';
let data = { resource: componentKey, metrics: metrics.join(',') };
@@ -12,3 +13,22 @@ export function getMeasures (componentKey, metrics) {
return measures;
});
}
+
+
+export function getMeasuresAndVariations (componentKey, metrics) {
+ let url = baseUrl + '/api/resources/index';
+ let data = { resource: componentKey, metrics: metrics.join(','), includetrends: 'true' };
+ return getJSON(url, data).then(r => {
+ let msr = r[0].msr || [];
+ let measures = {};
+ msr.forEach(measure => {
+ measures[measure.key] = {
+ value: measure.val != null ? measure.val : measure.data,
+ var1: measure.var1,
+ var2: measure.var2,
+ var3: measure.var3
+ };
+ });
+ return measures;
+ });
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/app.js b/server/sonar-web/src/main/js/apps/overview/app.js
index f2912244670..418784e1405 100644
--- a/server/sonar-web/src/main/js/apps/overview/app.js
+++ b/server/sonar-web/src/main/js/apps/overview/app.js
@@ -2,15 +2,26 @@ import $ from 'jquery';
import _ from 'underscore';
import React from 'react';
import ReactDOM from 'react-dom';
-import { Overview } from './main';
+
+import { Overview, EmptyOverview } from './overview';
+
+
+const LEAK_PERIOD = '1';
+
class App {
start (options) {
let opts = _.extend({}, options, window.sonarqube.overview);
_.extend(opts.component, options.component);
+
$('html').toggleClass('dashboard-page', opts.component.hasSnapshot);
let el = document.querySelector(opts.el);
- ReactDOM.render(<Overview {...opts}/>, el);
+
+ if (opts.component.hasSnapshot) {
+ ReactDOM.render(<Overview {...opts} leakPeriodIndex={LEAK_PERIOD}/>, el);
+ } else {
+ ReactDOM.render(<EmptyOverview/>, el);
+ }
}
}
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/main.js b/server/sonar-web/src/main/js/apps/overview/coverage/main.js
index 084a7f94ea0..a4e8f90fa37 100644
--- a/server/sonar-web/src/main/js/apps/overview/coverage/main.js
+++ b/server/sonar-web/src/main/js/apps/overview/coverage/main.js
@@ -9,7 +9,14 @@ import { CoverageTreemap } from './treemap';
export default class extends React.Component {
render () {
- return <div className="overview-domain">
+ return <div className="overview-detailed-page">
+ <div className="overview-domain-header">
+ <h2 className="overview-title">Coverage & Tests</h2>
+ </div>
+
+ <a className="overview-detailed-page-back" href="#">
+ <i className="icon-chevron-left"/>
+ </a>
<CoverageTimeline {...this.props}/>
diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/main.js b/server/sonar-web/src/main/js/apps/overview/duplications/main.js
index a029f6ebb32..02aa9eceecc 100644
--- a/server/sonar-web/src/main/js/apps/overview/duplications/main.js
+++ b/server/sonar-web/src/main/js/apps/overview/duplications/main.js
@@ -8,7 +8,15 @@ import { DuplicationsTreemap } from './treemap';
export default class extends React.Component {
render () {
- return <div className="overview-domain">
+ return <div className="overview-detailed-page">
+ <div className="overview-domain-header">
+ <h2 className="overview-title">Duplications</h2>
+ </div>
+
+ <a className="overview-detailed-page-back" href="#">
+ <i className="icon-chevron-left"/>
+ </a>
+
<DuplicationsTimeline {...this.props}/>
<div className="flex-columns">
<div className="flex-column flex-column-half">
diff --git a/server/sonar-web/src/main/js/apps/overview/formatting.js b/server/sonar-web/src/main/js/apps/overview/formatting.js
deleted file mode 100644
index 7f05531226c..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/formatting.js
+++ /dev/null
@@ -1,78 +0,0 @@
-const METRIC_TYPES = {
- 'violations': 'SHORT_INT',
- 'blocker_violations': 'SHORT_INT',
- 'critical_violations': 'SHORT_INT',
- 'major_violations': 'SHORT_INT',
- 'minor_violations': 'SHORT_INT',
- 'info_violations': 'SHORT_INT',
- 'confirmed_issues': 'SHORT_INT',
- 'false_positive_issues': 'SHORT_INT',
- 'open_issues': 'SHORT_INT',
- 'reopened_issues': 'SHORT_INT',
- 'sqale_index': 'SHORT_WORK_DUR',
- 'sqale_debt_ratio': 'PERCENT',
- 'sqale_rating': 'RATING',
-
- 'coverage': 'PERCENT',
- 'line_coverage': 'PERCENT',
- 'branch_coverage': 'PERCENT',
- 'lines_to_cover': 'SHORT_INT',
- 'conditions_to_cover': 'SHORT_INT',
- 'uncovered_lines': 'SHORT_INT',
- 'uncovered_conditions': 'SHORT_INT',
-
- 'it_coverage': 'PERCENT',
- 'it_line_coverage': 'PERCENT',
- 'it_branch_coverage': 'PERCENT',
- 'it_lines_to_cover': 'SHORT_INT',
- 'it_conditions_to_cover': 'SHORT_INT',
- 'it_uncovered_lines': 'SHORT_INT',
- 'it_uncovered_conditions': 'SHORT_INT',
-
- 'overall_coverage': 'PERCENT',
- 'overall_line_coverage': 'PERCENT',
- 'overall_branch_coverage': 'PERCENT',
- 'overall_lines_to_cover': 'SHORT_INT',
- 'overall_conditions_to_cover': 'SHORT_INT',
- 'overall_uncovered_lines': 'SHORT_INT',
- 'overall_uncovered_conditions': 'SHORT_INT',
-
- 'tests': 'SHORT_INT',
- 'skipped_tests': 'SHORT_INT',
- 'test_errors': 'SHORT_INT',
- 'test_failures': 'SHORT_INT',
- 'test_execution_time': 'MILLISEC',
- 'test_success_density': 'PERCENT',
-
- 'duplicated_blocks': 'INT',
- 'duplicated_files': 'INT',
- 'duplicated_lines': 'INT',
- 'duplicated_lines_density': 'PERCENT',
-
- 'ncloc': 'SHORT_INT',
- 'classes': 'SHORT_INT',
- 'lines': 'SHORT_INT',
- 'generated_ncloc': 'SHORT_INT',
- 'generated_lines': 'SHORT_INT',
- 'directories': 'SHORT_INT',
- 'files': 'SHORT_INT',
- 'functions': 'SHORT_INT',
- 'statements': 'SHORT_INT',
- 'public_api': 'SHORT_INT',
-
- 'complexity': 'SHORT_INT',
- 'class_complexity': 'SHORT_INT',
- 'file_complexity': 'SHORT_INT',
- 'function_complexity': 'SHORT_INT',
-
- 'comment_lines_density': 'PERCENT',
- 'comment_lines': 'SHORT_INT',
- 'commented_out_code_lines': 'SHORT_INT',
- 'public_documented_api_density': 'PERCENT',
- 'public_undocumented_api': 'SHORT_INT'
-};
-
-export function formatMeasure (value, metric) {
- let type = METRIC_TYPES[metric];
- return type ? window.formatMeasure(value, type) : value;
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js
new file mode 100644
index 00000000000..5699a79aeeb
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js
@@ -0,0 +1,41 @@
+import React from 'react';
+
+import Measure from './../helpers/measure';
+import { getPeriodLabel, 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,
+ period = this.props.condition.period ?
+ getPeriodLabel(this.props.component.periods, this.props.condition.period) : null,
+ periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period);
+
+ let classes = 'alert_' + this.props.condition.level.toUpperCase();
+
+ return (
+ <li className="overview-gate-condition">
+ <div className="little-spacer-bottom">{period}</div>
+
+ <div style={{ display: 'flex', alignItems: 'center' }}>
+ <div className="overview-gate-condition-value">
+ <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name}
+ period={this.props.condition.period} periodDate={periodDate}>
+ <span className={classes}>
+ <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/>
+ </span>
+ </DrilldownLink>&nbsp;
+ </div>
+
+ <div className="overview-gate-condition-metric">
+ <div>{metricName}</div>
+ <div>{window.t('quality_gates.operator', this.props.condition.op, 'short')} <Measure value={threshold} type={this.props.condition.metric.type}/></div>
+ </div>
+ </div>
+ </li>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js
new file mode 100644
index 00000000000..adefecbded4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import GateCondition from './gate-condition';
+
+export default React.createClass({
+ propTypes: {
+ gate: React.PropTypes.object.isRequired,
+ component: React.PropTypes.object.isRequired
+ },
+
+ render() {
+ let conditions = this.props.gate.conditions
+ .filter(c => c.level !== 'OK')
+ .map(c => <GateCondition key={c.metric.name} condition={c} component={this.props.component}/>);
+ return <ul className="overview-gate-conditions-list">{conditions}</ul>;
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js
index fb74bfa8a12..61347185593 100644
--- a/server/sonar-web/src/main/js/apps/overview/general/gate-empty.js
+++ b/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js
@@ -7,7 +7,7 @@ export default React.createClass({
return (
<div className="overview-gate">
<h2 className="overview-title">{window.t('overview.quality_gate')}</h2>
- <p className="overview-paragraph big-spacer-top">
+ <p className="overview-gate-warning">
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/general/gate.js b/server/sonar-web/src/main/js/apps/overview/gate/gate.js
index 2436d2d468e..076cbcd376a 100644
--- a/server/sonar-web/src/main/js/apps/overview/general/gate.js
+++ b/server/sonar-web/src/main/js/apps/overview/gate/gate.js
@@ -1,15 +1,17 @@
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(),
+ let level = this.props.gate.level.toLowerCase(),
+ badgeClassName = 'badge badge-' + level,
badgeText = window.t('overview.gate', this.props.gate.level);
return (
diff --git a/server/sonar-web/src/main/js/apps/overview/general/card.js b/server/sonar-web/src/main/js/apps/overview/general/card.js
deleted file mode 100644
index 4d5eb06adf6..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/card.js
+++ /dev/null
@@ -1,8 +0,0 @@
-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/general/cards.js b/server/sonar-web/src/main/js/apps/overview/general/cards.js
deleted file mode 100644
index 3d69cf8bf3a..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/cards.js
+++ /dev/null
@@ -1,7 +0,0 @@
-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/general/components.js b/server/sonar-web/src/main/js/apps/overview/general/components.js
new file mode 100644
index 00000000000..441f6d12e01
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/general/components.js
@@ -0,0 +1,126 @@
+import moment from 'moment';
+import React from 'react';
+
+import { Timeline } from './timeline';
+
+
+export const Domain = React.createClass({
+ render () {
+ return <div className="overview-domain">{this.props.children}</div>;
+ }
+});
+
+
+export const DomainTitle = React.createClass({
+ render () {
+ return <div className="overview-title">{this.props.children}</div>;
+ }
+});
+
+
+export const DomainLeakTitle = React.createClass({
+ render() {
+ if (!this.props.label || !this.props.date) {
+ return null;
+ }
+ let momentDate = moment(this.props.date);
+ let fromNow = momentDate.fromNow();
+ let tooltip = 'Started ' + fromNow + ', ' + momentDate.format('LLL');
+ return <span title={tooltip} data-toggle="tooltip">Water Leak: {this.props.label}</span>;
+ }
+});
+
+
+export const DomainHeader = React.createClass({
+ render () {
+ return <div className="overview-domain-header">
+ <DomainTitle>{this.props.title}</DomainTitle>
+ <DomainLeakTitle label={this.props.leakPeriodLabel} date={this.props.leakPeriodDate}/>
+ </div>;
+ }
+});
+
+
+export const DomainPanel = React.createClass({
+ propTypes: {
+ domain: React.PropTypes.string
+ },
+
+ render () {
+ return <div className="overview-domain-panel">
+ {this.props.children}
+ </div>;
+ }
+});
+
+
+export const DomainNutshell = React.createClass({
+ render () {
+ return <div className="overview-domain-nutshell">{this.props.children}</div>;
+ }
+});
+
+export const DomainLeak = React.createClass({
+ render () {
+ return <div className="overview-domain-leak">{this.props.children}</div>;
+ }
+});
+
+
+export const MeasuresList = React.createClass({
+ render () {
+ return <div className="overview-domain-measures">{this.props.children}</div>;
+ }
+});
+
+
+export const Measure = React.createClass({
+ propTypes: {
+ label: React.PropTypes.string,
+ composite: React.PropTypes.bool
+ },
+
+ getDefaultProps() {
+ return { composite: false };
+ },
+
+ renderValue () {
+ if (this.props.composite) {
+ return this.props.children;
+ } else {
+ return <div className="overview-domain-measure-value">
+ {this.props.children}
+ </div>;
+ }
+ },
+
+ renderLabel() {
+ return this.props.label ?
+ <div className="overview-domain-measure-label">{this.props.label}</div> : null;
+ },
+
+ render () {
+ return <div className="overview-domain-measure">
+ {this.renderValue()}
+ {this.renderLabel()}
+ </div>;
+ }
+});
+
+
+export const DomainMixin = {
+ renderTimeline(range) {
+ if (!this.props.history) {
+ return null;
+ }
+ let props = { history: this.props.history };
+ props[range] = this.props.leakPeriodDate;
+ return <div className="overview-domain-timeline">
+ <Timeline {...props}/>
+ </div>;
+ },
+
+ hasLeakPeriod () {
+ return this.props.leakPeriodDate != null;
+ }
+};
diff --git a/server/sonar-web/src/main/js/apps/overview/general/coverage.js b/server/sonar-web/src/main/js/apps/overview/general/coverage.js
new file mode 100644
index 00000000000..bec2f468bf1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/general/coverage.js
@@ -0,0 +1,64 @@
+import React from 'react';
+
+import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
+import DrilldownLink from '../helpers/drilldown-link';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { getMetricName } from '../helpers/metrics';
+
+
+export const GeneralCoverage = React.createClass({
+ mixins: [TooltipsMixin, DomainMixin],
+
+ propTypes: {
+ measures: React.PropTypes.object.isRequired,
+ leakPeriodLabel: React.PropTypes.string,
+ leakPeriodDate: React.PropTypes.object
+ },
+
+ renderLeak () {
+ if (!this.hasLeakPeriod()) {
+ return null;
+ }
+
+ return <DomainLeak>
+ <MeasuresList>
+ <Measure label={getMetricName('new_coverage')}>
+ <DrilldownLink component={this.props.component.key} metric="new_overall_coverage" period="1">
+ {window.formatMeasure(this.props.leak['new_overall_coverage'], 'PERCENT')}
+ </DrilldownLink>
+ </Measure>
+ </MeasuresList>
+ {this.renderTimeline('after')}
+ </DomainLeak>;
+ },
+
+ render () {
+ if (this.props.measures['overall_coverage'] == null) {
+ return null;
+ }
+
+ return <Domain>
+ <DomainHeader title="Tests"
+ leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
+
+ <DomainPanel domain="coverage">
+ <DomainNutshell>
+ <MeasuresList>
+ <Measure label={getMetricName('coverage')}>
+ <DrilldownLink component={this.props.component.key} metric="overall_coverage">
+ {window.formatMeasure(this.props.measures['overall_coverage'], 'PERCENT')}
+ </DrilldownLink>
+ </Measure>
+ <Measure label={getMetricName('tests')}>
+ <DrilldownLink component={this.props.component.key} metric="tests">
+ {window.formatMeasure(this.props.measures['tests'], 'SHORT_INT')}
+ </DrilldownLink>
+ </Measure>
+ </MeasuresList>
+ {this.renderTimeline('before')}
+ </DomainNutshell>
+ {this.renderLeak()}
+ </DomainPanel>
+ </Domain>;
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/details-link.js b/server/sonar-web/src/main/js/apps/overview/general/details-link.js
deleted file mode 100644
index dca106dd3dd..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/details-link.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-import classNames from 'classnames';
-
-export default React.createClass({
- handleClick(e) {
- e.preventDefault();
- this.props.onRoute(this.props.linkTo);
- },
-
- render() {
- let classes = classNames('overview-card', 'overview-card-section', {
- 'active': this.props.active
- });
- return <li className={classes}>
- <a onClick={this.handleClick}>{window.t('overview.domain', this.props.linkTo)}</a>
- </li>;
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/details.js b/server/sonar-web/src/main/js/apps/overview/general/details.js
deleted file mode 100644
index f5ee24bc4bb..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/details.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import Cards from './cards';
-import DetailsLink from './details-link';
-
-
-function checkMeasureForDomain (domain, measures) {
- if (domain === 'coverage' && measures.coverage == null) {
- return false;
- }
- if (domain === 'duplications' && measures.duplications == null) {
- return false;
- }
- return true;
-}
-
-
-export default React.createClass({
- render() {
- let domains = ['issues', 'coverage', 'duplications', 'size'].map(domain => {
- if (!checkMeasureForDomain(domain, this.props.measures)) {
- return null;
- }
- let active = domain === this.props.section;
- return <DetailsLink key={domain} linkTo={domain} onRoute={this.props.onRoute} active={active}/>;
- });
-
- return (
- <div className="overview-more">
- <h2 className="overview-title">More Details</h2>
- <Cards>{domains}</Cards>
- </div>
- );
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/duplications.js b/server/sonar-web/src/main/js/apps/overview/general/duplications.js
new file mode 100644
index 00000000000..d316eea8ba6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/general/duplications.js
@@ -0,0 +1,56 @@
+import React from 'react';
+
+import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
+import DrilldownLink from '../helpers/drilldown-link';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { getMetricName } from '../helpers/metrics';
+
+
+export const GeneralDuplications = React.createClass({
+ mixins: [TooltipsMixin, DomainMixin],
+
+ propTypes: {
+ leakPeriodLabel: React.PropTypes.string,
+ leakPeriodDate: React.PropTypes.object
+ },
+
+ renderLeak () {
+ if (!this.hasLeakPeriod()) {
+ return null;
+ }
+ return <DomainLeak>
+ <MeasuresList>
+ <Measure label={getMetricName('duplications')}>
+ {window.formatMeasureVariation(this.props.leak['duplicated_lines_density'], 'PERCENT')}
+ </Measure>
+ </MeasuresList>
+ {this.renderTimeline('after')}
+ </DomainLeak>;
+ },
+
+ render () {
+ return <Domain>
+ <DomainHeader title="Duplications"
+ leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
+
+ <DomainPanel domain="duplications">
+ <DomainNutshell>
+ <MeasuresList>
+ <Measure label={getMetricName('duplications')}>
+ <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density">
+ {window.formatMeasure(this.props.measures['duplicated_lines_density'], 'PERCENT')}
+ </DrilldownLink>
+ </Measure>
+ <Measure label={getMetricName('duplicated_blocks')}>
+ <DrilldownLink component={this.props.component.key} metric="duplicated_blocks">
+ {window.formatMeasure(this.props.measures['duplicated_blocks'], 'SHORT_INT')}
+ </DrilldownLink>
+ </Measure>
+ </MeasuresList>
+ {this.renderTimeline('before')}
+ </DomainNutshell>
+ {this.renderLeak()}
+ </DomainPanel>
+ </Domain>;
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/empty.js b/server/sonar-web/src/main/js/apps/overview/general/empty.js
deleted file mode 100644
index 78c5320a1f8..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/empty.js
+++ /dev/null
@@ -1,13 +0,0 @@
-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/general/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/general/gate-condition.js
deleted file mode 100644
index 32727972d03..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/gate-condition.js
+++ /dev/null
@@ -1,37 +0,0 @@
-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,
- 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);
-
- let classes = 'alert_' + this.props.condition.level.toUpperCase();
-
- return (
- <div>
- <h4 className="overview-gate-condition-metric">{metricName}<br/><span className="nowrap">{period}</span></h4>
- <div className="overview-gate-condition-value">
- <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name}
- period={this.props.condition.period} periodDate={periodDate}>
- <span className={classes}>
- <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/>
- </span>
- </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/general/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js
deleted file mode 100644
index 65b92e42a71..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js
+++ /dev/null
@@ -1,21 +0,0 @@
-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/general/issues.js b/server/sonar-web/src/main/js/apps/overview/general/issues.js
new file mode 100644
index 00000000000..b97b0db57f9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/general/issues.js
@@ -0,0 +1,129 @@
+import moment from 'moment';
+import React from 'react';
+
+import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
+import Rating from './../helpers/rating';
+import IssuesLink from '../helpers/issues-link';
+import DrilldownLink from '../helpers/drilldown-link';
+import SeverityHelper from '../../../components/shared/severity-helper';
+import SeverityIcon from '../../../components/shared/severity-icon';
+import StatusIcon from '../../../components/shared/status-icon';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { getMetricName } from '../helpers/metrics';
+import { SEVERITIES } from '../../../helpers/constants';
+
+
+export const GeneralIssues = React.createClass({
+ mixins: [TooltipsMixin, DomainMixin],
+
+ propTypes: {
+ leakPeriodLabel: React.PropTypes.string,
+ leakPeriodDate: React.PropTypes.object
+ },
+
+ renderSeverities() {
+ let severities = SEVERITIES.map((s, index) => {
+ let measure = this.props.measures.issuesSeverities[index];
+ return <tr key={s}>
+ <td>
+ <SeverityHelper severity={s}/>
+ </td>
+ <td className="thin nowrap text-right">
+ <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: s }}>
+ {window.formatMeasure(measure, 'SHORT_INT')}
+ </IssuesLink>
+ </td>
+ </tr>;
+ });
+
+ return <div style={{ width: 120 }}>
+ <table className="data">
+ <tbody>{severities}</tbody>
+ </table>
+ </div>;
+ },
+
+ renderLeak () {
+ if (!this.hasLeakPeriod()) {
+ return null;
+ }
+
+ let createdAfter = moment(this.props.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+ return <DomainLeak>
+ <MeasuresList>
+ <Measure label={getMetricName('new_issues')}>
+ <IssuesLink component={this.props.component.key}
+ params={{ resolved: 'false', createdAfter: createdAfter }}>
+ {window.formatMeasureVariation(this.props.leak.issues, 'SHORT_INT')}
+ </IssuesLink>
+ </Measure>
+ <Measure label={getMetricName('new_debt')}>
+ <IssuesLink component={this.props.component.key}
+ params={{ resolved: 'false', createdAfter: createdAfter, facetMode: 'debt' }}>
+ {window.formatMeasureVariation(this.props.leak.debt, 'SHORT_WORK_DUR')}
+ </IssuesLink>
+ </Measure>
+ </MeasuresList>
+ <MeasuresList>
+ <Measure label={getMetricName('new_blocker_issues')}>
+ <span className="spacer-right"><SeverityIcon severity="BLOCKER"/></span>
+ <IssuesLink component={this.props.component.key}
+ params={{ resolved: 'false', severities: 'BLOCKER', createdAfter: createdAfter }}>
+ {window.formatMeasureVariation(this.props.leak.issuesSeverities[0], 'SHORT_INT')}
+ </IssuesLink>
+ </Measure>
+ <Measure label={getMetricName('new_critical_issues')}>
+ <span className="spacer-right"><SeverityIcon severity="CRITICAL"/></span>
+ <IssuesLink component={this.props.component.key}
+ params={{ resolved: 'false', severities: 'CRITICAL', createdAfter: createdAfter }}>
+ {window.formatMeasureVariation(this.props.leak.issuesSeverities[1], 'SHORT_INT')}
+ </IssuesLink>
+ </Measure>
+ <Measure label={getMetricName('new_open_issues')}>
+ <span className="spacer-right"><StatusIcon status="OPEN"/></span>
+ <IssuesLink component={this.props.component.key}
+ params={{ resolved: 'false', statuses: 'OPEN,REOPENED', createdAfter: createdAfter }}>
+ {window.formatMeasureVariation(this.props.leak.issuesStatuses[0] + this.props.leak.issuesStatuses[1],
+ 'SHORT_INT')}
+ </IssuesLink>
+ </Measure>
+ </MeasuresList>
+ {this.renderTimeline('after')}
+ </DomainLeak>;
+ },
+
+ render () {
+ return <Domain>
+ <DomainHeader title="Technical Debt"
+ leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
+
+ <DomainPanel domain="issues">
+ <DomainNutshell>
+ <MeasuresList>
+ <Measure>
+ <DrilldownLink component={this.props.component.key} metric="sqale_rating">
+ <Rating value={this.props.measures['sqale_rating']}/>
+ </DrilldownLink>
+ </Measure>
+ <Measure label={getMetricName('issues')}>
+ <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}>
+ {window.formatMeasure(this.props.measures.issues, 'SHORT_INT')}
+ </IssuesLink>
+ </Measure>
+ <Measure label={getMetricName('debt')}>
+ <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}>
+ {window.formatMeasure(this.props.measures.debt, 'SHORT_WORK_DUR')}
+ </IssuesLink>
+ </Measure>
+ <Measure composite={true}>
+ {this.renderSeverities()}
+ </Measure>
+ </MeasuresList>
+ {this.renderTimeline('before')}
+ </DomainNutshell>
+ {this.renderLeak()}
+ </DomainPanel>
+ </Domain>;
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js b/server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js
deleted file mode 100644
index 23e3fabd7e1..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js
+++ /dev/null
@@ -1,46 +0,0 @@
-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-name">{window.t('overview.metric.new_coverage')}</span>
- <span className="measure-value">
- <DrilldownLink component={this.props.component.key} metric="new_coverage" period="1">
- <Measure value={newCoverage} type="PERCENT"/>
- </DrilldownLink>
- </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/general/leak-dups.js b/server/sonar-web/src/main/js/apps/overview/general/leak-dups.js
deleted file mode 100644
index f59e996c967..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/leak-dups.js
+++ /dev/null
@@ -1,42 +0,0 @@
-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-name">{window.t('overview.metric.duplications')}</span>
- <span className="measure-value">
- <MeasureVariation value={density} type="PERCENT"/>
- </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/general/leak-issues.js b/server/sonar-web/src/main/js/apps/overview/general/leak-issues.js
deleted file mode 100644
index 29574509109..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/leak-issues.js
+++ /dev/null
@@ -1,69 +0,0 @@
-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, '1')).format('YYYY-MM-DDTHH:mm:ssZZ');
-
- return (
- <Card>
- <div className="measures">
- <div className="measure measure-big" data-metric="sqale_index">
- <span className="measure-name">{window.t('overview.metric.new_debt')}</span>
- <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>
- </div>
- <div className="measure measure-big" data-metric="violations">
- <span className="measure-name">{window.t('overview.metric.new_issues')}</span>
- <span className="measure-value">
- <IssuesLink component={this.props.component.key}
- params={{ resolved: 'false', createdAfter: periodDate }}>
- <Measure value={issues} type="SHORT_INT"/>
- </IssuesLink>
- </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/general/leak-size.js b/server/sonar-web/src/main/js/apps/overview/general/leak-size.js
deleted file mode 100644
index 5b09ddf0eed..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/leak-size.js
+++ /dev/null
@@ -1,30 +0,0 @@
-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-name">{window.t('overview.metric.lines')}</span>
- <span className="measure-value">
- <MeasureVariation value={lines} type="SHORT_INT"/>
- </span>
- </div>
- <div className="measure measure-big" data-metric="files">
- <span className="measure-name">{window.t('overview.metric.files')}</span>
- <span className="measure-value">
- <MeasureVariation value={files} type="SHORT_INT"/>
- </span>
- </div>
- </div>
- </Card>
- );
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak.js b/server/sonar-web/src/main/js/apps/overview/general/leak.js
deleted file mode 100644
index 1cef6139954..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/leak.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import _ from 'underscore';
-import moment from 'moment';
-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, getPeriodDate } from './../helpers/period-label';
-
-
-export default React.createClass({
- render() {
- if (_.size(this.props.component.periods) < 1) {
- return null;
- }
-
- let period = periodLabel(this.props.component.periods, '1');
- let periodDate = getPeriodDate(this.props.component.periods, '1');
-
- return (
- <div className="overview-leak">
- <h2 className="overview-title">
- {window.t('overview.water_leak')}
- <span className="overview-leak-period">{period} / {moment(periodDate).format('LL')}</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/general/main.js b/server/sonar-web/src/main/js/apps/overview/general/main.js
index 233e111063e..dd0d748788c 100644
--- a/server/sonar-web/src/main/js/apps/overview/general/main.js
+++ b/server/sonar-web/src/main/js/apps/overview/general/main.js
@@ -1,99 +1,189 @@
-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 MoreDetails from './details';
-import { getPeriodDate } from './../helpers/period-label';
+
+import { GeneralIssues } from './issues';
+import { GeneralCoverage } from './coverage';
+import { GeneralDuplications } from './duplications';
+import { GeneralSize } from './size';
+import { getPeriodLabel, getPeriodDate } from './../helpers/period-label';
+import { getMeasuresAndVariations } from '../../../api/measures';
+import { getFacet, getIssuesCount } from '../../../api/issues';
+import { getTimeMachineData } from '../../../api/time-machine';
+import { SEVERITIES, STATUSES } from '../../../helpers/constants';
+
+
+const METRICS_LIST = [
+ 'sqale_rating',
+ 'overall_coverage',
+ 'new_overall_coverage',
+ 'tests',
+ 'duplicated_lines_density',
+ 'duplicated_blocks',
+ 'ncloc',
+ 'files'
+];
+
+const HISTORY_METRICS_LIST = [
+ 'violations',
+ 'overall_coverage',
+ 'duplicated_lines_density',
+ 'ncloc'
+];
+
+
+function getFacetValue (facet, key) {
+ return _.findWhere(facet, { val: key }).count;
+}
+
export default React.createClass({
+ propTypes: {
+ leakPeriodIndex: React.PropTypes.string.isRequired
+ },
+
getInitialState() {
- return { leak: this.props.leak, measures: this.props.measures };
+ return {
+ ready: false,
+ history: {},
+ leakPeriodLabel: getPeriodLabel(this.props.component.periods, this.props.leakPeriodIndex),
+ leakPeriodDate: getPeriodDate(this.props.component.periods, this.props.leakPeriodIndex)
+ };
},
componentDidMount() {
- if (this._hasWaterLeak()) {
- this.requestLeakIssues();
- this.requestLeakDebt();
- }
- this.requestNutshellIssues();
- this.requestNutshellDebt();
- },
+ Promise.all([
+ this.requestMeasures(),
+ this.requestIssuesAndDebt(),
+ this.requestIssuesSeverities(),
+ this.requestLeakIssuesAndDebt(),
+ this.requestIssuesLeakSeverities(),
+ this.requestIssuesLeakStatuses()
+ ]).then(responses => {
+ let measures = this.getMeasuresValues(responses[0], 'value');
+ measures.issues = responses[1].issues;
+ measures.debt = responses[1].debt;
+ measures.issuesSeverities = SEVERITIES.map(s => getFacetValue(responses[2].facet, s));
- _hasWaterLeak() {
- return !!_.findWhere(this.props.component.periods, { index: '1' });
+ let leak;
+ if (this.state.leakPeriodLabel) {
+ leak = this.getMeasuresValues(responses[0], 'var' + this.props.leakPeriodIndex);
+ leak.issues = responses[3].issues;
+ leak.debt = responses[3].debt;
+ leak.issuesSeverities = SEVERITIES.map(s => getFacetValue(responses[4].facet, s));
+ leak.issuesStatuses = STATUSES.map(s => getFacetValue(responses[5].facet, s));
+ }
+
+ this.setState({
+ ready: true,
+ measures: measures,
+ leak: leak
+ }, this.requestHistory);
+ });
},
- _requestIssues(data) {
- let url = `${baseUrl}/api/issues/search`;
- data.ps = 1;
- data.componentUuids = this.props.component.id;
- return $.get(url, data);
+ requestMeasures () {
+ return getMeasuresAndVariations(this.props.component.key, METRICS_LIST);
},
- requestLeakIssues() {
- let createdAfter = moment(getPeriodDate(this.props.component.periods, '1')).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;
+ getMeasuresValues (measures, fieldKey) {
+ let values = {};
+ Object.keys(measures).forEach(measureKey => {
+ values[measureKey] = measures[measureKey][fieldKey];
+ });
+ return 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
- })
- });
+ requestIssuesAndDebt() {
+ // FIXME requesting severities facet only to get debtTotal
+ return getIssuesCount({
+ componentUuids: this.props.component.id,
+ resolved: 'false',
+ facets: 'severities'
});
},
- 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;
+ requestLeakIssuesAndDebt() {
+ if (!this.state.leakPeriodLabel) {
+ return Promise.resolve();
+ }
- 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
- })
- });
+ let createdAfter = moment(this.state.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+ // FIXME requesting severities facet only to get debtTotal
+ return getIssuesCount({
+ componentUuids: this.props.component.id,
+ createdAfter: createdAfter,
+ resolved: 'false',
+ facets: 'severities'
});
},
- requestLeakDebt() {
- let createdAfter = moment(getPeriodDate(this.props.component.periods, '1')).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 })
- });
- });
+ requestIssuesSeverities() {
+ return getFacet({ componentUuids: this.props.component.id, resolved: 'false' }, 'severities');
},
- requestNutshellDebt() {
- this._requestIssues({ resolved: 'false', facets: 'severities', facetMode: 'debt' }).done(r => {
- this.setState({
- measures: _.extend({}, this.state.measures, { debt: r.debtTotal })
+ requestIssuesLeakSeverities() {
+ if (!this.state.leakPeriodLabel) {
+ return Promise.resolve();
+ }
+
+ let createdAfter = moment(this.state.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+ return getFacet({
+ componentUuids: this.props.component.id,
+ createdAfter: createdAfter,
+ resolved: 'false'
+ }, 'severities');
+ },
+
+ requestIssuesLeakStatuses() {
+ if (!this.state.leakPeriodLabel) {
+ return Promise.resolve();
+ }
+
+ let createdAfter = moment(this.state.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+ return getFacet({
+ componentUuids: this.props.component.id,
+ createdAfter: createdAfter,
+ resolved: 'false'
+ }, 'statuses');
+ },
+
+ requestHistory () {
+ let metrics = HISTORY_METRICS_LIST.join(',');
+ return getTimeMachineData(this.props.component.key, metrics).then(r => {
+ let history = {};
+ r[0].cols.forEach((col, index) => {
+ history[col.metric] = r[0].cells.map(cell => {
+ let date = moment(cell.d).toDate();
+ let value = cell.v[index] || 0;
+ return { date, value };
+ });
});
+ this.setState({ history });
});
},
+ renderLoading () {
+ return <div className="text-center">
+ <i className="spinner spinner-margin"/>
+ </div>;
+ },
+
render() {
- return <div>
- <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} section={this.props.section}/>
- <MoreDetails component={this.props.component} measures={this.state.measures}
- section={this.props.section} onRoute={this.props.onRoute}/>
+ if (!this.state.ready) {
+ return this.renderLoading();
+ }
+
+ let props = _.extend({}, this.props, this.state);
+
+ return <div className="overview-domains">
+ <GeneralIssues {...props} history={this.state.history['violations']}/>
+ <GeneralCoverage {...props} history={this.state.history['overall_coverage']}/>
+ <GeneralDuplications {...props} history={this.state.history['duplicated_lines_density']}/>
+ <GeneralSize {...props} history={this.state.history['ncloc']}/>
</div>;
}
});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js
deleted file mode 100644
index dabcb76cc6b..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js
+++ /dev/null
@@ -1,47 +0,0 @@
-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-name">{window.t('overview.metric.coverage')}</span>
- <span className="measure-value">
- <DrilldownLink component={this.props.component.key} metric="overall_coverage">
- <Measure value={coverage} type="PERCENT"/>
- </DrilldownLink>
- </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/general/nutshell-dups.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js
deleted file mode 100644
index 6c4bb0b1c3a..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js
+++ /dev/null
@@ -1,47 +0,0 @@
-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-name">{window.t('overview.metric.duplications')}</span>
- <span className="measure-value">
- <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density">
- <Measure value={density} type="PERCENT"/>
- </DrilldownLink>
- </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/general/nutshell-issues.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js
deleted file mode 100644
index 3cad21fdc41..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js
+++ /dev/null
@@ -1,68 +0,0 @@
-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-name">{window.t('overview.metric.debt')}</span>
- <span className="measure-value">
- <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}>
- <Measure value={debt} type="SHORT_WORK_DUR"/>
- </IssuesLink>
- </span>
- </div>
- <div className="measure measure-big" data-metric="violations">
- <span className="measure-name">{window.t('overview.metric.issues')}</span>
- <span className="measure-value">
- <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}>
- <Measure value={issues} type="SHORT_INT"/>
- </IssuesLink>
- </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/general/nutshell-size.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js
deleted file mode 100644
index 845088f9491..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js
+++ /dev/null
@@ -1,35 +0,0 @@
-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-name">{window.t('overview.metric.lines')}</span>
- <span className="measure-value">
- <DrilldownLink component={this.props.component.key} metric="lines">
- <Measure value={lines} type="SHORT_INT"/>
- </DrilldownLink>
- </span>
- </div>
- <div className="measure measure-big" data-metric="files">
- <span className="measure-name">{window.t('overview.metric.files')}</span>
- <span className="measure-value">
- <DrilldownLink component={this.props.component.key} metric="files">
- <Measure value={files} type="SHORT_INT"/>
- </DrilldownLink>
- </span>
- </div>
- </div>
- </Card>
- );
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell.js
deleted file mode 100644
index e538149958e..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/general/nutshell.js
+++ /dev/null
@@ -1,26 +0,0 @@
-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/apps/overview/general/size.js b/server/sonar-web/src/main/js/apps/overview/general/size.js
new file mode 100644
index 00000000000..ac43ab6b001
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/general/size.js
@@ -0,0 +1,60 @@
+import React from 'react';
+
+import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
+import DrilldownLink from '../helpers/drilldown-link';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { getMetricName } from '../helpers/metrics';
+
+
+export const GeneralSize = React.createClass({
+ mixins: [TooltipsMixin, DomainMixin],
+
+ propTypes: {
+ leakPeriodLabel: React.PropTypes.string,
+ leakPeriodDate: React.PropTypes.object
+ },
+
+ renderLeak () {
+ if (!this.hasLeakPeriod()) {
+ return null;
+ }
+
+ return <DomainLeak>
+ <MeasuresList>
+ <Measure label={getMetricName('ncloc')}>
+ {window.formatMeasureVariation(this.props.leak['ncloc'], 'SHORT_INT')}
+ </Measure>
+ <Measure label={getMetricName('files')}>
+ {window.formatMeasureVariation(this.props.leak['files'], 'SHORT_INT')}
+ </Measure>
+ </MeasuresList>
+ {this.renderTimeline('after')}
+ </DomainLeak>;
+ },
+
+ render () {
+ return <Domain>
+ <DomainHeader title="Size"
+ leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
+
+ <DomainPanel domain="size">
+ <DomainNutshell>
+ <MeasuresList>
+ <Measure label={getMetricName('ncloc')}>
+ <DrilldownLink component={this.props.component.key} metric="ncloc">
+ {window.formatMeasure(this.props.measures['ncloc'], 'SHORT_INT')}
+ </DrilldownLink>
+ </Measure>
+ <Measure label={getMetricName('files')}>
+ <DrilldownLink component={this.props.component.key} metric="files">
+ {window.formatMeasure(this.props.measures['files'], 'SHORT_INT')}
+ </DrilldownLink>
+ </Measure>
+ </MeasuresList>
+ {this.renderTimeline('before')}
+ </DomainNutshell>
+ {this.renderLeak()}
+ </DomainPanel>
+ </Domain>;
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/timeline.js b/server/sonar-web/src/main/js/apps/overview/general/timeline.js
new file mode 100644
index 00000000000..3938fff0330
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/general/timeline.js
@@ -0,0 +1,47 @@
+import d3 from 'd3';
+import React from 'react';
+
+import { LineChart } from '../../../components/charts/line-chart';
+
+
+const HEIGHT = 80;
+
+
+export class Timeline extends React.Component {
+ filterSnapshots () {
+ return this.props.history.filter(s => {
+ let matchBefore = !this.props.before || s.date <= this.props.before;
+ let matchAfter = !this.props.after || s.date >= this.props.after;
+ return matchBefore && matchAfter;
+ });
+ }
+
+ render () {
+ let snapshots = this.filterSnapshots();
+
+ if (snapshots.length < 2) {
+ return null;
+ }
+
+ let data = snapshots.map((snapshot, index) => {
+ return { x: index, y: snapshot.value };
+ });
+
+ let domain = [0, d3.max(this.props.history, d => d.value)];
+
+ return <LineChart data={data}
+ domain={domain}
+ interpolate="basis"
+ displayBackdrop={true}
+ displayPoints={false}
+ displayVerticalGrid={false}
+ height={HEIGHT}
+ padding={[0, 0, 0, 0]}/>;
+ }
+}
+
+Timeline.propTypes = {
+ history: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ before: React.PropTypes.object,
+ after: React.PropTypes.object
+};
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
index d3118579036..f1331395671 100644
--- 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
@@ -5,7 +5,7 @@ export default React.createClass({
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}`;
+ url = `${window.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/metrics.js b/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js
index 1a1dcefcc47..f556841abe3 100644
--- a/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js
+++ b/server/sonar-web/src/main/js/apps/overview/helpers/metrics.js
@@ -19,3 +19,8 @@ export function filterMetricsForDomains (metrics, domains) {
return hasRightDomain(metric, domains) && isNotHidden(metric) && hasSimpleType(metric) && isNotDifferential(metric);
});
}
+
+
+export function getMetricName (metricKey) {
+ return window.t('overview.metric', metricKey);
+}
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
index 109a9df9a57..07cfcd43b9f 100644
--- 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
@@ -1,7 +1,7 @@
import _ from 'underscore';
import moment from 'moment';
-export let periodLabel = (periods, periodIndex) => {
+export let getPeriodLabel = (periods, periodIndex) => {
let period = _.findWhere(periods, { index: periodIndex });
if (!period) {
return null;
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/assignees.js b/server/sonar-web/src/main/js/apps/overview/issues/assignees.js
index af445cc981b..2d0905c8030 100644
--- a/server/sonar-web/src/main/js/apps/overview/issues/assignees.js
+++ b/server/sonar-web/src/main/js/apps/overview/issues/assignees.js
@@ -1,7 +1,6 @@
import React from 'react';
import Assignee from '../../../components/shared/assignee-helper';
import { DomainHeader } from '../domain/header';
-import { formatMeasure } from '../formatting';
import { componentIssuesUrl } from '../../../helpers/Url';
export default class extends React.Component {
@@ -13,7 +12,7 @@ export default class extends React.Component {
<Assignee user={s.user}/>
</td>
<td className="thin text-right">
- <a href={href}>{formatMeasure(s.count, 'violations')}</a>
+ <a href={href}>{window.formatMeasure(s.count, 'SHORT_INT')}</a>
</td>
</tr>;
});
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/main.js b/server/sonar-web/src/main/js/apps/overview/issues/main.js
index e55a2f0cf2b..d615b8d44a3 100644
--- a/server/sonar-web/src/main/js/apps/overview/issues/main.js
+++ b/server/sonar-web/src/main/js/apps/overview/issues/main.js
@@ -18,7 +18,7 @@ export default class OverviewDomain extends React.Component {
componentDidMount () {
Promise.all([
- this.requestSeverities(),
+ this.requestIssuesSeverities(),
this.requestTags(),
this.requestAssignees()
]).then(responses => {
@@ -43,7 +43,15 @@ export default class OverviewDomain extends React.Component {
}
render () {
- return <div className="overview-domain">
+ return <div className="overview-detailed-page">
+
+ <div className="overview-domain-header">
+ <h2 className="overview-title">Issues & Technical Debt</h2>
+ </div>
+
+ <a className="overview-detailed-page-back" href="#">
+ <i className="icon-chevron-left"/>
+ </a>
<IssuesTimeline {...this.props}/>
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/severities.js b/server/sonar-web/src/main/js/apps/overview/issues/severities.js
index be2911b9889..5ccb3098b86 100644
--- a/server/sonar-web/src/main/js/apps/overview/issues/severities.js
+++ b/server/sonar-web/src/main/js/apps/overview/issues/severities.js
@@ -2,7 +2,6 @@ import _ from 'underscore';
import React from 'react';
import SeverityHelper from '../../../components/shared/severity-helper';
import { DomainHeader } from '../domain/header';
-import { formatMeasure } from '../formatting';
import { componentIssuesUrl } from '../../../helpers/Url';
export default class extends React.Component {
@@ -19,7 +18,7 @@ export default class extends React.Component {
</td>
<td className="thin text-right">
<a className="cell-link" href={href}>
- {formatMeasure(s.count, 'violations')}
+ {window.formatMeasure(s.count, 'SHORT_INT')}
</a>
</td>
</tr>;
diff --git a/server/sonar-web/src/main/js/apps/overview/main.js b/server/sonar-web/src/main/js/apps/overview/main.js
deleted file mode 100644
index 5c19f4282f5..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/main.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import React from 'react';
-import offset from 'document-offset';
-
-import GeneralMain from './general/main';
-import IssuesMain from './issues/main';
-import CoverageMain from './coverage/main';
-import DuplicationsMain from './duplications/main';
-import SizeMain from './size/main';
-import Meta from './meta';
-import Empty from './general/empty';
-
-import { getMetrics } from '../../api/metrics';
-
-
-export const Overview = React.createClass({
- getInitialState () {
- let hash = window.location.hash;
- return { section: hash.length ? hash.substr(1) : null };
- },
-
- componentWillMount () {
- window.addEventListener('hashchange', this.handleHashChange);
- },
-
- componentDidMount () {
- if (this.props.component.hasSnapshot) {
- this.requestMetrics();
- }
- },
-
- componentWillUnmount () {
- window.removeEventListener('hashchange', this.handleHashChange);
- },
-
- requestMetrics () {
- return getMetrics().then(metrics => this.setState({ metrics }));
- },
-
- handleRoute (section) {
- if (section !== this.state.section) {
- let el = document.querySelector('.overview-more');
- this.setState({ section }, () => this.scrollToEl(el));
- window.location.href = '#' + section;
- } else {
- this.setState({ section: null });
- window.location.href = '#';
- }
- },
-
- handleHashChange () {
- let hash = window.location.hash;
- this.setState({ section: hash.substr(1) });
- },
-
- scrollToEl (el) {
- let top = offset(el).top;
- window.scrollTo(0, top);
- },
-
- render () {
- if (!this.props.component.hasSnapshot) {
- return <div className="overview"><Empty/></div>;
- }
-
- if (!this.state.metrics) {
- return null;
- }
-
- let child;
- switch (this.state.section) {
- case 'issues':
- child = <IssuesMain {...this.props} {...this.state}/>;
- break;
- case 'coverage':
- child = <CoverageMain {...this.props} {...this.state}/>;
- break;
- case 'duplications':
- child = <DuplicationsMain {...this.props} {...this.state}/>;
- break;
- case 'size':
- child = <SizeMain {...this.props} {...this.state}/>;
- break;
- default:
- child = null;
- }
-
- return <div className="overview">
- <div className="overview-main">
- <GeneralMain {...this.props} section={this.state.section} onRoute={this.handleRoute}/>
- {child}
- </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
index 310aadd8ae6..e3e632a30fa 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta.js
+++ b/server/sonar-web/src/main/js/apps/overview/meta.js
@@ -25,26 +25,26 @@ export default React.createClass({
});
let descriptionCard = this.props.component.description ? (
- <div className="overview-card">
+ <div className="overview-meta-card">
<div className="overview-meta-description">{this.props.component.description}</div>
</div>
) : null,
linksCard = _.size(this.props.component.links) > 0 ? (
- <div className="overview-card">
+ <div className="overview-meta-card">
<ul className="overview-meta-list">{links}</ul>
</div>
) : null,
profilesCard = _.size(this.props.component.profiles) > 0 ? (
- <div className="overview-card">
+ <div className="overview-meta-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">
+ <div className="overview-meta-card">
<h4 className="overview-meta-header">{window.t('overview.quality_gate')}</h4>
<ul className="overview-meta-list">
<li>
@@ -60,8 +60,8 @@ export default React.createClass({
<div className="overview-meta">
{descriptionCard}
{linksCard}
- {profilesCard}
{gateCard}
+ {profilesCard}
</div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/overview/overview.js b/server/sonar-web/src/main/js/apps/overview/overview.js
new file mode 100644
index 00000000000..f03573a2677
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/overview.js
@@ -0,0 +1,54 @@
+import React from 'react';
+
+import Gate from './gate/gate';
+import GeneralMain from './general/main';
+import Meta from './meta';
+import { getMetrics } from '../../api/metrics';
+
+
+export const Overview = React.createClass({
+ getInitialState () {
+ return { ready: false };
+ },
+
+ componentDidMount () {
+ this.requestMetrics();
+ },
+
+ requestMetrics () {
+ return getMetrics().then(metrics => this.setState({ ready: true, metrics }));
+ },
+
+ renderLoading () {
+ return <div className="text-center">
+ <i className="spinner spinner-margin"/>
+ </div>;
+ },
+
+ render () {
+ if (!this.state.ready) {
+ return this.renderLoading();
+ }
+
+ return <div className="overview">
+ <div className="overview-main">
+ <Gate component={this.props.component} gate={this.props.gate}/>
+ <GeneralMain {...this.props} {...this.state}/>
+ </div>
+ <Meta component={this.props.component}/>
+ </div>;
+ }
+});
+
+
+export const EmptyOverview = React.createClass({
+ render() {
+ return (
+ <div className="page">
+ <div className="alert alert-warning">
+ {window.t('provisioning.no_analysis')}
+ </div>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/size/main.js b/server/sonar-web/src/main/js/apps/overview/size/main.js
index 72f892c9db6..9e037e136c9 100644
--- a/server/sonar-web/src/main/js/apps/overview/size/main.js
+++ b/server/sonar-web/src/main/js/apps/overview/size/main.js
@@ -11,7 +11,15 @@ import { SizeTreemap } from './treemap';
export default class extends React.Component {
render () {
- return <div className="overview-domain">
+ return <div className="overview-detailed-page">
+ <div className="overview-domain-header">
+ <h2 className="overview-title">Size</h2>
+ </div>
+
+ <a className="overview-detailed-page-back" href="#">
+ <i className="icon-chevron-left"/>
+ </a>
+
<SizeTimeline {...this.props}/>
<div className="flex-columns">
diff --git a/server/sonar-web/src/main/js/components/charts/bar-chart.js b/server/sonar-web/src/main/js/components/charts/bar-chart.js
index c7444fedcb9..f82a9c7cda2 100644
--- a/server/sonar-web/src/main/js/components/charts/bar-chart.js
+++ b/server/sonar-web/src/main/js/components/charts/bar-chart.js
@@ -2,7 +2,7 @@ import d3 from 'd3';
import React from 'react';
import { ResizeMixin } from './mixins/resize-mixin';
-import { TooltipsMixin } from './mixins/tooltips-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
export const BarChart = React.createClass({
mixins: [ResizeMixin, TooltipsMixin],
diff --git a/server/sonar-web/src/main/js/components/charts/bubble-chart.js b/server/sonar-web/src/main/js/components/charts/bubble-chart.js
index e388a2baecb..9e3facaa74d 100644
--- a/server/sonar-web/src/main/js/components/charts/bubble-chart.js
+++ b/server/sonar-web/src/main/js/components/charts/bubble-chart.js
@@ -2,7 +2,7 @@ import d3 from 'd3';
import React from 'react';
import { ResizeMixin } from './mixins/resize-mixin';
-import { TooltipsMixin } from './mixins/tooltips-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
export const Bubble = React.createClass({
diff --git a/server/sonar-web/src/main/js/components/charts/line-chart.js b/server/sonar-web/src/main/js/components/charts/line-chart.js
index f7c560bd3e1..eeaed5e88a9 100644
--- a/server/sonar-web/src/main/js/components/charts/line-chart.js
+++ b/server/sonar-web/src/main/js/components/charts/line-chart.js
@@ -2,7 +2,7 @@ import d3 from 'd3';
import React from 'react';
import { ResizeMixin } from './mixins/resize-mixin';
-import { TooltipsMixin } from './mixins/tooltips-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
export const LineChart = React.createClass({
@@ -124,14 +124,21 @@ export const LineChart = React.createClass({
let availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3];
let availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2];
- let maxY = d3.max(this.props.data, d => d.y);
+ let maxY;
let xScale = d3.scale.linear()
.domain(d3.extent(this.props.data, d => d.x))
.range([0, availableWidth]);
let yScale = d3.scale.linear()
- .domain([0, maxY])
.range([availableHeight, 0]);
+ if (this.props.domain) {
+ maxY = this.props.domain[1];
+ yScale.domain(this.props.domain);
+ } else {
+ maxY = d3.max(this.props.data, d => d.y);
+ yScale.domain([0, maxY]);
+ }
+
return <svg className="line-chart" width={this.state.width} height={this.state.height}>
<g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
{this.renderVerticalGrid(xScale, yScale, maxY)}
diff --git a/server/sonar-web/src/main/js/components/charts/treemap.js b/server/sonar-web/src/main/js/components/charts/treemap.js
index be054fdc055..24bcff49ce2 100644
--- a/server/sonar-web/src/main/js/components/charts/treemap.js
+++ b/server/sonar-web/src/main/js/components/charts/treemap.js
@@ -3,7 +3,7 @@ import d3 from 'd3';
import React from 'react';
import { ResizeMixin } from './mixins/resize-mixin';
-import { TooltipsMixin } from './mixins/tooltips-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
const SIZE_SCALE = d3.scale.linear()
diff --git a/server/sonar-web/src/main/js/components/charts/word-cloud.js b/server/sonar-web/src/main/js/components/charts/word-cloud.js
index ed59b040fbf..1edbc2274ec 100644
--- a/server/sonar-web/src/main/js/components/charts/word-cloud.js
+++ b/server/sonar-web/src/main/js/components/charts/word-cloud.js
@@ -2,7 +2,7 @@ import _ from 'underscore';
import d3 from 'd3';
import React from 'react';
-import { TooltipsMixin } from './mixins/tooltips-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
export const Word = React.createClass({
propTypes: {
diff --git a/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
index 240edee02c5..240edee02c5 100644
--- a/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js
+++ b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
diff --git a/server/sonar-web/src/main/js/helpers/constants.js b/server/sonar-web/src/main/js/helpers/constants.js
new file mode 100644
index 00000000000..392adf2202a
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/constants.js
@@ -0,0 +1,2 @@
+export const SEVERITIES = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
+export const STATUSES = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED'];
diff --git a/server/sonar-web/src/main/js/libs/application.js b/server/sonar-web/src/main/js/libs/application.js
index 8692a73f9cb..8d2236d9710 100644
--- a/server/sonar-web/src/main/js/libs/application.js
+++ b/server/sonar-web/src/main/js/libs/application.js
@@ -281,7 +281,7 @@ function closeModalWindow () {
function shortIntVariationFormatter (value) {
if (value === 0) {
- return '0';
+ return '+0';
}
var format = '+0,0';
if (Math.abs(value) >= 1000) {
@@ -354,21 +354,6 @@ function closeModalWindow () {
}
/**
- * Check if about sign be displayed for a work duration
- * @param {number} days
- * @param {number} hours
- * @param {number} minutes
- * @returns {boolean}
- */
- function shouldDisplayAbout (days, hours, minutes) {
- var hasDays = days > 0,
- fewDays = days < 5,
- hasHours = hours > 0,
- hasMinutes = minutes > 0;
- return (hasDays && fewDays && hasHours) || (!hasDays && hasHours && hasMinutes);
- }
-
- /**
* Format a work duration based on parameters
* @param {bool} isNegative
* @param {number} days
@@ -414,9 +399,6 @@ function closeModalWindow () {
formatted = addSpaceIfNeeded(formatted);
formatted += tp('work_duration.x_minutes', isNegative && formatted.length === 0 ? -1 * minutes : minutes);
}
- if (shouldDisplayAbout(days, hours, minutes)) {
- formatted = tp('work_duration.about', formatted);
- }
return formatted;
}
@@ -432,9 +414,9 @@ function closeModalWindow () {
var hoursInDay = window.SS.hoursInDay || 8,
isNegative = value < 0,
absValue = Math.abs(value);
- var days = Math.floor(absValue / hoursInDay / 60);
+ var days = Math.round(absValue / hoursInDay / 60);
var remainingValue = absValue - days * hoursInDay * 60;
- var hours = Math.floor(remainingValue / 60);
+ var hours = Math.round(remainingValue / 60);
remainingValue -= hours * 60;
return formatDuration(isNegative, days, hours, remainingValue);
};
@@ -462,6 +444,7 @@ function closeModalWindow () {
/**
* Format a work duration variation
* @param {number} value
+ * @returns {string}
*/
var durationVariationFormatter = function (value) {
if (value === 0) {
@@ -472,6 +455,19 @@ function closeModalWindow () {
};
/**
+ * Format a work duration variation
+ * @param {number} value
+ * @returns {string}
+ */
+ var shortDurationVariationFormatter = function (value) {
+ if (value === 0) {
+ return '0';
+ }
+ var formatted = shortDurationFormatter(value);
+ return formatted[0] !== '-' ? '+' + formatted : formatted;
+ };
+
+ /**
* Format a rating measure
* @param {number} value
*/
@@ -552,9 +548,10 @@ function closeModalWindow () {
return value === 0 ? '0' : numeral(value).format('+0,0.0');
},
'PERCENT': function (value) {
- return value === 0 ? '0%' : numeral(+value / 100).format('+0,0.0%');
+ return value === 0 ? '+0%' : numeral(+value / 100).format('+0,0.0%');
},
- 'WORK_DUR': durationVariationFormatter
+ 'WORK_DUR': durationVariationFormatter,
+ 'SHORT_WORK_DUR': shortDurationVariationFormatter
};
if (measure != null && type != null) {
formatted = formatters[type] != null ? formatters[type](measure) : measure;
diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less
index 072a4cea062..2e519588943 100644
--- a/server/sonar-web/src/main/less/pages/overview.less
+++ b/server/sonar-web/src/main/less/pages/overview.less
@@ -3,208 +3,105 @@
@import (reference) "../init/type";
@import (reference) "../init/links";
-@side-padding: 30px;
+@side-padding: 20px;
.overview {
display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
width: 100%;
min-height: ~"calc(100vh - @{navbarGlobalHeight} - @{navbarContextHeight} - @{pageFooterHeight})";
-}
-
-.overview > .panel {
- flex: 1;
+ overflow-x: hidden;
+ animation: fadeIn 0.5s forwards;
}
.overview-main {
- flex: 1;
+ flex: 4;
box-sizing: border-box;
- background-color: #fff;
+ background-color: @barBackgroundColor;
+ transition: transform 0.5s ease, opacity 0.5s ease;
}
-.overview-gate {
- .clearfix;
- padding: 50px 0 25px;
-}
+/*
+ * Gate
+ */
-.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 {
+ margin-right: 20px;
+ padding: 15px 0;
+ border-bottom: 1px solid @barBorderColor;
+ background-color: @barBackgroundColor;
-.overview-gate-box-warn {
- background-color: @orange;
+ .overview-title {
+ margin: 0 @side-padding;
+ }
}
-.overview-gate-box-ok {
- background-color: @green;
+.overview-gate-conditions-list {
+ display: flex;
+ flex-wrap: wrap;
}
-.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 {
+ padding: 10px @side-padding;
}
.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;
+ margin-right: 4px;
font-weight: 300;
- font-size: 22px;
+ font-size: 24px;
}
-.overview-gate-condition-itself {
- padding-left: 4px;
- color: mix(@baseFontColor, @barBackgroundColor, 70%);
- font-size: 13px;
- font-weight: 400;
+.overview-gate-warning {
+ margin: 15px @side-padding 0;
}
-.overview-gate-condition-level {
- margin-top: 8px;
-}
-
-.overview-leak {
- padding: 50px 0 25px;
- border-top: 1px solid @barBorderColor;
- border-bottom: 1px solid @barBorderColor;
-}
+/*
+ * Title
+ */
.overview-title {
- padding: 0 @side-padding;
- font-size: 18px;
+ font-size: 16px;
font-weight: 400;
& > .badge {
position: relative;
top: -2px;
margin-left: 15px;
- padding: 8px 15px;
- font-size: 16px;
- letter-spacing: 0.04em;
+ padding: 6px 12px;
+ font-size: 14px;
+ letter-spacing: 0.05em;
}
}
-.overview-leak-period {
- margin-left: 10px;
- font-size: 14px;
-}
-
-.overview-nutshell {
- padding: 50px 0 25px;
-}
+/*
+ * Cards
+ * TODO drop it
+ */
.overview-cards {
display: flex;
+ flex-wrap: wrap;
}
-.overview-card {
- flex: 1 0 25%;
- padding: 25px @side-padding;
- 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: @side-padding;
- }
-
- .measure-big .measure-name {
- margin-top: 0;
- margin-bottom: 2px;
- }
-
- .list-inline {
- margin-left: -10px;
- margin-right: -10px;
-
- & > li {
- padding-left: 10px;
- padding-right: 10px;
- }
- }
-}
-
-.overview-card-section {
- padding: 0;
- text-align: center;
-
- a {
- display: block;
- padding: 25px @side-padding;
- .link-no-underline;
- cursor: pointer;
- transition: none;
- }
-}
-
-.overview-card-section.active a,
-.overview-card-section a:hover {
- background-color: #2c3946;
- color: mix(#fff, #2c3946, 75%);
-}
-
-.overview-measure {
- font-size: 28px;
-}
-
-.overview-measure-label {
- font-size: 16px;
-}
+/*
+ * Meta
+ */
.overview-meta {
- width: 240px;
- border-left: 1px solid @barBorderColor;
+ flex: 1;
box-sizing: border-box;
background-color: @barBackgroundColor;
-
- .panel {
- border: none !important;
- }
}
-.overview-meta .overview-card {
- width: auto;
+.overview-meta-card {
+ min-width: 200px;
+ padding: @side-padding;
+ box-sizing: border-box;
}
.overview-meta-description {
@@ -222,165 +119,140 @@
}
}
-.overview-domain {
- margin-top: -25px;
-}
-
-.overview-domain-dark {
- background-color: #2c3946;
- color: mix(#fff, #2c3946, 75%);
-
- a {
- color: @blue;
- border-bottom-color: @darkBlue;
-
- &:hover, &:focus {
- border-bottom-color: @blue;
- }
- }
+/*
+ * Domain
+ */
- .overview-title {
- color: mix(#fff, #2c3946, 75%);
- }
-
- table.data.zebra > tbody > tr:nth-child(odd) {
- background-color: mix(#fff, #2c3946, 5%);;
- }
+.overview-domains {
+ animation: fadeIn 0.5s forwards;
}
-.overview-domain-section {
- padding: 50px @side-padding;
-
- .overview-title {
- margin-bottom: 25px;
- padding-left: 0;
- padding-right: 0;
- }
+.overview-domain {
+ margin: 30px @side-padding;
}
.overview-domain-header {
display: flex;
align-items: baseline;
- margin-bottom: 20px;;
- padding: 50px @side-padding 0;
+ justify-content: space-between;
+ margin-bottom: 10px;
.overview-title {
flex: 1;
- margin: 0;
- padding: 0;
}
}
-.overview-timeline {
+.overview-domain-panel {
+ display: flex;
+ margin-top: 10px;
+ border: 1px solid @barBorderColor;
+ background-color: #fff;
+ overflow: hidden;
+}
+
+.overview-domain-nutshell,
+.overview-domain-leak {
position: relative;
+ padding: 30px 10px;
+}
- .line-chart {
+.overview-domain-nutshell {
+ flex: 2;
+ .line-chart-backdrop {
+ fill: #e5f1f9;
}
+}
- .line-chart-grid {
- shape-rendering: crispedges;
- stroke: #384653;
- }
+.overview-domain-leak {
+ flex: 1;
+ background-color: #fffae7;
- .line-chart-path {
- fill: none;
- stroke-width: 2;
- stroke: @blue;
+ .line-chart-backdrop {
+ fill: #f1ecd1;
}
+}
- .line-chart-point {
- fill: @blue;
- stroke: none;
- }
+.overview-domain-measures {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ flex: 1;
+ justify-content: space-around;
+ align-items: center;
+}
+
+.overview-domain-measures + .overview-domain-measures {
+ margin-top: 30px;
- .line-chart-tick {
- fill: mix(#fff, #2c3946);
- font-size: 11px;
- text-anchor: middle;
+ .overview-domain-measure-value {
+ font-size: 14px;
+ font-weight: 400;
}
- .line-chart-backdrop {
- fill: #4b9fd5;
- fill-opacity: 0.2;
+ .overview-domain-measure-label {
+ margin-top: 4px;
}
}
-.overview-timeline-select {
- height: @formControlHeight;
- border: 1px solid mix(#fff, #2c3946);
- background-color: transparent;
- color: mix(#fff, #2c3946);
-}
+.overview-domain-measure {
-.overview-bubble-chart {
- .bubble-chart-tick {
- fill: mix(#fff, #2c3946);
- font-size: 11px;
- text-anchor: middle;
- }
+}
- .bubble-chart-tick-y {
- text-anchor: end;
- }
+.overview-domain-measure-value {
+ line-height: 1;
+ font-size: 36px;
+ font-weight: 300;
+ text-align: center;
+}
- .bubble-chart-bubble {
- stroke: @blue;
- fill: @blue;
- fill-opacity: 0.2;
- transition: fill-opacity 0.2s ease;
+.overview-domain-measure-label {
+ margin-top: 10px;
+ text-align: center;
+}
- &:hover {
- fill-opacity: 0.5;
- }
- }
+.overview-domain-timeline {
+ position: absolute;
+ z-index: 1;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ animation: fadeIn 0.5s forwards;
- .bubble-chart-grid {
- stroke: #ccc;
+ .line-chart-path {
+ fill: none;
+ stroke: none;
}
}
-.overview-bar-chart {
- .bar-chart-bar {
- fill: @blue;
- }
- .bar-chart-tick {
- fill: @baseFontColor;
- font-size: 11px;
- text-anchor: middle;
+
+/*
+ * Responsive Stuff
+ */
+
+@media (max-width: 1200px) {
+ .overview {
+ display: block;
}
-}
-.overview-treemap {
- .overview-domain-header {
- padding-top: 0;
- padding-left: 0;
- padding-right: 0;
+ .overview-meta {
+ display: flex;
+ justify-content: flex-start;
}
-}
-.overview-chart-placeholder {
- display: flex;
- justify-content: center;
- align-items: center;
- align-content: center;
+ .overview-meta .overview-meta-card {
+ max-width: 25%;
+ }
}
-.overview-paragraph {
- padding: 0 @side-padding;
-}
-.overview-more {
- padding-top: 50px;
- padding-bottom: 25px;
- border-top: 1px solid @barBorderColor;
- .overview-title {
- padding-bottom: 25px;
- }
+/*
+ * Animations
+ */
- .overview-card + .overview-card {
- border-left: 1px solid @barBorderColor;
- }
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
}
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
index c86e998a518..b9447a88eb5 100644
--- 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
@@ -119,61 +119,9 @@
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(1) -%>',
- <% end %>
- <% if @snapshot.measure('tests') %>
- tests: '<%= @snapshot.measure('tests').variation(1) -%>',
- <% end %>
-
- // duplications
- duplications: '<%= @snapshot.measure('duplicated_lines_density').variation(1) -%>',
- duplicatedLines: '<%= @snapshot.measure('duplicated_lines').variation(1) -%>',
- duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').variation(1) -%>',
-
- // size
- lines: '<%= @snapshot.measure('lines').variation(1) -%>',
- files: '<%= @snapshot.measure('files').variation(1) -%>'
- <% end %>
- };
-
window.sonarqube.overview = {
component: component,
- gate: gate,
- measures: measures,
- leak: leak
+ gate: gate
};
})();
</script>