aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-11-05 09:54:55 +0100
committerStas Vilchik <vilchiks@gmail.com>2015-11-06 16:48:25 +0100
commitbb00fc56370280793250bc06638f689295af2f01 (patch)
treedf8cafb77a2032ab7c0063b289e2c9621636e303 /server
parent266e3139c3ef6112d43d3422489a0654937b1816 (diff)
downloadsonarqube-bb00fc56370280793250bc06638f689295af2f01.tar.gz
sonarqube-bb00fc56370280793250bc06638f689295af2f01.zip
SONAR-6357 add detailed "Size" panel for the "Overview" main page
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/overview/app.js1
-rw-r--r--server/sonar-web/src/main/js/apps/overview/common-components.js38
-rw-r--r--server/sonar-web/src/main/js/apps/overview/domain/timeline.js167
-rw-r--r--server/sonar-web/src/main/js/apps/overview/domain/treemap.js8
-rw-r--r--server/sonar-web/src/main/js/apps/overview/helpers/metrics.js18
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/components.js (renamed from server/sonar-web/src/main/js/apps/overview/general/components.js)12
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/coverage.js (renamed from server/sonar-web/src/main/js/apps/overview/general/coverage.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/duplications.js (renamed from server/sonar-web/src/main/js/apps/overview/general/duplications.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/gate/gate-condition.js (renamed from server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js)6
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/gate/gate-conditions.js (renamed from server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/gate/gate-empty.js (renamed from server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/gate/gate.js (renamed from server/sonar-web/src/main/js/apps/overview/gate/gate.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/issues.js (renamed from server/sonar-web/src/main/js/apps/overview/general/issues.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/main.js (renamed from server/sonar-web/src/main/js/apps/overview/general/main.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/size.js (renamed from server/sonar-web/src/main/js/apps/overview/general/size.js)3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/main/timeline.js (renamed from server/sonar-web/src/main/js/apps/overview/general/timeline.js)0
-rw-r--r--server/sonar-web/src/main/js/apps/overview/overview.js37
-rw-r--r--server/sonar-web/src/main/js/apps/overview/size/comments-details.js19
-rw-r--r--server/sonar-web/src/main/js/apps/overview/size/complexity-details.js21
-rw-r--r--server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js47
-rw-r--r--server/sonar-web/src/main/js/apps/overview/size/language-distribution.js82
-rw-r--r--server/sonar-web/src/main/js/apps/overview/size/main.js174
-rw-r--r--server/sonar-web/src/main/js/apps/overview/size/size-details.js24
-rw-r--r--server/sonar-web/src/main/js/apps/overview/size/timeline.js16
-rw-r--r--server/sonar-web/src/main/js/apps/overview/size/treemap.js10
-rw-r--r--server/sonar-web/src/main/js/apps/overview/timeline/domain-timeline.js201
-rw-r--r--server/sonar-web/src/main/js/apps/overview/timeline/timeline-chart.js132
-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/histogram.js93
-rw-r--r--server/sonar-web/src/main/js/components/charts/line-chart.js2
-rw-r--r--server/sonar-web/src/main/js/components/charts/treemap.js9
-rw-r--r--server/sonar-web/src/main/js/components/mixins/resize-mixin.js (renamed from server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js)0
-rw-r--r--server/sonar-web/src/main/js/components/router/router.js48
-rw-r--r--server/sonar-web/src/main/js/helpers/measures.js28
-rw-r--r--server/sonar-web/src/main/less/init/icons.less2
-rw-r--r--server/sonar-web/src/main/less/pages/overview.less225
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb1
-rw-r--r--server/sonar-web/tests/apps/overview-test.js6
39 files changed, 1003 insertions, 431 deletions
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 418784e1405..48fae6531f7 100644
--- a/server/sonar-web/src/main/js/apps/overview/app.js
+++ b/server/sonar-web/src/main/js/apps/overview/app.js
@@ -13,6 +13,7 @@ class App {
start (options) {
let opts = _.extend({}, options, window.sonarqube.overview);
_.extend(opts.component, options.component);
+ opts.urlRoot = window.baseUrl + '/overview';
$('html').toggleClass('dashboard-page', opts.component.hasSnapshot);
let el = document.querySelector(opts.el);
diff --git a/server/sonar-web/src/main/js/apps/overview/common-components.js b/server/sonar-web/src/main/js/apps/overview/common-components.js
new file mode 100644
index 00000000000..993b98bc672
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/common-components.js
@@ -0,0 +1,38 @@
+import React from 'react';
+
+import { formatMeasure, formatMeasureVariation, localizeMetric } from '../../helpers/measures';
+import DrilldownLink from './helpers/drilldown-link';
+import { getShortType } from './helpers/metrics';
+
+
+export const DetailedMeasure = React.createClass({
+ renderLeak () {
+ if (!this.props.leakPeriodDate) {
+ return null;
+ }
+ let leak = this.props.leak[this.props.metric];
+ return <div className="overview-detailed-measure-leak">
+ <span className="overview-detailed-measure-value">
+ {formatMeasureVariation(leak, getShortType(this.props.type))}
+ </span>
+ </div>;
+ },
+
+ render () {
+ let measure = this.props.measures[this.props.metric];
+ if (measure == null) {
+ return null;
+ }
+
+ return <div className="overview-detailed-measure">
+ <div className="overview-detailed-measure-nutshell">
+ <span>{localizeMetric(this.props.metric)}</span>
+ <DrilldownLink component={this.props.component.key} metric={this.props.metric}>
+ <span className="overview-detailed-measure-value">{formatMeasure(measure, this.props.type)}</span>
+ </DrilldownLink>
+ {this.props.children}
+ </div>
+ {this.renderLeak()}
+ </div>;
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/timeline.js b/server/sonar-web/src/main/js/apps/overview/domain/timeline.js
index 88b95bcef34..9e5f5f8a47c 100644
--- a/server/sonar-web/src/main/js/apps/overview/domain/timeline.js
+++ b/server/sonar-web/src/main/js/apps/overview/domain/timeline.js
@@ -1,174 +1,7 @@
-import _ from 'underscore';
-import moment from 'moment';
-import React from 'react';
-import { LineChart } from '../../../components/charts/line-chart';
-import { getTimeMachineData } from '../../../api/time-machine';
-import { getEvents } from '../../../api/events';
-import { formatMeasure } from '../../../helpers/measures';
-const HEIGHT = 280;
-function parseValue (value, type) {
- return type === 'RATING' && typeof value === 'string' ?
- value.charCodeAt(0) - 'A'.charCodeAt(0) + 1 :
- value;
-}
-export class DomainTimeline extends React.Component {
- constructor (props) {
- super(props);
- this.state = { loading: true, currentMetric: props.initialMetric };
- }
-
- componentDidMount () {
- Promise.all([
- this.requestTimeMachineData(),
- this.requestEvents()
- ]).then(() => this.setState({ loading: false }));
- }
-
- requestTimeMachineData () {
- return getTimeMachineData(this.props.component.key, this.state.currentMetric).then(r => {
- let snapshots = r[0].cells.map(cell => {
- return { date: moment(cell.d).toDate(), value: cell.v[0] };
- });
- this.setState({ snapshots });
- });
- }
-
- requestEvents () {
- return getEvents(this.props.component.key, 'Version').then(r => {
- let events = r.map(event => {
- return { version: event.n, date: moment(event.dt).toDate() };
- });
- events = _.sortBy(events, 'date');
- this.setState({ events });
- });
- }
-
- prepareEvents () {
- let events = this.state.events;
- let snapshots = this.state.snapshots;
- return events
- .map(event => {
- let snapshot = snapshots.find(s => s.date.getTime() === event.date.getTime());
- event.value = snapshot && snapshot.value;
- return event;
- })
- .filter(event => event.value != null);
- }
-
- handleMetricChange () {
- let metric = this.refs.metricSelect.value;
- this.setState({ currentMetric: metric }, this.requestTimeMachineData);
- }
-
- groupMetricsByDomain () {
- return _.sortBy(
- _.map(
- _.groupBy(this.props.metrics, 'domain'),
- (metricList, domain) => {
- return {
- domain: domain,
- metrics: _.sortBy(metricList, 'name')
- };
- }),
- 'domain'
- );
- }
-
- renderLoading () {
- return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
- <i className="spinner"/>
- </div>;
- }
-
- renderWhenNoHistoricalData () {
- return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
- There is no historical data.
- </div>;
- }
-
- renderLineChart () {
- if (this.state.loading) {
- return this.renderLoading();
- }
-
- let events = this.prepareEvents();
-
- if (!events.length) {
- return this.renderWhenNoHistoricalData();
- }
-
- let currentMetricType = _.findWhere(this.props.metrics, { key: this.state.currentMetric }).type;
-
- let data = events.map((event, index) => {
- return { x: index, y: parseValue(event.value, currentMetricType) };
- });
-
- let xTicks = events.map(event => event.version.substr(0, 6));
-
- let xValues = events.map(event => {
- return currentMetricType === 'RATING' ? event.value : formatMeasure(event.value, currentMetricType);
- });
-
- // TODO use leak period
- let backdropConstraints = [
- this.state.events.length - 2,
- this.state.events.length - 1
- ];
-
- return <LineChart data={data}
- xTicks={xTicks}
- xValues={xValues}
- backdropConstraints={backdropConstraints}
- height={HEIGHT}
- interpolate="linear"
- padding={[25, 30, 50, 30]}/>;
- }
-
- renderMetricOption (metric) {
- return <option key={metric.key} value={metric.key}>{metric.name}</option>;
- }
-
- renderTimelineMetricSelect () {
- if (this.state.loading) {
- return null;
- }
- let groupedMetrics = this.groupMetricsByDomain();
- let inner;
- if (groupedMetrics.length > 1) {
- inner = groupedMetrics.map(metricGroup => {
- let options = metricGroup.metrics.map(this.renderMetricOption);
- return <optgroup key={metricGroup.domain} label={metricGroup.domain}>{options}</optgroup>;
- });
- } else {
- inner = groupedMetrics[0].metrics.map(this.renderMetricOption);
- }
- return <select ref="metricSelect"
- className="overview-timeline-select"
- onChange={this.handleMetricChange.bind(this)}
- value={this.state.currentMetric}>{inner}</select>;
- }
-
- render () {
- return <div className="overview-timeline overview-domain-dark">
- <div className="overview-domain-header">
- <h2 className="overview-title">Project History</h2>
- {this.renderTimelineMetricSelect()}
- </div>
- <div>
- {this.renderLineChart()}
- </div>
- </div>;
- }
-}
-
-DomainTimeline.propTypes = {
- metrics: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
- initialMetric: React.PropTypes.string.isRequired
-};
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/treemap.js b/server/sonar-web/src/main/js/apps/overview/domain/treemap.js
index 2bc32e36b7e..e22345f78d5 100644
--- a/server/sonar-web/src/main/js/apps/overview/domain/treemap.js
+++ b/server/sonar-web/src/main/js/apps/overview/domain/treemap.js
@@ -5,7 +5,7 @@ import { Treemap } from '../../../components/charts/treemap';
import { getChildren } from '../../../api/components';
import { formatMeasure } from '../../../helpers/measures';
-const HEIGHT = 360;
+const HEIGHT = 302;
export class DomainTreemap extends React.Component {
@@ -79,15 +79,15 @@ export class DomainTreemap extends React.Component {
render () {
let color = this.props.colorMetric ? <li>Color: {this.state.colorMetric.name}</li> : null;
- return <div className="overview-domain-section overview-treemap">
+ return <div className="overview-domain overview-domain-chart">
<div className="overview-domain-header">
- <h2 className="overview-title">Project Components</h2>
+ <h2 className="overview-title">Treemap</h2>
<ul className="list-inline small">
<li>Size: {this.state.sizeMetric.name}</li>
{color}
</ul>
</div>
- <div>
+ <div className="overview-treemap">
{this.renderTreemap()}
</div>
</div>;
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 f556841abe3..4b289bc3611 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
@@ -14,12 +14,26 @@ function isNotDifferential (metric) {
return metric.key.indexOf('new_') !== 0;
}
-export function filterMetricsForDomains (metrics, domains) {
+export function filterMetrics (metrics) {
return metrics.filter(metric => {
- return hasRightDomain(metric, domains) && isNotHidden(metric) && hasSimpleType(metric) && isNotDifferential(metric);
+ return isNotHidden(metric) && hasSimpleType(metric) && isNotDifferential(metric);
});
}
+export function filterMetricsForDomains (metrics, domains) {
+ return filterMetrics(metrics).filter(metric => hasRightDomain(metric, domains));
+}
+
+
+export function getShortType (type) {
+ if (type === 'INT') {
+ return 'SHORT_INT';
+ } else if (type === 'WORK_DUR') {
+ return 'SHORT_WORK_DUR';
+ }
+ return type;
+}
+
export function getMetricName (metricKey) {
return window.t('overview.metric', metricKey);
diff --git a/server/sonar-web/src/main/js/apps/overview/general/components.js b/server/sonar-web/src/main/js/apps/overview/main/components.js
index 441f6d12e01..6371375dc2e 100644
--- a/server/sonar-web/src/main/js/apps/overview/general/components.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/components.js
@@ -2,6 +2,7 @@ import moment from 'moment';
import React from 'react';
import { Timeline } from './timeline';
+import { navigate } from '../../../components/router/router';
export const Domain = React.createClass({
@@ -25,16 +26,23 @@ export const DomainLeakTitle = React.createClass({
}
let momentDate = moment(this.props.date);
let fromNow = momentDate.fromNow();
- let tooltip = 'Started ' + fromNow + ', ' + momentDate.format('LLL');
+ let tooltip = 'Started ' + fromNow + ', ' + momentDate.format('LL');
return <span title={tooltip} data-toggle="tooltip">Water Leak: {this.props.label}</span>;
}
});
export const DomainHeader = React.createClass({
+ handleClick(e) {
+ e.preventDefault();
+ navigate(this.props.linkTo);
+ },
+
render () {
return <div className="overview-domain-header">
- <DomainTitle>{this.props.title}</DomainTitle>
+ <DomainTitle>
+ <a onClick={this.handleClick} href="#">{this.props.title}</a>
+ </DomainTitle>
<DomainLeakTitle label={this.props.leakPeriodLabel} date={this.props.leakPeriodDate}/>
</div>;
}
diff --git a/server/sonar-web/src/main/js/apps/overview/general/coverage.js b/server/sonar-web/src/main/js/apps/overview/main/coverage.js
index 2af648184ac..2af648184ac 100644
--- a/server/sonar-web/src/main/js/apps/overview/general/coverage.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/coverage.js
diff --git a/server/sonar-web/src/main/js/apps/overview/general/duplications.js b/server/sonar-web/src/main/js/apps/overview/main/duplications.js
index bd95a2d4a39..bd95a2d4a39 100644
--- a/server/sonar-web/src/main/js/apps/overview/general/duplications.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/duplications.js
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/main/gate/gate-condition.js
index 5699a79aeeb..0819d6002c8 100644
--- a/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/gate/gate-condition.js
@@ -1,8 +1,8 @@
import React from 'react';
-import Measure from './../helpers/measure';
-import { getPeriodLabel, getPeriodDate } from './../helpers/period-label';
-import DrilldownLink from './../helpers/drilldown-link';
+import Measure from '../../helpers/measure';
+import { getPeriodLabel, getPeriodDate } from '../../helpers/period-label';
+import DrilldownLink from '../../helpers/drilldown-link';
export default React.createClass({
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/main/gate/gate-conditions.js
index adefecbded4..adefecbded4 100644
--- a/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/gate/gate-conditions.js
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/main/gate/gate-empty.js
index 61347185593..61347185593 100644
--- a/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/gate/gate-empty.js
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate.js b/server/sonar-web/src/main/js/apps/overview/main/gate/gate.js
index 076cbcd376a..076cbcd376a 100644
--- a/server/sonar-web/src/main/js/apps/overview/gate/gate.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/gate/gate.js
diff --git a/server/sonar-web/src/main/js/apps/overview/general/issues.js b/server/sonar-web/src/main/js/apps/overview/main/issues.js
index e34bb88b073..e34bb88b073 100644
--- a/server/sonar-web/src/main/js/apps/overview/general/issues.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/issues.js
diff --git a/server/sonar-web/src/main/js/apps/overview/general/main.js b/server/sonar-web/src/main/js/apps/overview/main/main.js
index dd0d748788c..dd0d748788c 100644
--- a/server/sonar-web/src/main/js/apps/overview/general/main.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/main.js
diff --git a/server/sonar-web/src/main/js/apps/overview/general/size.js b/server/sonar-web/src/main/js/apps/overview/main/size.js
index 8642b9918ae..e639a239e7e 100644
--- a/server/sonar-web/src/main/js/apps/overview/general/size.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/size.js
@@ -35,8 +35,7 @@ export const GeneralSize = React.createClass({
render () {
return <Domain>
- <DomainHeader title="Size"
- leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
+ <DomainHeader {...this.props} title="Size" linkTo="/size"/>
<DomainPanel domain="size">
<DomainNutshell>
diff --git a/server/sonar-web/src/main/js/apps/overview/general/timeline.js b/server/sonar-web/src/main/js/apps/overview/main/timeline.js
index 3938fff0330..3938fff0330 100644
--- a/server/sonar-web/src/main/js/apps/overview/general/timeline.js
+++ b/server/sonar-web/src/main/js/apps/overview/main/timeline.js
diff --git a/server/sonar-web/src/main/js/apps/overview/overview.js b/server/sonar-web/src/main/js/apps/overview/overview.js
index f03573a2677..dae5318a477 100644
--- a/server/sonar-web/src/main/js/apps/overview/overview.js
+++ b/server/sonar-web/src/main/js/apps/overview/overview.js
@@ -1,12 +1,17 @@
import React from 'react';
-import Gate from './gate/gate';
-import GeneralMain from './general/main';
+import Gate from './main/gate/gate';
+import GeneralMain from './main/main';
import Meta from './meta';
+import { SizeMain } from './size/main';
+
import { getMetrics } from '../../api/metrics';
+import { RouterMixin } from '../../components/router/router';
export const Overview = React.createClass({
+ mixins: [RouterMixin],
+
getInitialState () {
return { ready: false };
},
@@ -25,18 +30,34 @@ export const Overview = React.createClass({
</div>;
},
- render () {
- if (!this.state.ready) {
- return this.renderLoading();
- }
-
+ renderMain() {
return <div className="overview">
<div className="overview-main">
<Gate component={this.props.component} gate={this.props.gate}/>
- <GeneralMain {...this.props} {...this.state}/>
+ <GeneralMain {...this.props} {...this.state} navigate={this.navigate}/>
</div>
<Meta component={this.props.component}/>
</div>;
+ },
+
+ renderSize () {
+ return <div className="overview">
+ <SizeMain {...this.props} {...this.state}/>
+ </div>;
+ },
+
+ render () {
+ if (!this.state.ready) {
+ return this.renderLoading();
+ }
+ switch (this.state.route) {
+ case '':
+ return this.renderMain();
+ case '/size':
+ return this.renderSize();
+ default:
+ throw new Error('Unknown route: ' + this.state.route);
+ }
}
});
diff --git a/server/sonar-web/src/main/js/apps/overview/size/comments-details.js b/server/sonar-web/src/main/js/apps/overview/size/comments-details.js
deleted file mode 100644
index 1f2058276c8..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/size/comments-details.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-
-import { DomainMeasuresList } from '../domain/measures-list';
-
-
-const METRICS = [
- 'comment_lines',
- 'comment_lines_density'
-];
-
-
-export class CommentsDetails extends React.Component {
- render () {
- return <div className="overview-domain-section">
- <h2 className="overview-title">Comments</h2>
- <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
- </div>;
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/complexity-details.js b/server/sonar-web/src/main/js/apps/overview/size/complexity-details.js
deleted file mode 100644
index bd1ba93b2ae..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/size/complexity-details.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-
-import { DomainMeasuresList } from '../domain/measures-list';
-
-
-const METRICS = [
- 'complexity',
- 'class_complexity',
- 'file_complexity',
- 'function_complexity'
-];
-
-
-export class ComplexityDetails extends React.Component {
- render () {
- return <div className="overview-domain-section">
- <h2 className="overview-title">Complexity</h2>
- <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
- </div>;
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js b/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js
index 9582e985f3c..2c82df4d6a3 100644
--- a/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js
+++ b/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js
@@ -1,42 +1,15 @@
import React from 'react';
import { BarChart } from '../../../components/charts/bar-chart';
-import { getMeasures } from '../../../api/measures';
import { formatMeasure } from '../../../helpers/measures';
-const HEIGHT = 120;
-const COMPLEXITY_DISTRIBUTION_METRIC = 'file_complexity_distribution';
+const HEIGHT = 80;
export class ComplexityDistribution extends React.Component {
- constructor (props) {
- super(props);
- this.state = { loading: true };
- }
-
- componentDidMount () {
- this.requestData();
- }
-
- requestData () {
- return getMeasures(this.props.component.key, [COMPLEXITY_DISTRIBUTION_METRIC]).then(measures => {
- this.setState({ loading: false, distribution: measures[COMPLEXITY_DISTRIBUTION_METRIC] });
- });
- }
-
- renderLoading () {
- return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
- <i className="spinner"/>
- </div>;
- }
-
renderBarChart () {
- if (this.state.loading) {
- return this.renderLoading();
- }
-
- let data = this.state.distribution.split(';').map((point, index) => {
+ let data = this.props.distribution.split(';').map((point, index) => {
let tokens = point.split('=');
return { x: index, y: parseInt(tokens[1], 10), value: parseInt(tokens[0], 10) };
});
@@ -48,24 +21,14 @@ export class ComplexityDistribution extends React.Component {
return <BarChart data={data}
xTicks={xTicks}
xValues={xValues}
- width={40 * data.length * 2 + 60}
height={HEIGHT}
- barsWidth={40}
- padding={[25, 30, 50, 30]}/>;
+ barsWidth={10}
+ padding={[25, 0, 25, 0]}/>;
}
render () {
return <div className="overview-bar-chart">
- <div className="overview-domain-header">
- <h2 className="overview-title">&nbsp;</h2>
- <ul className="list-inline small">
- <li>X: Complexity/file</li>
- <li>Size: Number of Files</li>
- </ul>
- </div>
- <div>
- {this.renderBarChart()}
- </div>
+ {this.renderBarChart()}
</div>;
}
}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js b/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js
index ab71de614fc..d3bde4d64ae 100644
--- a/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js
+++ b/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js
@@ -1,84 +1,52 @@
import _ from 'underscore';
import React from 'react';
-import { BarChart } from '../../../components/charts/bar-chart';
-import { getMeasures } from '../../../api/measures';
-import { getLanguages } from '../../../api/languages';
+import { Histogram } from '../../../components/charts/histogram';
import { formatMeasure } from '../../../helpers/measures';
-
-
-const HEIGHT = 180;
-const COMPLEXITY_DISTRIBUTION_METRIC = 'ncloc_language_distribution';
+import { getLanguages } from '../../../api/languages';
export class LanguageDistribution extends React.Component {
- constructor (props) {
- super(props);
- this.state = { loading: true };
- }
-
componentDidMount () {
- this.requestData();
+ this.requestLanguages();
}
- requestData () {
- return Promise.all([
- getMeasures(this.props.component.key, [COMPLEXITY_DISTRIBUTION_METRIC]),
- getLanguages()
- ]).then(responses => {
- this.setState({
- loading: false,
- distribution: responses[0][COMPLEXITY_DISTRIBUTION_METRIC],
- languages: responses[1]
- });
- });
+ requestLanguages () {
+ getLanguages().then(languages => this.setState({ languages }));
}
getLanguageName (langKey) {
- let lang = _.findWhere(this.state.languages, { key: langKey });
- return lang ? lang.name : window.t('unknown');
- }
-
- renderLoading () {
- return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
- <i className="spinner"/>
- </div>;
+ if (this.state && this.state.languages) {
+ let lang = _.findWhere(this.state.languages, { key: langKey });
+ return lang ? lang.name : window.t('unknown');
+ } else {
+ return langKey;
+ }
}
renderBarChart () {
- if (this.state.loading) {
- return this.renderLoading();
- }
-
- let data = this.state.distribution.split(';').map((d, index) => {
- let tokens = d.split('=');
- return { x: index, y: parseInt(tokens[1], 10), lang: tokens[0] };
+ let data = this.props.distribution.split(';').map((point, index) => {
+ let tokens = point.split('=');
+ return { x: parseInt(tokens[1], 10), y: index, value: tokens[0] };
});
- let xTicks = data.map(d => this.getLanguageName(d.lang));
+ data = _.sortBy(data, d => -d.x);
+
+ let yTicks = data.map(point => this.getLanguageName(point.value));
- let xValues = data.map(d => formatMeasure(d.y, 'INT'));
+ let yValues = data.map(point => formatMeasure(point.x / this.props.lines * 100, 'PERCENT'));
- return <BarChart data={data}
- xTicks={xTicks}
- xValues={xValues}
- width={40 * data.length * 2 + 60}
- height={HEIGHT}
- barsWidth={40}
- padding={[25, 30, 50, 30]}/>;
+ return <Histogram data={data}
+ yTicks={yTicks}
+ yValues={yValues}
+ height={data.length * 25}
+ barsWidth={10}
+ padding={[0, 50, 0, 80]}/>;
}
render () {
return <div className="overview-bar-chart">
- <div className="overview-domain-header">
- <h2 className="overview-title">&nbsp;</h2>
- <ul className="list-inline small">
- <li>Size: Lines of Code</li>
- </ul>
- </div>
- <div>
- {this.renderBarChart()}
- </div>
+ {this.renderBarChart()}
</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 9e037e136c9..a140c986873 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
@@ -1,52 +1,154 @@
import React from 'react';
-import { SizeTimeline } from './timeline';
-import { SizeDetails } from './size-details';
-import { ComplexityDetails } from './complexity-details';
-import { CommentsDetails } from './comments-details';
-import { ComplexityDistribution } from './complexity-distribution';
+import { DomainLeakTitle } from '../main/components';
import { LanguageDistribution } from './language-distribution';
-import { SizeTreemap } from './treemap';
+import { ComplexityDistribution } from './complexity-distribution';
+import { getMeasuresAndVariations } from '../../../api/measures';
+import { DetailedMeasure } from '../common-components';
+import { DomainTimeline } from '../timeline/domain-timeline';
+import { DomainTreemap } from '../domain/treemap';
+import { getPeriodLabel, getPeriodDate } from './../helpers/period-label';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { filterMetrics, filterMetricsForDomains } from '../helpers/metrics';
-export default class extends React.Component {
- render () {
- return <div className="overview-detailed-page">
- <div className="overview-domain-header">
- <h2 className="overview-title">Size</h2>
- </div>
+export const SizeMain = React.createClass({
+ mixins: [TooltipsMixin],
- <a className="overview-detailed-page-back" href="#">
- <i className="icon-chevron-left"/>
- </a>
+ getInitialState() {
+ return {
+ ready: false,
+ leakPeriodLabel: getPeriodLabel(this.props.component.periods, this.props.leakPeriodIndex),
+ leakPeriodDate: getPeriodDate(this.props.component.periods, this.props.leakPeriodIndex)
+ };
+ },
- <SizeTimeline {...this.props}/>
+ componentDidMount() {
+ this.requestMeasures().then(r => {
+ let measures = this.getMeasuresValues(r, 'value');
+ let leak = this.getMeasuresValues(r, 'var' + this.props.leakPeriodIndex);
+ this.setState({ ready: true, measures, leak });
+ });
+ },
- <div className="flex-columns">
- <div className="flex-column flex-column-third">
- <SizeDetails {...this.props}/>
- </div>
- <div className="flex-column flex-column-two-thirds">
- <LanguageDistribution {...this.props}/>
- </div>
- </div>
+ getMeasuresValues (measures, fieldKey) {
+ let values = {};
+ Object.keys(measures).forEach(measureKey => {
+ values[measureKey] = measures[measureKey][fieldKey];
+ });
+ return values;
+ },
- <div className="flex-columns">
- <div className="flex-column flex-column-third">
- <ComplexityDetails {...this.props}/>
- </div>
- <div className="flex-column flex-column-two-thirds">
- <ComplexityDistribution {...this.props}/>
+ getMetricsForDomain() {
+ return this.props.metrics
+ .filter(metric => ['Size', 'Complexity', 'Documentation'].indexOf(metric.domain) !== -1)
+ .map(metric => metric.key);
+ },
+
+ getMetricsForTimeline() {
+ return filterMetricsForDomains(this.props.metrics, ['Size', 'Complexity', 'Documentation']);
+ },
+
+ getAllMetricsForTimeline() {
+ return filterMetrics(this.props.metrics);
+ },
+
+ requestMeasures () {
+ return getMeasuresAndVariations(this.props.component.key, this.getMetricsForDomain());
+ },
+
+ renderLoading () {
+ return <div className="text-center">
+ <i className="spinner spinner-margin"/>
+ </div>;
+ },
+
+ renderLegend () {
+ if (!this.state.leakPeriodDate) {
+ return null;
+ }
+ return <ul className="overview-legend list-inline">
+ <li><span className="overview-legend-nutshell"/> Nutshell</li>
+ <li><span className="overview-legend-leak"/> <DomainLeakTitle label={this.state.leakPeriodLabel}
+ date={this.state.leakPeriodDate}/></li>
+ </ul>;
+ },
+
+ renderOtherMeasures(domain, hiddenMetrics) {
+ let metrics = filterMetricsForDomains(this.props.metrics, [domain])
+ .filter(metric => hiddenMetrics.indexOf(metric.key) === -1)
+ .map(metric => {
+ return <DetailedMeasure key={metric.key} {...this.props} {...this.state} metric={metric.key}
+ type={metric.type}/>;
+ });
+ return <div>{metrics}</div>;
+ },
+
+ renderOtherSizeMeasures() {
+ return this.renderOtherMeasures('Size', ['ncloc']);
+ },
+
+ renderOtherComplexityMeasures() {
+ return this.renderOtherMeasures('Complexity',
+ ['complexity', 'function_complexity', 'file_complexity', 'class_complexity']);
+ },
+
+ renderOtherDocumentationMeasures() {
+ return this.renderOtherMeasures('Documentation', []);
+ },
+
+ render () {
+ if (!this.state.ready) {
+ return this.renderLoading();
+ }
+ return <div className="overview-detailed-page">
+ <div className="overview-domain">
+ <div className="overview-domain-header">
+ <div className="overview-title">Size Overview</div>
+ {this.renderLegend()}
</div>
- </div>
- <div className="flex-columns">
- <div className="flex-column flex-column-third">
- <CommentsDetails {...this.props}/>
+ <div className="overview-detailed-layout-size">
+ <div className="overview-detailed-layout-column">
+ <div className="overview-detailed-measures-list">
+ <DetailedMeasure {...this.props} {...this.state} metric="ncloc" type="INT">
+ <LanguageDistribution lines={this.state.measures['ncloc']}
+ distribution={this.state.measures['ncloc_language_distribution']}/>
+ </DetailedMeasure>
+ {this.renderOtherSizeMeasures()}
+ </div>
+ </div>
+
+ <div className="overview-detailed-layout-column">
+ <div className="overview-detailed-measures-list">
+ <DetailedMeasure {...this.props} {...this.state} metric="complexity" type="INT"/>
+ <DetailedMeasure {...this.props} {...this.state} metric="function_complexity" type="FLOAT">
+ <ComplexityDistribution distribution={this.state.measures['function_complexity_distribution']}/>
+ </DetailedMeasure>
+ <DetailedMeasure {...this.props} {...this.state} metric="file_complexity" type="FLOAT">
+ <ComplexityDistribution distribution={this.state.measures['file_complexity_distribution']}/>
+ </DetailedMeasure>
+ <DetailedMeasure {...this.props} {...this.state} metric="class_complexity" type="FLOAT"/>
+ {this.renderOtherComplexityMeasures()}
+ </div>
+ </div>
+
+ <div className="overview-detailed-layout-column">
+ <div className="overview-detailed-measures-list">
+ {this.renderOtherDocumentationMeasures()}
+ </div>
+ </div>
</div>
</div>
- <SizeTreemap {...this.props}/>
+ <div className="overview-domain-charts">
+ <DomainTimeline {...this.props} {...this.state}
+ initialMetric="ncloc"
+ metrics={this.getMetricsForTimeline()}
+ allMetrics={this.getAllMetricsForTimeline()}/>
+ <DomainTreemap {...this.props} sizeMetric="ncloc"/>
+ </div>
</div>;
+
}
-}
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/size/size-details.js b/server/sonar-web/src/main/js/apps/overview/size/size-details.js
deleted file mode 100644
index c7362244222..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/size/size-details.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-
-import { DomainMeasuresList } from '../domain/measures-list';
-
-
-const METRICS = [
- 'ncloc',
- 'lines',
- 'files',
- 'directories',
- 'functions',
- 'classes',
- 'accessors'
-];
-
-
-export class SizeDetails extends React.Component {
- render () {
- return <div className="overview-domain-section">
- <h2 className="overview-title">Size</h2>
- <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
- </div>;
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/timeline.js b/server/sonar-web/src/main/js/apps/overview/size/timeline.js
deleted file mode 100644
index 21978726ccf..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/size/timeline.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-
-import { DomainTimeline } from '../domain/timeline';
-import { filterMetricsForDomains } from '../helpers/metrics';
-
-
-const DOMAINS = ['Size', 'Complexity', 'Documentation'];
-
-
-export class SizeTimeline extends React.Component {
- render () {
- return <DomainTimeline {...this.props}
- initialMetric="ncloc"
- metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/treemap.js b/server/sonar-web/src/main/js/apps/overview/size/treemap.js
deleted file mode 100644
index ddfc2671db6..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/size/treemap.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-
-import { DomainTreemap } from '../domain/treemap';
-
-
-export class SizeTreemap extends React.Component {
- render () {
- return <DomainTreemap {...this.props} sizeMetric="ncloc"/>;
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/timeline/domain-timeline.js b/server/sonar-web/src/main/js/apps/overview/timeline/domain-timeline.js
new file mode 100644
index 00000000000..59e5280bd48
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/timeline/domain-timeline.js
@@ -0,0 +1,201 @@
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { getTimeMachineData } from '../../../api/time-machine';
+import { getEvents } from '../../../api/events';
+import { formatMeasure, groupByDomain } from '../../../helpers/measures';
+import { getShortType } from '../helpers/metrics';
+import { Timeline } from './timeline-chart';
+
+
+const HEIGHT = 280;
+
+function parseValue (value, type) {
+ return type === 'RATING' && typeof value === 'string' ? value.charCodeAt(0) - 'A'.charCodeAt(0) + 1 : value;
+}
+
+
+export const DomainTimeline = React.createClass({
+ propTypes: {
+ allMetrics: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ metrics: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ initialMetric: React.PropTypes.string.isRequired
+ },
+
+ getInitialState() {
+ return {
+ loading: true,
+ currentMetric: this.props.initialMetric,
+ comparisonMetric: ''
+ };
+ },
+
+ componentDidMount () {
+ Promise.all([
+ this.requestTimeMachineData(this.state.currentMetric, this.state.comparisonMetric),
+ this.requestEvents()
+ ]).then(responses => {
+ this.setState({
+ loading: false,
+ snapshots: responses[0],
+ events: responses[1]
+ });
+ });
+ },
+
+ requestTimeMachineData (currentMetric, comparisonMetric) {
+ let metricsToRequest = [currentMetric];
+ if (comparisonMetric) {
+ metricsToRequest.push(comparisonMetric);
+ }
+ return getTimeMachineData(this.props.component.key, metricsToRequest.join()).then(r => {
+ return r[0].cells.map(cell => {
+ return { date: moment(cell.d).toDate(), values: cell.v };
+ });
+ });
+ },
+
+ requestEvents () {
+ return getEvents(this.props.component.key, 'Version').then(r => {
+ let events = r.map(event => {
+ return { version: event.n, date: moment(event.dt).toDate() };
+ });
+ return _.sortBy(events, 'date');
+ });
+ },
+
+ handleMetricChange (e) {
+ let newMetric = e.target.value,
+ comparisonMetric = this.state.comparisonMetric;
+ if (newMetric === comparisonMetric) {
+ comparisonMetric = '';
+ }
+ this.requestTimeMachineData(newMetric, comparisonMetric).then(snapshots => {
+ this.setState({ currentMetric: newMetric, comparisonMetric: comparisonMetric, snapshots });
+ });
+ },
+
+ handleComparisonMetricChange (e) {
+ let newMetric = e.target.value;
+ this.requestTimeMachineData(this.state.currentMetric, newMetric).then(snapshots => {
+ this.setState({ comparisonMetric: newMetric, snapshots });
+ });
+ },
+
+ groupMetricsByDomain () {
+ return groupByDomain(this.props.metrics);
+ },
+
+ renderLoading () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ <i className="spinner"/>
+ </div>;
+ },
+
+ renderWhenNoHistoricalData () {
+ return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+ There is no historical data.
+ </div>;
+ },
+
+ renderLineCharts () {
+ if (this.state.loading) {
+ return this.renderLoading();
+ }
+ return <div>
+ {this.renderLineChart(this.state.snapshots, this.state.currentMetric, 0)}
+ {this.renderLineChart(this.state.snapshots, this.state.comparisonMetric, 1)}
+ </div>;
+ },
+
+ renderLineChart (snapshots, metric, index) {
+ if (!metric) {
+ return null;
+ }
+
+ if (snapshots.length < 2) {
+ return this.renderWhenNoHistoricalData();
+ }
+
+ let metricType = _.findWhere(this.props.allMetrics, { key: metric }).type;
+ let data = snapshots.map(snapshot => {
+ return {
+ x: snapshot.date,
+ y: parseValue(snapshot.values[index], metricType)
+ };
+ });
+
+ let formatValue = (value) => formatMeasure(value, metricType);
+ let formatYTick = (tick) => formatMeasure(tick, getShortType(metricType));
+
+ return <div className={'overview-timeline-' + index}>
+ <Timeline key={metric}
+ data={data}
+ events={this.state.events}
+ height={HEIGHT}
+ interpolate="linear"
+ formatValue={formatValue}
+ formatYTick={formatYTick}
+ leakPeriodDate={this.props.leakPeriodDate}
+ padding={[25, 25, 25, 60]}/>
+ </div>;
+ },
+
+ renderMetricOption (metric) {
+ return <option key={metric.key} value={metric.key}>{metric.name}</option>;
+ },
+
+ renderMetricOptions (metrics) {
+ let groupedMetrics = groupByDomain(metrics);
+ return groupedMetrics.map(metricGroup => {
+ let options = metricGroup.metrics.map(this.renderMetricOption);
+ return <optgroup key={metricGroup.domain} label={metricGroup.domain}>{options}</optgroup>;
+ });
+ },
+
+ renderTimelineMetricSelect () {
+ if (this.state.loading) {
+ return null;
+ }
+ return <span>
+ <span className="overview-timeline-sample overview-timeline-sample-0"/>
+ <select ref="metricSelect"
+ className="overview-timeline-select"
+ onChange={this.handleMetricChange}
+ value={this.state.currentMetric}>{this.renderMetricOptions(this.props.metrics)}</select>
+ </span>;
+ },
+
+ renderComparisonMetricSelect () {
+ if (this.state.loading) {
+ return null;
+ }
+ let metrics = this.props.allMetrics.filter(metric => metric.key !== this.state.currentMetric);
+ return <span>
+ {this.state.comparisonMetric ? <span className="overview-timeline-sample overview-timeline-sample-1"/> : null}
+ <select ref="comparisonMetricSelect"
+ className="overview-timeline-select"
+ onChange={this.handleComparisonMetricChange}
+ value={this.state.comparisonMetric}>
+ <option value="">Compare with...</option>
+ {this.renderMetricOptions(metrics)}
+ </select>
+ </span>;
+ },
+
+ render () {
+ return <div className="overview-domain overview-domain-chart">
+ <div className="overview-domain-header">
+ <div>
+ <h2 className="overview-title">Timeline</h2>
+ {this.renderTimelineMetricSelect()}
+ </div>
+ {this.renderComparisonMetricSelect()}
+ </div>
+ <div className="overview-timeline">
+ {this.renderLineCharts()}
+ </div>
+ </div>;
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/timeline/timeline-chart.js b/server/sonar-web/src/main/js/apps/overview/timeline/timeline-chart.js
new file mode 100644
index 00000000000..fc61b1912ff
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/timeline/timeline-chart.js
@@ -0,0 +1,132 @@
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { ResizeMixin } from '../../../components/mixins/resize-mixin';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+
+
+export const Timeline = React.createClass({
+ mixins: [ResizeMixin, TooltipsMixin],
+
+ propTypes: {
+ data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ padding: React.PropTypes.arrayOf(React.PropTypes.number),
+ height: React.PropTypes.number,
+ interpolate: React.PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ padding: [10, 10, 10, 10],
+ interpolate: 'basis'
+ };
+ },
+
+ getInitialState() {
+ return { width: this.props.width, height: this.props.height };
+ },
+
+ renderHorizontalGrid (xScale, yScale) {
+ let ticks = yScale.ticks(4);
+ let grid = ticks.map(tick => {
+ let opts = {
+ x: xScale.range()[0],
+ y: yScale(tick)
+ };
+ return <g key={tick}>
+ <text className="line-chart-tick line-chart-tick-x" dx="-1em" dy="0.3em"
+ textAnchor="end" {...opts}>{this.props.formatYTick(tick)}</text>
+ <line className="line-chart-grid"
+ x1={xScale.range()[0]}
+ x2={xScale.range()[1]}
+ y1={yScale(tick)}
+ y2={yScale(tick)}/>
+ </g>;
+ });
+ return <g>{grid}</g>;
+ },
+
+ renderTicks (xScale, yScale) {
+ let format = xScale.tickFormat(7);
+ let ticks = xScale.ticks(7);
+ ticks = _.initial(ticks).map((tick, index) => {
+ let nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
+ let x = (xScale(tick) + xScale(nextTick)) / 2;
+ let y = yScale.range()[0];
+ return <text key={index} className="line-chart-tick" x={x} y={y} dy="1.5em">{format(tick)}</text>;
+ });
+ return <g>{ticks}</g>;
+ },
+
+ renderLeak (xScale, yScale) {
+ if (!this.props.leakPeriodDate) {
+ return null;
+ }
+ let opts = {
+ x: xScale(this.props.leakPeriodDate),
+ y: yScale.range()[1],
+ width: xScale.range()[1] - xScale(this.props.leakPeriodDate),
+ height: yScale.range()[0] - yScale.range()[1],
+ fill: '#fffae7'
+ };
+ return <rect {...opts}/>;
+ },
+
+ renderLine (xScale, yScale) {
+ let path = d3.svg.line()
+ .x(d => xScale(d.x))
+ .y(d => yScale(d.y))
+ .interpolate(this.props.interpolate);
+ return <path className="line-chart-path" d={path(this.props.data)}/>;
+ },
+
+ renderEvents(xScale, yScale) {
+ let points = this.props.events
+ .map(event => {
+ let snapshot = this.props.data.find(d => d.x.getTime() === event.date.getTime());
+ return _.extend(event, { snapshot });
+ })
+ .filter(event => event.snapshot)
+ .map(event => {
+ let key = `${event.date.getTime()}-${event.snapshot.y}`;
+ let tooltip = [
+ `<span class="nowrap">${event.version}</span>`,
+ `<span class="nowrap">${moment(event.date).format('LL')}</span>`,
+ `<span class="nowrap">${event.snapshot.y ? this.props.formatValue(event.snapshot.y) : '—'}</span>`
+ ].join('<br>');
+ return <circle key={key} className="line-chart-point"
+ r="4" cx={xScale(event.snapshot.x)} cy={yScale(event.snapshot.y)}
+ data-toggle="tooltip" data-title={tooltip}/>;
+ });
+ return <g>{points}</g>;
+ },
+
+ render () {
+ if (!this.state.width || !this.state.height) {
+ return <div/>;
+ }
+
+ 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 xScale = d3.time.scale()
+ .domain(d3.extent(this.props.data, d => d.x))
+ .range([0, availableWidth])
+ .clamp(true);
+ let yScale = d3.scale.linear()
+ .range([availableHeight, 0])
+ .domain([0, d3.max(this.props.data, d => d.y)])
+ .nice();
+
+ 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.renderLeak(xScale, yScale)}
+ {this.renderHorizontalGrid(xScale, yScale)}
+ {this.renderTicks(xScale, yScale)}
+ {this.renderLine(xScale, yScale)}
+ {this.renderEvents(xScale, yScale)}
+ </g>
+ </svg>;
+ }
+});
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 f82a9c7cda2..7ce292921bc 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
@@ -1,7 +1,7 @@
import d3 from 'd3';
import React from 'react';
-import { ResizeMixin } from './mixins/resize-mixin';
+import { ResizeMixin } from './../mixins/resize-mixin';
import { TooltipsMixin } from './../mixins/tooltips-mixin';
export const BarChart = React.createClass({
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 9e3facaa74d..2dd929c7c4c 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
@@ -1,7 +1,7 @@
import d3 from 'd3';
import React from 'react';
-import { ResizeMixin } from './mixins/resize-mixin';
+import { ResizeMixin } from './../mixins/resize-mixin';
import { TooltipsMixin } from './../mixins/tooltips-mixin';
diff --git a/server/sonar-web/src/main/js/components/charts/histogram.js b/server/sonar-web/src/main/js/components/charts/histogram.js
new file mode 100644
index 00000000000..d406d14eedf
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/charts/histogram.js
@@ -0,0 +1,93 @@
+import d3 from 'd3';
+import React from 'react';
+
+import { ResizeMixin } from './../mixins/resize-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
+
+export const Histogram = React.createClass({
+ mixins: [ResizeMixin, TooltipsMixin],
+
+ propTypes: {
+ data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ yTicks: React.PropTypes.arrayOf(React.PropTypes.any),
+ yValues: React.PropTypes.arrayOf(React.PropTypes.any),
+ width: React.PropTypes.number,
+ height: React.PropTypes.number,
+ padding: React.PropTypes.arrayOf(React.PropTypes.number),
+ barsHeight: React.PropTypes.number
+ },
+
+ getDefaultProps() {
+ return {
+ xTicks: [],
+ xValues: [],
+ padding: [10, 10, 10, 10],
+ barsHeight: 10
+ };
+ },
+
+ getInitialState () {
+ return { width: this.props.width, height: this.props.height };
+ },
+
+ renderTicks (xScale, yScale) {
+ if (!this.props.yTicks.length) {
+ return null;
+ }
+ let ticks = this.props.yTicks.map((tick, index) => {
+ let point = this.props.data[index];
+ let x = xScale.range()[0];
+ let y = Math.round(yScale(point.y) + yScale.rangeBand() / 2 + this.props.barsHeight / 2);
+ return <text key={index} className="bar-chart-tick histogram-tick" x={x} y={y} dx="-1em" dy="0.3em">{tick}</text>;
+ });
+ return <g>{ticks}</g>;
+ },
+
+ renderValues (xScale, yScale) {
+ if (!this.props.yValues.length) {
+ return null;
+ }
+ let ticks = this.props.yValues.map((value, index) => {
+ let point = this.props.data[index];
+ let x = xScale(point.x);
+ let y = Math.round(yScale(point.y) + yScale.rangeBand() / 2 + this.props.barsHeight / 2);
+ return <text key={index} className="bar-chart-tick histogram-value" x={x} y={y} dx="1em" dy="0.3em">{value}</text>;
+ });
+ return <g>{ticks}</g>;
+ },
+
+ renderBars (xScale, yScale) {
+ let bars = this.props.data.map((d, index) => {
+ let x = Math.round(xScale(d.x)) + /* minimum bar width */ 1;
+ let y = Math.round(yScale(d.y) + yScale.rangeBand() / 2);
+ return <rect key={index} className="bar-chart-bar"
+ x={0} y={y} width={x} height={this.props.barsHeight}/>;
+ });
+ return <g>{bars}</g>;
+ },
+
+ render () {
+ if (!this.state.width || !this.state.height) {
+ return <div/>;
+ }
+
+ 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 maxX = d3.max(this.props.data, d => d.x);
+ let xScale = d3.scale.linear()
+ .domain([0, maxX])
+ .range([0, availableWidth]);
+ let yScale = d3.scale.ordinal()
+ .domain(this.props.data.map(d => d.y))
+ .rangeRoundBands([0, availableHeight]);
+
+ return <svg className="bar-chart" width={this.state.width} height={this.state.height}>
+ <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+ {this.renderTicks(xScale, yScale)}
+ {this.renderValues(xScale, yScale)}
+ {this.renderBars(xScale, yScale)}
+ </g>
+ </svg>;
+ }
+});
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 eeaed5e88a9..375931ea9f5 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
@@ -1,7 +1,7 @@
import d3 from 'd3';
import React from 'react';
-import { ResizeMixin } from './mixins/resize-mixin';
+import { ResizeMixin } from './../mixins/resize-mixin';
import { TooltipsMixin } from './../mixins/tooltips-mixin';
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 24bcff49ce2..de56351ff7e 100644
--- a/server/sonar-web/src/main/js/components/charts/treemap.js
+++ b/server/sonar-web/src/main/js/components/charts/treemap.js
@@ -2,7 +2,7 @@ import _ from 'underscore';
import d3 from 'd3';
import React from 'react';
-import { ResizeMixin } from './mixins/resize-mixin';
+import { ResizeMixin } from './../mixins/resize-mixin';
import { TooltipsMixin } from './../mixins/tooltips-mixin';
@@ -81,14 +81,11 @@ export const Treemap = React.createClass({
return <div>&nbsp;</div>;
}
- let sizeScale = d3.scale.linear()
- .domain([0, d3.max(this.props.items, d => d.size)])
- .range([5, 45]);
let treemap = d3.layout.treemap()
.round(true)
- .value(d => sizeScale(d.size))
+ .value(d => d.size)
.sort((a, b) => a.value - b.value)
- .size([this.state.width, 360]);
+ .size([this.state.width, this.state.height]);
let nodes = treemap
.nodes({ children: this.props.items })
.filter(d => !d.children)
diff --git a/server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js b/server/sonar-web/src/main/js/components/mixins/resize-mixin.js
index ebd7360fe07..ebd7360fe07 100644
--- a/server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js
+++ b/server/sonar-web/src/main/js/components/mixins/resize-mixin.js
diff --git a/server/sonar-web/src/main/js/components/router/router.js b/server/sonar-web/src/main/js/components/router/router.js
new file mode 100644
index 00000000000..7489700841e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/router/router.js
@@ -0,0 +1,48 @@
+let listener;
+
+
+export const RouterMixin = {
+ getDefaultProps() {
+ return { urlRoot: '/' };
+ },
+
+ getInitialState() {
+ return { route: this.getRoute() };
+ },
+
+ getRoute() {
+ let path = window.location.pathname;
+ if (path.indexOf(this.props.urlRoot) === 0) {
+ return path.substr(this.props.urlRoot.length);
+ } else {
+ return null;
+ }
+ },
+
+ componentDidMount () {
+ listener = this;
+ window.addEventListener('popstate', this.handleRouteChange);
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener('popstate', this.handleRouteChange);
+ },
+
+ handleRouteChange() {
+ let route = this.getRoute();
+ this.setState({ route });
+ },
+
+ navigate (route) {
+ let url = this.props.urlRoot + route + window.location.search + window.location.hash;
+ window.history.pushState({ route }, document.title, url);
+ this.setState({ route });
+ }
+};
+
+
+export function navigate (route) {
+ if (listener) {
+ listener.navigate(route);
+ }
+}
diff --git a/server/sonar-web/src/main/js/helpers/measures.js b/server/sonar-web/src/main/js/helpers/measures.js
index 0e9433295f6..23a27e9270e 100644
--- a/server/sonar-web/src/main/js/helpers/measures.js
+++ b/server/sonar-web/src/main/js/helpers/measures.js
@@ -1,4 +1,5 @@
import numeral from 'numeral';
+import _ from 'underscore';
/**
@@ -23,6 +24,33 @@ export function formatMeasureVariation (value, type) {
}
+/**
+ * Return a localized metric name
+ * @param {string} metricKey
+ * @returns {string}
+ */
+export function localizeMetric (metricKey) {
+ return window.t('metric', metricKey, 'name');
+}
+
+
+/**
+ * Group list of metrics by their domain
+ * @param {Array} metrics
+ * @returns {Array}
+ */
+export function groupByDomain (metrics) {
+ let groupedMetrics = _.groupBy(metrics, 'domain');
+ let domains = _.map(groupedMetrics, (metricList, domain) => {
+ return {
+ domain: domain,
+ metrics: _.sortBy(metricList, 'name')
+ };
+ });
+ return _.sortBy(domains, 'domain');
+}
+
+
/*
* Helpers
*/
diff --git a/server/sonar-web/src/main/less/init/icons.less b/server/sonar-web/src/main/less/init/icons.less
index 3b2fff04283..5ec80ef3073 100644
--- a/server/sonar-web/src/main/less/init/icons.less
+++ b/server/sonar-web/src/main/less/init/icons.less
@@ -389,7 +389,7 @@ a[class^="icon-"], a[class*=" icon-"] {
}
.icon-plus:before {
content: "\f067";
- font-size: @iconFontSize;
+ font-size: @iconSmallFontSize;
}
.icon-link:before {
content: "\f127";
diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less
index 2e519588943..a2142d9be4d 100644
--- a/server/sonar-web/src/main/less/pages/overview.less
+++ b/server/sonar-web/src/main/less/pages/overview.less
@@ -11,7 +11,6 @@
justify-content: space-between;
width: 100%;
min-height: ~"calc(100vh - @{navbarGlobalHeight} - @{navbarContextHeight} - @{pageFooterHeight})";
- overflow-x: hidden;
animation: fadeIn 0.5s forwards;
}
@@ -136,6 +135,7 @@
align-items: baseline;
justify-content: space-between;
margin-bottom: 10px;
+ line-height: @formControlHeight;
.overview-title {
flex: 1;
@@ -225,7 +225,220 @@
}
}
+/*
+ * Detailed Pages
+ */
+
+.overview-detailed-page {
+ flex: 1;
+}
+
+.overview-detailed-measures-list {
+ border: 1px solid @barBorderColor;
+ background-color: #fff;
+ overflow: hidden;
+}
+
+.overview-detailed-measure {
+ display: flex;
+ background-color: #fff;
+}
+
+.overview-detailed-measure-nutshell,
+.overview-detailed-measure-leak {
+ position: relative;
+ padding: 7px 10px;
+}
+
+.overview-detailed-measure-nutshell {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: baseline;
+ flex: 3;
+}
+
+.overview-detailed-measure-leak {
+ flex: 1;
+ background-color: #fffae7;
+ text-align: center;
+}
+
+.overview-detailed-measure-value {
+ font-size: 16px;
+}
+
+.overview-detailed-layout-size {
+ display: flex;
+ justify-content: space-between;
+ margin: 0 -10px;
+
+ .overview-detailed-layout-column {
+ flex: 1;
+ max-width: 560px;
+ }
+}
+
+.overview-detailed-layout-column {
+ padding: 0 10px;
+}
+
+.overview-legend-nutshell,
+.overview-legend-leak {
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ height: 16px;
+ margin-top: -2px;
+ margin-left: 16px;
+ margin-right: 8px;
+ border: 1px solid @barBorderColor;
+ box-sizing: border-box;
+}
+
+.overview-legend-nutshell {
+ background-color: #fff;
+}
+
+.overview-legend-leak {
+ background-color: #fffae7;
+}
+
+/*
+ * Charts
+ */
+
+.overview-domain-charts {
+ display: flex;
+
+ .overview-domain-chart {
+ flex: 1;
+ }
+}
+
+.overview-domain-chart {
+ .overview-title {
+ display: inline-block;
+ margin-right: 20px;
+ }
+}
+
+.overview-bar-chart {
+ width: 100%;
+ padding-top: 10px;
+ padding-bottom: 15px;
+}
+
+.bar-chart-bar {
+ fill: @blue;
+}
+
+.bar-chart-tick {
+ fill: @secondFontColor;
+ font-size: 11px;
+ text-anchor: middle;
+}
+
+.histogram-tick {
+ text-anchor: end;
+}
+
+.histogram-value {
+ text-anchor: start;
+}
+
+.overview-timeline {
+ padding: 10px;
+ border: 1px solid @barBorderColor;
+ box-sizing: border-box;
+ background-color: #fff;
+
+ .line-chart-path {
+ fill: none;
+ stroke: @blue;
+ stroke-width: 2px;
+ }
+
+ .line-chart-point {
+ fill: #fff;
+ stroke: @darkBlue;
+ stroke-width: 2px;
+ }
+
+ .line-chart-backdrop {
+ fill: #fffae7;
+ }
+
+ .line-chart-tick {
+ fill: @secondFontColor;
+ font-size: 11px;
+ text-anchor: middle;
+ }
+
+ .line-chart-tick-x {
+ text-anchor: end;
+ }
+
+ .line-chart-tick-x-right {
+ text-anchor: start;
+ }
+ .line-chart-grid {
+ shape-rendering: crispedges;
+ stroke: #eee;
+ }
+}
+
+.overview-timeline-1 {
+ .line-chart-path {
+ stroke: @purple;
+ }
+
+ .line-chart-point {
+ stroke: darken(@purple, 20%);
+ }
+}
+
+.overview-timeline-sample {
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ height: 2px;
+ margin-right: 8px;
+}
+
+.overview-timeline-sample-0 {
+ background-color: @blue;
+}
+
+.overview-timeline-sample-1 {
+ background-color: @purple;
+}
+
+.overview-timeline-chart {
+ text-align: center;
+}
+
+.overview-timeline-chart + .overview-timeline-chart {
+ margin-top: 40px;
+}
+
+.overview-timeline-select {
+ width: 12em;
+ height: @formControlHeight;
+ line-height: @formControlHeight;
+ border: 1px solid #cdcdcd;
+ background: none;
+}
+
+.overview-treemap {
+}
+
+.overview-chart-placeholder {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
/*
* Responsive Stuff
@@ -246,13 +459,15 @@
}
}
-
-
/*
* Animations
*/
@keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
}
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb b/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb
index 098d8cccf9a..36e2931ee2f 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb
@@ -33,6 +33,7 @@ ActionController::Routing::Routes.draw do |map|
map.connect 'api_documentation/*other', :controller => 'api_documentation', :action => 'index'
map.connect 'quality_gates/*other', :controller => 'quality_gates', :action => 'index'
+ map.connect 'overview/*other', :controller => 'overview', :action => 'index'
# Install the default route as the lowest priority.
map.connect ':controller/:action/:id', :requirements => { :id => /.*/ }
diff --git a/server/sonar-web/tests/apps/overview-test.js b/server/sonar-web/tests/apps/overview-test.js
index 2ba57a13ff5..2bfea7f419a 100644
--- a/server/sonar-web/tests/apps/overview-test.js
+++ b/server/sonar-web/tests/apps/overview-test.js
@@ -3,9 +3,9 @@ import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
import { expect } from 'chai';
-import Gate from '../../src/main/js/apps/overview/gate/gate';
-import GateConditions from '../../src/main/js/apps/overview/gate/gate-conditions';
-import GateCondition from '../../src/main/js/apps/overview/gate/gate-condition';
+import Gate from '../../src/main/js/apps/overview/main/gate/gate';
+import GateConditions from '../../src/main/js/apps/overview/main/gate/gate-conditions';
+import GateCondition from '../../src/main/js/apps/overview/main/gate/gate-condition';
describe('Overview', function () {