]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7062 SONAR-7016 improve timeline on detailed overview pages
authorStas Vilchik <vilchiks@gmail.com>
Mon, 30 Nov 2015 16:23:18 +0000 (17:23 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 30 Nov 2015 16:27:49 +0000 (17:27 +0100)
server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js
server/sonar-web/src/main/js/apps/overview/components/timeline-chart.js
server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
server/sonar-web/tests/apps/overview/components/timeline-chart-test.js

index 336268b9c35262f9c7912efe22e8a58d1186dd83..e0e35b7b243a3999b45cc32e1a41674b18f86d93 100644 (file)
@@ -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>;
   },
index 3453d64918a0a30eba3d467ba32941d9def7defb..c9783bdfc344cd31abd23cb87ed25c4491fe81d3 100644 (file)
@@ -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>;
   }
index af0c2be023e67e2b104539f031d2ba5bd8e874d1..03e9168c2dab6f9473bdb447096637b84ec2125d 100644 (file)
@@ -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))
index 3aae4e12c6471b89c872f6017b4b9779f861fdd6..842c34057440be6a837c71d5bc13533bfecc8148 100644 (file)
@@ -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');