aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js10
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/timeline-chart.js67
-rw-r--r--server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js13
-rw-r--r--server/sonar-web/tests/apps/overview/components/timeline-chart-test.js28
4 files changed, 112 insertions, 6 deletions
diff --git a/server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js b/server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js
index 336268b9c35..e0e35b7b243 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js
+++ b/server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js
@@ -32,6 +32,10 @@ export const DomainTimeline = React.createClass({
};
},
+ componentWillMount () {
+ this.handleScan = _.debounce(this.handleScan, 10, true);
+ },
+
componentDidMount () {
Promise.all([
this.requestTimeMachineData(this.state.currentMetric, this.state.comparisonMetric),
@@ -88,6 +92,10 @@ export const DomainTimeline = React.createClass({
return groupByDomain(this.props.metrics);
},
+ handleScan(scanner) {
+ this.setState({ scanner });
+ },
+
renderLoading () {
return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
<i className="spinner"/>
@@ -140,6 +148,8 @@ export const DomainTimeline = React.createClass({
formatValue={formatValue}
formatYTick={formatYTick}
leakPeriodDate={this.props.leakPeriodDate}
+ scanner={this.state.scanner}
+ onScan={this.handleScan}
padding={[25, 25, 25, 60]}/>
</div>;
},
diff --git a/server/sonar-web/src/main/js/apps/overview/components/timeline-chart.js b/server/sonar-web/src/main/js/apps/overview/components/timeline-chart.js
index 3453d64918a..c9783bdfc34 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/timeline-chart.js
+++ b/server/sonar-web/src/main/js/apps/overview/components/timeline-chart.js
@@ -1,3 +1,4 @@
+import $ from 'jquery';
import _ from 'underscore';
import d3 from 'd3';
import moment from 'moment';
@@ -25,7 +26,7 @@ export const Timeline = React.createClass({
},
getInitialState() {
- return { width: this.props.width, height: this.props.height };
+ return { width: this.props.width, height: this.props.height, scanner: 0 };
},
getRatingScale(availableHeight) {
@@ -53,6 +54,16 @@ export const Timeline = React.createClass({
}
},
+ handleMouseMove(xScale, e) {
+ const x = e.pageX - this.refs.container.getBoundingClientRect().left;
+ const scanner = this.props.data
+ .reduce((previousValue, currentValue) => {
+ return Math.abs(xScale(currentValue.x) - x) < Math.abs(xScale(previousValue.x) - x) ?
+ currentValue : previousValue;
+ }, _.first(this.props.data));
+ this.props.onScan(scanner.x);
+ },
+
renderHorizontalGrid (xScale, yScale) {
let hasTicks = typeof yScale.ticks === 'function';
let ticks = hasTicks ? yScale.ticks(4) : yScale.domain();
@@ -89,6 +100,36 @@ export const Timeline = React.createClass({
return <g>{ticks}</g>;
},
+ renderBackdrop (xScale, yScale) {
+ let opts = {
+ x: _.first(xScale.range()),
+ y: _.last(yScale.range()),
+ width: _.last(xScale.range()) - _.first(xScale.range()),
+ height: _.first(yScale.range()) - _.last(yScale.range()),
+ style: {
+ opacity: 0
+ }
+ };
+ return <rect {...opts} ref="container" onMouseMove={this.handleMouseMove.bind(this, xScale)}/>;
+ },
+
+ renderScanner (xScale, yScale) {
+ if (this.props.scanner == null) {
+ return null;
+ }
+ let snapshotIndex = this.props.data.findIndex(snapshot => {
+ return snapshot.x.getTime() === this.props.scanner.getTime();
+ });
+ $(this.refs[`snapshot${snapshotIndex}`]).tooltip('show');
+ return <line
+ style={{ opacity: 0 }}
+ className="line-chart-grid"
+ x1={xScale(this.props.scanner)}
+ y1={_.first(yScale.range())}
+ x2={xScale(this.props.scanner)}
+ y2={_.last(yScale.range())}/>;
+ },
+
renderLeak (xScale, yScale) {
if (!this.props.leakPeriodDate) {
return null;
@@ -111,6 +152,27 @@ export const Timeline = React.createClass({
return <path className="line-chart-path" d={path(this.props.data)}/>;
},
+ renderTooltips(xScale, yScale) {
+ let points = this.props.data
+ .map(snapshot => {
+ let event = this.props.events.find(e => snapshot.x.getTime() === e.date.getTime());
+ return _.extend(snapshot, { event });
+ })
+ .map((snapshot, index) => {
+ let tooltipLines = [];
+ if (snapshot.event) {
+ tooltipLines.push(`<span class="nowrap">${snapshot.event.version}</span>`);
+ }
+ tooltipLines.push(`<span class="nowrap">${moment(snapshot.x).format('LL')}</span>`);
+ tooltipLines.push(`<span class="nowrap">${snapshot.y ? this.props.formatValue(snapshot.y) : '—'}</span>`);
+ let tooltip = tooltipLines.join('<br>');
+ return <circle key={index} ref={`snapshot${index}`} style={{ opacity: 0 }}
+ r="4" cx={xScale(snapshot.x)} cy={yScale(snapshot.y)}
+ data-toggle="tooltip" title={tooltip}/>;
+ });
+ return <g>{points}</g>;
+ },
+
renderEvents(xScale, yScale) {
let points = this.props.events
.map(event => {
@@ -152,7 +214,10 @@ export const Timeline = React.createClass({
{this.renderHorizontalGrid(xScale, yScale)}
{this.renderTicks(xScale, yScale)}
{this.renderLine(xScale, yScale)}
+ {this.renderTooltips(xScale, yScale)}
{this.renderEvents(xScale, yScale)}
+ {this.renderScanner(xScale, yScale)}
+ {this.renderBackdrop(xScale, yScale)}
</g>
</svg>;
}
diff --git a/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
index af0c2be023e..03e9168c2da 100644
--- a/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
+++ b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
@@ -8,13 +8,17 @@ export const TooltipsMixin = {
},
componentWillUpdate() {
- this.destroyTooltips();
+ this.hideTooltips();
},
componentDidUpdate () {
this.initTooltips();
},
+ componentWillUnmount() {
+ this.destroyTooltips();
+ },
+
initTooltips () {
if ($.fn.tooltip) {
$('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this))
@@ -22,6 +26,13 @@ export const TooltipsMixin = {
}
},
+ hideTooltips () {
+ if ($.fn.tooltip) {
+ $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this))
+ .tooltip('hide');
+ }
+ },
+
destroyTooltips () {
if ($.fn.tooltip) {
$('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this))
diff --git a/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js b/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js
index 3aae4e12c64..842c3405744 100644
--- a/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js
+++ b/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js
@@ -31,7 +31,12 @@ describe('TimelineChart', function () {
{ x: new Date(2015, 0, 4), y: 'WARN' }
];
- let timeline = <Timeline width={100} height={100} data={DATA} metricType="LEVEL" events={[]}
+ let timeline = <Timeline width={100}
+ height={100}
+ data={DATA}
+ metricType="LEVEL"
+ events={[]}
+ formatValue={FORMAT}
formatYTick={FORMAT}/>;
let output = TestUtils.renderIntoDocument(timeline);
let ticks = TestUtils.scryRenderedDOMComponentsWithClass(output, 'line-chart-tick-x');
@@ -49,7 +54,12 @@ describe('TimelineChart', function () {
{ x: new Date(2015, 0, 4), y: 4 }
];
- let timeline = <Timeline width={100} height={100} data={DATA} metricType="RATING" events={[]}
+ let timeline = <Timeline width={100}
+ height={100}
+ data={DATA}
+ metricType="RATING"
+ events={[]}
+ formatValue={FORMAT}
formatYTick={FORMAT}/>;
let output = TestUtils.renderIntoDocument(timeline);
let ticks = TestUtils.scryRenderedDOMComponentsWithClass(output, 'line-chart-tick-x');
@@ -62,14 +72,24 @@ describe('TimelineChart', function () {
});
it('should display the zero Y tick if all values are zero', function () {
- let timeline = <Timeline width={100} height={100} data={ZERO_DATA} events={[]} formatYTick={FORMAT}/>;
+ let timeline = <Timeline width={100}
+ height={100}
+ data={ZERO_DATA}
+ events={[]}
+ formatValue={FORMAT}
+ formatYTick={FORMAT}/>;
let output = TestUtils.renderIntoDocument(timeline);
let tick = TestUtils.findRenderedDOMComponentWithClass(output, 'line-chart-tick-x');
expect(tick.textContent).to.equal('0');
});
it('should display the zero Y tick if all values are undefined', function () {
- let timeline = <Timeline width={100} height={100} data={NULL_DATA} events={[]} formatYTick={FORMAT}/>;
+ let timeline = <Timeline width={100}
+ height={100}
+ data={NULL_DATA}
+ events={[]}
+ formatValue={FORMAT}
+ formatYTick={FORMAT}/>;
let output = TestUtils.renderIntoDocument(timeline);
let tick = TestUtils.findRenderedDOMComponentWithClass(output, 'line-chart-tick-x');
expect(tick.textContent).to.equal('0');