aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-10-26 16:23:18 +0100
committerStas Vilchik <vilchiks@gmail.com>2015-10-26 16:23:18 +0100
commitd6652afabd60eee53364a30b8900d2885f43fcf3 (patch)
tree22ebdb4c465ae9213ab19d716d413e10ffd4d0aa
parent3c38542ce87deca7dab67fbe7cc87a3b4692beb7 (diff)
downloadsonarqube-d6652afabd60eee53364a30b8900d2885f43fcf3.tar.gz
sonarqube-d6652afabd60eee53364a30b8900d2885f43fcf3.zip
SONAR-6357 improve display of chart on the project overview page
-rw-r--r--server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js29
-rw-r--r--server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js67
-rw-r--r--server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/domain/measures-list.js19
-rw-r--r--server/sonar-web/src/main/js/apps/overview/domain/timeline.js10
-rw-r--r--server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js50
-rw-r--r--server/sonar-web/src/main/js/components/charts/bar-chart.js69
-rw-r--r--server/sonar-web/src/main/js/components/charts/bubble-chart.js155
-rw-r--r--server/sonar-web/src/main/js/components/charts/line-chart.js114
-rw-r--r--server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js27
-rw-r--r--server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js17
-rw-r--r--server/sonar-web/src/main/js/components/charts/timeline.js85
-rw-r--r--server/sonar-web/src/main/js/components/charts/treemap.js90
-rw-r--r--server/sonar-web/src/main/js/components/charts/word-cloud.js50
14 files changed, 305 insertions, 479 deletions
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js b/server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js
index 403d8b69120..a6510d60faf 100644
--- a/server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js
+++ b/server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js
@@ -1,5 +1,7 @@
import React from 'react';
+
import { getMeasures } from '../../../api/measures';
+import DrilldownLink from '../helpers/drilldown-link';
const METRICS = [
@@ -36,25 +38,35 @@ export class CoverageDetails extends React.Component {
});
}
- renderCoverage (coverage, lineCoverage, branchCoverage) {
+ renderValue (value, metricKey) {
+ if (value != null) {
+ return <DrilldownLink component={this.props.component.key} metric={metricKey}>
+ {window.formatMeasure(value, 'PERCENT')}
+ </DrilldownLink>;
+ } else {
+ return '—';
+ }
+ }
+
+ renderCoverage (coverage, lineCoverage, branchCoverage, prefix) {
return <table className="data zebra">
<tbody>
<tr>
<td>Coverage</td>
<td className="thin nowrap text-right">
- {formatCoverage(coverage)}
+ {this.renderValue(coverage, prefix + 'coverage')}
</td>
</tr>
<tr>
<td>Line Coverage</td>
<td className="thin nowrap text-right">
- {formatCoverage(lineCoverage)}
+ {this.renderValue(lineCoverage, prefix + 'line_coverage')}
</td>
</tr>
<tr>
<td>Branch Coverage</td>
<td className="thin nowrap text-right">
- {formatCoverage(branchCoverage)}
+ {this.renderValue(branchCoverage, prefix + 'branch_coverage')}
</td>
</tr>
</tbody>
@@ -70,7 +82,8 @@ export class CoverageDetails extends React.Component {
{this.renderCoverage(
this.state.measures['coverage'],
this.state.measures['line_coverage'],
- this.state.measures['branch_coverage'])}
+ this.state.measures['branch_coverage'],
+ '')}
</div>;
}
@@ -83,7 +96,8 @@ export class CoverageDetails extends React.Component {
{this.renderCoverage(
this.state.measures['it_coverage'],
this.state.measures['it_line_coverage'],
- this.state.measures['it_branch_coverage'])}
+ this.state.measures['it_branch_coverage'],
+ 'it_')}
</div>;
}
@@ -98,7 +112,8 @@ export class CoverageDetails extends React.Component {
{this.renderCoverage(
this.state.measures['overall_coverage'],
this.state.measures['overall_line_coverage'],
- this.state.measures['overall_branch_coverage'])}
+ this.state.measures['overall_branch_coverage'],
+ 'overall_')}
</div>;
}
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js b/server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js
index 65034729d79..0bc37699561 100644
--- a/server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js
+++ b/server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js
@@ -1,6 +1,6 @@
import React from 'react';
-import { getMeasures } from '../../../api/measures';
-import { formatMeasure } from '../formatting';
+
+import { DomainMeasuresList } from '../domain/measures-list';
const METRICS = [
@@ -13,70 +13,11 @@ const METRICS = [
];
-function format (value, metric) {
- return value != null ? formatMeasure(value, metric) : '—';
-}
-
-
export class TestsDetails extends React.Component {
- constructor () {
- super();
- this.state = { measures: {} };
- }
-
- componentDidMount () {
- this.requestDetails();
- }
-
- requestDetails () {
- return getMeasures(this.props.component.key, METRICS).then(measures => {
- this.setState({ measures });
- });
- }
-
render () {
return <div className="overview-domain-section">
- <h2 className="overview-title">Tests Details</h2>
- <table className="data zebra">
- <tbody>
- <tr>
- <td>Tests</td>
- <td className="thin nowrap text-right">
- {format(this.state.measures['tests'], 'tests')}
- </td>
- </tr>
- <tr>
- <td>Skipped Tests</td>
- <td className="thin nowrap text-right">
- {format(this.state.measures['skipped_tests'], 'skipped_tests')}
- </td>
- </tr>
- <tr>
- <td>Test Errors</td>
- <td className="thin nowrap text-right">
- {format(this.state.measures['test_errors'], 'test_errors')}
- </td>
- </tr>
- <tr>
- <td>Test Failures</td>
- <td className="thin nowrap text-right">
- {format(this.state.measures['test_failures'], 'test_failures')}
- </td>
- </tr>
- <tr>
- <td>Tests Execution Time</td>
- <td className="thin nowrap text-right">
- {format(this.state.measures['test_execution_time'], 'test_execution_time')}
- </td>
- </tr>
- <tr>
- <td>Tests Success</td>
- <td className="thin nowrap text-right">
- {format(this.state.measures['test_success_density'], 'test_success_density')}
- </td>
- </tr>
- </tbody>
- </table>
+ <h2 className="overview-title">Tests</h2>
+ <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
</div>;
}
}
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js
index 730795e11ac..6adef652fe6 100644
--- a/server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js
+++ b/server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js
@@ -90,11 +90,9 @@ export class DomainBubbleChart extends React.Component {
tooltip: this.getTooltip(component)
};
});
- let xGrid = this.state.files.map(component => getMeasure(component, this.props.xMetric));
let formatXTick = (tick) => window.formatMeasure(tick, this.state.xMetric.type);
let formatYTick = (tick) => window.formatMeasure(tick, this.state.yMetric.type);
return <BubbleChart items={items}
- xGrid={xGrid}
height={HEIGHT}
padding={[25, 30, 50, 60]}
formatXTick={formatXTick}
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js b/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js
index 16feb0425ae..32c10b9e43e 100644
--- a/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js
+++ b/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js
@@ -5,11 +5,6 @@ import DrilldownLink from '../helpers/drilldown-link';
import { getMeasures } from '../../../api/measures';
-function format (value, type) {
- return value != null ? window.formatMeasure(value, type) : '—';
-}
-
-
export class DomainMeasuresList extends React.Component {
constructor () {
super();
@@ -30,15 +25,23 @@ export class DomainMeasuresList extends React.Component {
return _.findWhere(this.props.metrics, { key: metricKey });
}
+ renderValue (value, metricKey, metricType) {
+ if (value != null) {
+ return <DrilldownLink component={this.props.component.key} metric={metricKey}>
+ {window.formatMeasure(value, metricType)}
+ </DrilldownLink>;
+ } else {
+ return '—';
+ }
+ }
+
render () {
let rows = this.props.metricsToDisplay.map(metric => {
let metricObject = this.getMetricObject(metric);
return <tr key={metric}>
<td>{metricObject.name}</td>
<td className="thin nowrap text-right">
- <DrilldownLink component={this.props.component.key} metric={metric}>
- {format(this.state.measures[metric], metricObject.type)}
- </DrilldownLink>
+ {this.renderValue(this.state.measures[metric], metric, metricObject.type)}
</td>
</tr>;
});
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 ff671f1fc71..7fdee332a63 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
@@ -79,11 +79,21 @@ export class DomainTimeline extends React.Component {
</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();
}
+ if (!this.state.events.length || !this.state.snapshots.length) {
+ return this.renderWhenNoHistoricalData();
+ }
+
let events = this.prepareEvents();
let currentMetricType = _.findWhere(this.props.metrics, { key: this.state.currentMetric }).type;
diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js b/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js
index dace4233130..b1b9ec79138 100644
--- a/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js
+++ b/server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js
@@ -1,6 +1,6 @@
import React from 'react';
-import { getMeasures } from '../../../api/measures';
-import { formatMeasure } from '../formatting';
+
+import { DomainMeasuresList } from '../domain/measures-list';
const METRICS = [
@@ -12,52 +12,10 @@ const METRICS = [
export class DuplicationsDetails extends React.Component {
- constructor () {
- super();
- this.state = { measures: {} };
- }
-
- componentDidMount () {
- this.requestDetails();
- }
-
- requestDetails () {
- return getMeasures(this.props.component.key, METRICS).then(measures => {
- this.setState({ measures });
- });
- }
-
render () {
return <div className="overview-domain-section">
- <h2 className="overview-title">Details</h2>
- <table className="data zebra">
- <tbody>
- <tr>
- <td>Duplications</td>
- <td className="thin nowrap text-right">
- {formatMeasure(this.state.measures['duplicated_lines_density'], 'duplicated_lines_density')}
- </td>
- </tr>
- <tr>
- <td>Blocks</td>
- <td className="thin nowrap text-right">
- {formatMeasure(this.state.measures['duplicated_blocks'], 'duplicated_blocks')}
- </td>
- </tr>
- <tr>
- <td>Files</td>
- <td className="thin nowrap text-right">
- {formatMeasure(this.state.measures['duplicated_files'], 'duplicated_files')}
- </td>
- </tr>
- <tr>
- <td>Lines</td>
- <td className="thin nowrap text-right">
- {formatMeasure(this.state.measures['duplicated_lines'], 'duplicated_lines')}
- </td>
- </tr>
- </tbody>
- </table>
+ <h2 className="overview-title">Duplications</h2>
+ <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
</div>;
}
}
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 347d45d9673..c7444fedcb9 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,31 +1,33 @@
import d3 from 'd3';
import React from 'react';
-export class BarChart extends React.Component {
- constructor (props) {
- super();
- this.state = { width: props.width, height: props.height };
- }
+import { ResizeMixin } from './mixins/resize-mixin';
+import { TooltipsMixin } from './mixins/tooltips-mixin';
- componentDidMount () {
- if (!this.props.width || !this.props.height) {
- this.handleResize();
- window.addEventListener('resize', this.handleResize.bind(this));
- }
- }
+export const BarChart = React.createClass({
+ mixins: [ResizeMixin, TooltipsMixin],
- componentWillUnmount () {
- if (!this.props.width || !this.props.height) {
- window.removeEventListener('resize', this.handleResize.bind(this));
- }
- }
+ propTypes: {
+ data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
+ xValues: React.PropTypes.arrayOf(React.PropTypes.any),
+ height: React.PropTypes.number,
+ padding: React.PropTypes.arrayOf(React.PropTypes.number),
+ barsWidth: React.PropTypes.number
+ },
- handleResize () {
- let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
- let newWidth = this.props.width || boundingClientRect.width;
- let newHeight = this.props.height || boundingClientRect.height;
- this.setState({ width: newWidth, height: newHeight });
- }
+ getDefaultProps() {
+ return {
+ xTicks: [],
+ xValues: [],
+ padding: [10, 10, 10, 10],
+ barsWidth: 40
+ };
+ },
+
+ getInitialState () {
+ return { width: this.props.width, height: this.props.height };
+ },
renderXTicks (xScale, yScale) {
if (!this.props.xTicks.length) {
@@ -38,7 +40,7 @@ export class BarChart extends React.Component {
return <text key={index} className="bar-chart-tick" x={x} y={y} dy="1.5em">{tick}</text>;
});
return <g>{ticks}</g>;
- }
+ },
renderXValues (xScale, yScale) {
if (!this.props.xValues.length) {
@@ -51,7 +53,7 @@ export class BarChart extends React.Component {
return <text key={index} className="bar-chart-tick" x={x} y={y} dy="-1em">{value}</text>;
});
return <g>{ticks}</g>;
- }
+ },
renderBars (xScale, yScale) {
let bars = this.props.data.map((d, index) => {
@@ -63,7 +65,7 @@ export class BarChart extends React.Component {
x={x} y={y} width={this.props.barsWidth} height={height}/>;
});
return <g>{bars}</g>;
- }
+ },
render () {
if (!this.state.width || !this.state.height) {
@@ -89,19 +91,4 @@ export class BarChart extends React.Component {
</g>
</svg>;
}
-}
-
-BarChart.defaultProps = {
- xTicks: [],
- xValues: [],
- padding: [10, 10, 10, 10],
- barsWidth: 40
-};
-
-BarChart.propTypes = {
- data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
- xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
- xValues: React.PropTypes.arrayOf(React.PropTypes.any),
- padding: React.PropTypes.arrayOf(React.PropTypes.number),
- barsWidth: React.PropTypes.number
-};
+});
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 82edf22ce4e..87f7ef84f5a 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,13 +1,24 @@
-import $ from 'jquery';
import d3 from 'd3';
import React from 'react';
-export class Bubble extends React.Component {
+import { ResizeMixin } from './mixins/resize-mixin';
+import { TooltipsMixin } from './mixins/tooltips-mixin';
+
+
+export const Bubble = React.createClass({
+ propTypes: {
+ x: React.PropTypes.number.isRequired,
+ y: React.PropTypes.number.isRequired,
+ r: React.PropTypes.number.isRequired,
+ tooltip: React.PropTypes.string,
+ link: React.PropTypes.string
+ },
+
handleClick () {
if (this.props.link) {
window.location = this.props.link;
}
- }
+ },
render () {
let tooltipAttrs = {};
@@ -17,48 +28,45 @@ export class Bubble extends React.Component {
'title': this.props.tooltip
};
}
- return <circle onClick={this.handleClick.bind(this)} className="bubble-chart-bubble"
+ return <circle onClick={this.handleClick} className="bubble-chart-bubble"
r={this.props.r} {...tooltipAttrs}
transform={`translate(${this.props.x}, ${this.props.y})`}/>;
}
-}
-
-
-export class BubbleChart extends React.Component {
- constructor (props) {
- super();
- this.state = { width: props.width, height: props.height };
- }
-
- componentDidMount () {
- if (!this.props.width || !this.props.height) {
- this.handleResize();
- window.addEventListener('resize', this.handleResize.bind(this));
- }
- this.initTooltips();
- }
-
- componentDidUpdate () {
- this.initTooltips();
- }
-
- componentWillUnmount () {
- if (!this.props.width || !this.props.height) {
- window.removeEventListener('resize', this.handleResize.bind(this));
- }
- }
-
- handleResize () {
- let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
- let newWidth = this.props.width || boundingClientRect.width;
- let newHeight = this.props.height || boundingClientRect.height;
- this.setState({ width: newWidth, height: newHeight });
- }
-
- initTooltips () {
- $('[data-toggle="tooltip"]', React.findDOMNode(this))
- .tooltip({ container: 'body', placement: 'bottom', html: true });
- }
+});
+
+
+export const BubbleChart = React.createClass({
+ mixins: [ResizeMixin, TooltipsMixin],
+
+ propTypes: {
+ items: React.PropTypes.arrayOf(React.PropTypes.object),
+ sizeRange: React.PropTypes.arrayOf(React.PropTypes.number),
+ displayXGrid: React.PropTypes.bool,
+ displayXTicks: React.PropTypes.bool,
+ displayYGrid: React.PropTypes.bool,
+ displayYTicks: React.PropTypes.bool,
+ height: React.PropTypes.number,
+ padding: React.PropTypes.arrayOf(React.PropTypes.number),
+ formatXTick: React.PropTypes.func,
+ formatYTick: React.PropTypes.func
+ },
+
+ getDefaultProps() {
+ return {
+ sizeRange: [5, 45],
+ displayXGrid: true,
+ displayYGrid: true,
+ displayXTicks: true,
+ displayYTicks: true,
+ padding: [10, 10, 10, 10],
+ formatXTick: d => d,
+ formatYTick: d => d
+ };
+ },
+
+ getInitialState() {
+ return { width: this.props.width, height: this.props.height };
+ },
getXRange (xScale, sizeScale, availableWidth) {
var minX = d3.min(this.props.items, d => xScale(d.x) - sizeScale(d.size)),
@@ -66,7 +74,7 @@ export class BubbleChart extends React.Component {
dMinX = minX < 0 ? xScale.range()[0] - minX : xScale.range()[0],
dMaxX = maxX > xScale.range()[1] ? maxX - xScale.range()[1] : 0;
return [dMinX, availableWidth - dMaxX];
- }
+ },
getYRange (yScale, sizeScale, availableHeight) {
var minY = d3.min(this.props.items, d => yScale(d.y) - sizeScale(d.size)),
@@ -74,7 +82,7 @@ export class BubbleChart extends React.Component {
dMinY = minY < 0 ? yScale.range()[1] - minY : yScale.range()[1],
dMaxY = maxY > yScale.range()[0] ? maxY - yScale.range()[0] : 0;
return [availableHeight - dMaxY, dMinY];
- }
+ },
renderXGrid (xScale, yScale) {
if (!this.props.displayXGrid) {
@@ -92,7 +100,7 @@ export class BubbleChart extends React.Component {
});
return <g ref="xGrid">{lines}</g>;
- }
+ },
renderYGrid (xScale, yScale) {
if (!this.props.displayYGrid) {
@@ -110,7 +118,7 @@ export class BubbleChart extends React.Component {
});
return <g ref="yGrid">{lines}</g>;
- }
+ },
renderXTicks (xScale, yScale) {
if (!this.props.displayXTicks) {
@@ -127,7 +135,7 @@ export class BubbleChart extends React.Component {
});
return <g>{ticks}</g>;
- }
+ },
renderYTicks (xScale, yScale) {
if (!this.props.displayYTicks) {
@@ -145,7 +153,7 @@ export class BubbleChart extends React.Component {
});
return <g>{ticks}</g>;
- }
+ },
render () {
if (!this.state.width || !this.state.height) {
@@ -156,27 +164,27 @@ export class BubbleChart extends React.Component {
let availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2];
let xScale = d3.scale.linear()
- .domain([0, d3.max(this.props.items, d => d.x)])
- .range([0, availableWidth])
- .nice();
+ .domain([0, d3.max(this.props.items, d => d.x)])
+ .range([0, availableWidth])
+ .nice();
let yScale = d3.scale.linear()
- .domain([0, d3.max(this.props.items, d => d.y)])
- .range([availableHeight, 0])
- .nice();
+ .domain([0, d3.max(this.props.items, d => d.y)])
+ .range([availableHeight, 0])
+ .nice();
let sizeScale = d3.scale.linear()
- .domain([0, d3.max(this.props.items, d => d.size)])
- .range(this.props.sizeRange);
+ .domain([0, d3.max(this.props.items, d => d.size)])
+ .range(this.props.sizeRange);
xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
let bubbles = this.props.items
- .map((item, index) => {
- return <Bubble key={index}
- tooltip={item.tooltip}
- link={item.link}
- x={xScale(item.x)} y={yScale(item.y)} r={sizeScale(item.size)}/>;
- });
+ .map((item, index) => {
+ return <Bubble key={index}
+ tooltip={item.tooltip}
+ link={item.link}
+ x={xScale(item.x)} y={yScale(item.y)} r={sizeScale(item.size)}/>;
+ });
return <svg className="bubble-chart" width={this.state.width} height={this.state.height}>
<g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
@@ -188,25 +196,4 @@ export class BubbleChart extends React.Component {
</g>
</svg>;
}
-}
-
-BubbleChart.defaultProps = {
- sizeRange: [5, 45],
- displayXGrid: true,
- displayYGrid: true,
- displayXTicks: true,
- displayYTicks: true,
- tooltips: [],
- padding: [10, 10, 10, 10],
- formatXTick: d => d,
- formatYTick: d => d
-};
-
-BubbleChart.propTypes = {
- sizeRange: React.PropTypes.arrayOf(React.PropTypes.number),
- displayXGrid: React.PropTypes.bool,
- displayYGrid: React.PropTypes.bool,
- padding: React.PropTypes.arrayOf(React.PropTypes.number),
- formatXTick: React.PropTypes.func,
- formatYTick: React.PropTypes.func
-};
+});
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 bbb14f3a69d..a92ea08cab4 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,31 +1,41 @@
import d3 from 'd3';
import React from 'react';
-export class LineChart extends React.Component {
- constructor (props) {
- super();
- this.state = { width: props.width, height: props.height };
- }
-
- componentDidMount () {
- if (!this.props.width || !this.props.height) {
- this.handleResize();
- window.addEventListener('resize', this.handleResize.bind(this));
- }
- }
-
- componentWillUnmount () {
- if (!this.props.width || !this.props.height) {
- window.removeEventListener('resize', this.handleResize.bind(this));
- }
- }
-
- handleResize () {
- let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
- let newWidth = this.props.width || boundingClientRect.width;
- let newHeight = this.props.height || boundingClientRect.height;
- this.setState({ width: newWidth, height: newHeight });
- }
+import { ResizeMixin } from './mixins/resize-mixin';
+import { TooltipsMixin } from './mixins/tooltips-mixin';
+
+
+export const LineChart = React.createClass({
+ mixins: [ResizeMixin, TooltipsMixin],
+
+ propTypes: {
+ data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
+ xValues: React.PropTypes.arrayOf(React.PropTypes.any),
+ padding: React.PropTypes.arrayOf(React.PropTypes.number),
+ backdropConstraints: React.PropTypes.arrayOf(React.PropTypes.number),
+ displayBackdrop: React.PropTypes.bool,
+ displayPoints: React.PropTypes.bool,
+ displayVerticalGrid: React.PropTypes.bool,
+ height: React.PropTypes.number,
+ interpolate: React.PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ displayBackdrop: true,
+ displayPoints: true,
+ displayVerticalGrid: true,
+ xTicks: [],
+ xValues: [],
+ padding: [10, 10, 10, 10],
+ interpolate: 'basis'
+ };
+ },
+
+ getInitialState() {
+ return { width: this.props.width, height: this.props.height };
+ },
renderBackdrop (xScale, yScale) {
if (!this.props.displayBackdrop) {
@@ -33,10 +43,10 @@ export class LineChart extends React.Component {
}
let area = d3.svg.area()
- .x(d => xScale(d.x))
- .y0(yScale.range()[0])
- .y1(d => yScale(d.y))
- .interpolate(this.props.interpolate);
+ .x(d => xScale(d.x))
+ .y0(yScale.range()[0])
+ .y1(d => yScale(d.y))
+ .interpolate(this.props.interpolate);
let data = this.props.data;
if (this.props.backdropConstraints) {
@@ -46,7 +56,7 @@ export class LineChart extends React.Component {
// TODO extract styling
return <path d={area(data)} fill="#4b9fd5" fillOpacity="0.2"/>;
- }
+ },
renderPoints (xScale, yScale) {
if (!this.props.displayPoints) {
@@ -58,7 +68,7 @@ export class LineChart extends React.Component {
return <circle key={index} className="line-chart-point" r="3" cx={x} cy={y}/>;
});
return <g>{points}</g>;
- }
+ },
renderVerticalGrid (xScale, yScale) {
if (!this.props.displayVerticalGrid) {
@@ -71,7 +81,7 @@ export class LineChart extends React.Component {
return <line key={index} className="line-chart-grid" x1={x} x2={x} y1={y1} y2={y2}/>;
});
return <g>{lines}</g>;
- }
+ },
renderXTicks (xScale, yScale) {
if (!this.props.xTicks.length) {
@@ -84,7 +94,7 @@ export class LineChart extends React.Component {
return <text key={index} className="line-chart-tick" x={x} y={y} dy="1.5em">{tick}</text>;
});
return <g>{ticks}</g>;
- }
+ },
renderXValues (xScale, yScale) {
if (!this.props.xValues.length) {
@@ -97,16 +107,16 @@ export class LineChart extends React.Component {
return <text key={index} className="line-chart-tick" x={x} y={y} dy="-1em">{value}</text>;
});
return <g>{ticks}</g>;
- }
+ },
renderLine (xScale, yScale) {
let path = d3.svg.line()
- .x(d => xScale(d.x))
- .y(d => yScale(d.y))
- .interpolate(this.props.interpolate);
+ .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)}/>;
- }
+ },
render () {
if (!this.state.width || !this.state.height) {
@@ -118,11 +128,11 @@ export class LineChart extends React.Component {
let maxY = d3.max(this.props.data, d => d.y);
let xScale = d3.scale.linear()
- .domain(d3.extent(this.props.data, d => d.x))
- .range([0, availableWidth]);
+ .domain(d3.extent(this.props.data, d => d.x))
+ .range([0, availableWidth]);
let yScale = d3.scale.linear()
- .domain([0, maxY])
- .range([availableHeight, 0]);
+ .domain([0, maxY])
+ .range([availableHeight, 0]);
return <svg className="line-chart" width={this.state.width} height={this.state.height}>
<g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
@@ -135,22 +145,4 @@ export class LineChart extends React.Component {
</g>
</svg>;
}
-}
-
-LineChart.defaultProps = {
- displayBackdrop: true,
- displayPoints: true,
- displayVerticalGrid: true,
- xTicks: [],
- xValues: [],
- padding: [10, 10, 10, 10],
- interpolate: 'basis'
-};
-
-LineChart.propTypes = {
- data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
- xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
- xValues: React.PropTypes.arrayOf(React.PropTypes.any),
- padding: React.PropTypes.arrayOf(React.PropTypes.number),
- backdropConstraints: React.PropTypes.arrayOf(React.PropTypes.number)
-};
+});
diff --git a/server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js b/server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js
new file mode 100644
index 00000000000..206cb90f355
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/charts/mixins/resize-mixin.js
@@ -0,0 +1,27 @@
+import React from 'react';
+
+export const ResizeMixin = {
+ componentDidMount () {
+ if (this.isResizable()) {
+ this.handleResize();
+ window.addEventListener('resize', this.handleResize);
+ }
+ },
+
+ componentWillUnmount () {
+ if (this.isResizable()) {
+ window.removeEventListener('resize', this.handleResize);
+ }
+ },
+
+ handleResize () {
+ let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+ let newWidth = this.props.width || boundingClientRect.width;
+ let newHeight = this.props.height || boundingClientRect.height;
+ this.setState({ width: newWidth, height: newHeight });
+ },
+
+ isResizable() {
+ return !this.props.width || !this.props.height;
+ }
+};
diff --git a/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js b/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js
new file mode 100644
index 00000000000..2b3fc24346e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js
@@ -0,0 +1,17 @@
+import $ from 'jquery';
+import React from 'react';
+
+export const TooltipsMixin = {
+ componentDidMount () {
+ this.initTooltips();
+ },
+
+ componentDidUpdate () {
+ this.initTooltips();
+ },
+
+ initTooltips () {
+ $('[data-toggle="tooltip"]', React.findDOMNode(this))
+ .tooltip({ container: 'body', placement: 'bottom', html: true });
+ }
+};
diff --git a/server/sonar-web/src/main/js/components/charts/timeline.js b/server/sonar-web/src/main/js/components/charts/timeline.js
deleted file mode 100644
index d200ff154cf..00000000000
--- a/server/sonar-web/src/main/js/components/charts/timeline.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import d3 from 'd3';
-import React from 'react';
-
-export class Timeline extends React.Component {
- constructor (props) {
- super();
- this.state = { width: props.width, height: props.height };
- }
-
- componentDidMount () {
- if (!this.props.width || !this.props.height) {
- this.handleResize();
- window.addEventListener('resize', this.handleResize.bind(this));
- }
- }
-
- componentWillUnmount () {
- if (!this.props.width || !this.props.height) {
- window.removeEventListener('resize', this.handleResize.bind(this));
- }
- }
-
- handleResize () {
- let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
- let newWidth = this.props.width || boundingClientRect.width;
- let newHeight = this.props.height || boundingClientRect.height;
- this.setState({ width: newWidth, height: newHeight });
- }
-
- renderBackdrop (xScale, yScale, maxY) {
- if (!this.props.displayBackdrop) {
- return null;
- }
-
- let area = d3.svg.area()
- .x(d => xScale(d.date))
- .y0(maxY)
- .y1(d => yScale(d.value))
- .interpolate(this.props.interpolate);
-
- // TODO extract styling
- return <path d={area(this.props.snapshots)} fill="#4b9fd5" fillOpacity="0.2"/>;
- }
-
- renderLine (xScale, yScale) {
- let path = d3.svg.line()
- .x(d => xScale(d.date))
- .y(d => yScale(d.value))
- .interpolate(this.props.interpolate);
-
- // TODO extract styling
- return <path d={path(this.props.snapshots)} stroke="#4b9fd5" strokeWidth={this.props.lineWidth} fill="none"/>;
- }
-
- render () {
- if (!this.state.width || !this.state.height) {
- return <div/>;
- }
-
- let maxY = d3.max(this.props.snapshots, d => d.value);
- let xScale = d3.time.scale()
- .domain(d3.extent(this.props.snapshots, d => d.date))
- .range([0, this.state.width - this.props.lineWidth]);
- let yScale = d3.scale.linear()
- .domain([0, maxY])
- .range([this.state.height, 0]);
-
- return <svg width={this.state.width} height={this.state.height}>
- <g transform={`translate(${this.props.lineWidth / 2}, ${this.props.lineWidth / 2})`}>
- {this.renderBackdrop(xScale, yScale, maxY)}
- {this.renderLine(xScale, yScale)}
- </g>
- </svg>;
- }
-}
-
-Timeline.defaultProps = {
- lineWidth: 2,
- displayBackdrop: true,
- interpolate: 'basis'
-};
-
-Timeline.propTypes = {
- snapshots: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
-};
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 d00ce0be0fc..51b140e003d 100644
--- a/server/sonar-web/src/main/js/components/charts/treemap.js
+++ b/server/sonar-web/src/main/js/components/charts/treemap.js
@@ -1,13 +1,15 @@
-import $ from 'jquery';
import _ from 'underscore';
import d3 from 'd3';
import React from 'react';
+import { ResizeMixin } from './mixins/resize-mixin';
+import { TooltipsMixin } from './mixins/tooltips-mixin';
+
const SIZE_SCALE = d3.scale.linear()
- .domain([3, 15])
- .range([11, 18])
- .clamp(true);
+ .domain([3, 15])
+ .range([11, 18])
+ .clamp(true);
function mostCommitPrefix (strings) {
@@ -25,7 +27,17 @@ function mostCommitPrefix (strings) {
}
-export class TreemapRect extends React.Component {
+export const TreemapRect = React.createClass({
+ propTypes: {
+ x: React.PropTypes.number.isRequired,
+ y: React.PropTypes.number.isRequired,
+ width: React.PropTypes.number.isRequired,
+ height: React.PropTypes.number.isRequired,
+ fill: React.PropTypes.string.isRequired,
+ label: React.PropTypes.string.isRequired,
+ prefix: React.PropTypes.string
+ },
+
render () {
let tooltipAttrs = {};
if (this.props.tooltip) {
@@ -48,52 +60,20 @@ export class TreemapRect extends React.Component {
style={{ maxWidth: this.props.width }}/>
</div>;
}
-}
+});
-TreemapRect.propTypes = {
- x: React.PropTypes.number.isRequired,
- y: React.PropTypes.number.isRequired,
- width: React.PropTypes.number.isRequired,
- height: React.PropTypes.number.isRequired,
- fill: React.PropTypes.string.isRequired
-};
+export const Treemap = React.createClass({
+ mixins: [ResizeMixin, TooltipsMixin],
-export class Treemap extends React.Component {
- constructor (props) {
- super();
- this.state = { width: props.width, height: props.height };
- }
+ propTypes: {
+ items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ height: React.PropTypes.number
+ },
- componentDidMount () {
- if (!this.props.width || !this.props.height) {
- this.handleResize();
- window.addEventListener('resize', this.handleResize.bind(this));
- }
- this.initTooltips();
- }
-
- componentDidUpdate () {
- this.initTooltips();
- }
-
- componentWillUnmount () {
- if (!this.props.width || !this.props.height) {
- window.removeEventListener('resize', this.handleResize.bind(this));
- }
- }
-
- initTooltips () {
- $('[data-toggle="tooltip"]', React.findDOMNode(this))
- .tooltip({ container: 'body', placement: 'top', html: true });
- }
-
- handleResize () {
- let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
- let newWidth = this.props.width || boundingClientRect.width;
- let newHeight = this.props.height || boundingClientRect.height;
- this.setState({ width: newWidth, height: newHeight });
- }
+ getInitialState() {
+ return { width: this.props.width, height: this.props.height };
+ },
render () {
if (!this.state.width || !this.state.height || !this.props.items.length) {
@@ -101,12 +81,12 @@ export class Treemap extends React.Component {
}
let sizeScale = d3.scale.linear()
- .domain([0, d3.max(this.props.items, d => d.size)])
- .range([5, 45]);
+ .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))
- .size([this.state.width, 360]);
+ .round(true)
+ .value(d => sizeScale(d.size))
+ .size([this.state.width, 360]);
let nodes = treemap
.nodes({ children: this.props.items })
.filter(d => !d.children);
@@ -133,8 +113,4 @@ export class Treemap extends React.Component {
</div>
</div>;
}
-}
-
-Treemap.propTypes = {
- items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
-};
+});
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 e2382d9e035..53c36ee76af 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
@@ -1,8 +1,16 @@
-import $ from 'jquery';
import _ from 'underscore';
import React from 'react';
-export class Word extends React.Component {
+import { TooltipsMixin } from './mixins/tooltips-mixin';
+
+export const Word = React.createClass({
+ propTypes: {
+ size: React.PropTypes.number.isRequired,
+ text: React.PropTypes.string.isRequired,
+ tooltip: React.PropTypes.string,
+ link: React.PropTypes.string.isRequired
+ },
+
render () {
let tooltipAttrs = {};
if (this.props.tooltip) {
@@ -13,22 +21,22 @@ export class Word extends React.Component {
}
return <a {...tooltipAttrs} style={{ fontSize: this.props.size }} href={this.props.link}>{this.props.text}</a>;
}
-}
+});
-export class WordCloud extends React.Component {
- componentDidMount () {
- this.initTooltips();
- }
+export const WordCloud = React.createClass({
+ mixins: [TooltipsMixin],
- componentDidUpdate () {
- this.initTooltips();
- }
+ propTypes: {
+ items: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ sizeRange: React.PropTypes.arrayOf(React.PropTypes.number)
+ },
- initTooltips () {
- $('[data-toggle="tooltip"]', React.findDOMNode(this))
- .tooltip({ container: 'body', placement: 'bottom', html: true });
- }
+ getDefaultProps() {
+ return {
+ sizeRange: [10, 24]
+ };
+ },
render () {
let len = this.props.items.length;
@@ -38,8 +46,8 @@ export class WordCloud extends React.Component {
});
let sizeScale = d3.scale.linear()
- .domain([0, d3.max(this.props.items, d => d.size)])
- .range(this.props.sizeRange);
+ .domain([0, d3.max(this.props.items, d => d.size)])
+ .range(this.props.sizeRange);
let words = sortedItems
.map((item, index) => <Word key={index}
text={item.text}
@@ -48,12 +56,4 @@ export class WordCloud extends React.Component {
tooltip={item.tooltip}/>);
return <div className="word-cloud">{words}</div>;
}
-}
-
-WordCloud.defaultProps = {
- sizeRange: [10, 24]
-};
-
-WordCloud.propTypes = {
- sizeRange: React.PropTypes.arrayOf(React.PropTypes.number)
-};
+});