diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-11-30 17:23:18 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-11-30 17:27:49 +0100 |
commit | 92d021f649a8a39a9ee8122730056c2aea590f05 (patch) | |
tree | 94aa5d3daeecce6936ab0dbd4491f0fa88eb6c08 /server/sonar-web | |
parent | 5a1a0e3fbec92bbc8508cfc5165b2c6590666ce5 (diff) | |
download | sonarqube-92d021f649a8a39a9ee8122730056c2aea590f05.tar.gz sonarqube-92d021f649a8a39a9ee8122730056c2aea590f05.zip |
SONAR-7062 SONAR-7016 improve timeline on detailed overview pages
Diffstat (limited to 'server/sonar-web')
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'); |