From 92d021f649a8a39a9ee8122730056c2aea590f05 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 30 Nov 2015 17:23:18 +0100 Subject: [PATCH] SONAR-7062 SONAR-7016 improve timeline on detailed overview pages --- .../overview/components/domain-timeline.js | 10 +++ .../overview/components/timeline-chart.js | 67 ++++++++++++++++++- .../js/components/mixins/tooltips-mixin.js | 13 +++- .../components/timeline-chart-test.js | 28 ++++++-- 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
@@ -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]}/>
; }, 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 {ticks}; }, + 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 ; + }, + + 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 ; + }, + renderLeak (xScale, yScale) { if (!this.props.leakPeriodDate) { return null; @@ -111,6 +152,27 @@ export const Timeline = React.createClass({ return ; }, + 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(`${snapshot.event.version}`); + } + tooltipLines.push(`${moment(snapshot.x).format('LL')}`); + tooltipLines.push(`${snapshot.y ? this.props.formatValue(snapshot.y) : '—'}`); + let tooltip = tooltipLines.join('
'); + return ; + }); + return {points}; + }, + 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)} ; } 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 = ; 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 = ; 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 = ; + let timeline = ; 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 = ; + let timeline = ; let output = TestUtils.renderIntoDocument(timeline); let tick = TestUtils.findRenderedDOMComponentWithClass(output, 'line-chart-tick-x'); expect(tick.textContent).to.equal('0'); -- 2.39.5