]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6361 add detailed "Duplications" panel for the "Overview" main page
authorStas Vilchik <vilchiks@gmail.com>
Mon, 9 Nov 2015 13:07:58 +0000 (14:07 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 9 Nov 2015 16:24:30 +0000 (17:24 +0100)
12 files changed:
server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js
server/sonar-web/src/main/js/apps/overview/duplications/bubble-chart.js [deleted file]
server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js [deleted file]
server/sonar-web/src/main/js/apps/overview/duplications/main.js
server/sonar-web/src/main/js/apps/overview/duplications/timeline.js [deleted file]
server/sonar-web/src/main/js/apps/overview/duplications/treemap.js [deleted file]
server/sonar-web/src/main/js/apps/overview/main/duplications.js
server/sonar-web/src/main/js/apps/overview/overview.js
server/sonar-web/src/main/js/components/charts/bubble-chart.js
server/sonar-web/src/main/js/helpers/constants.js
server/sonar-web/src/main/less/pages/overview.less
server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb

index 58c65c45ad1bba9cc10452d6a7d394aea1fb3810..e112d8a230d0778fe71c383008a8d9a129f04898 100644 (file)
@@ -101,7 +101,7 @@ export class DomainBubbleChart extends React.Component {
   }
 
   render () {
-    return <div className="overview-bubble-chart overview-domain-dark">
+    return <div className="overview-domain overview-domain-chart">
       <div className="overview-domain-header">
         <h2 className="overview-title">Project Files</h2>
         <ul className="list-inline small">
@@ -110,7 +110,7 @@ export class DomainBubbleChart extends React.Component {
           <li>Size: {this.getSizeMetricsTitle()}</li>
         </ul>
       </div>
-      <div>
+      <div className="overview-bubble-chart">
         {this.renderBubbleChart()}
       </div>
     </div>;
diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/duplications/bubble-chart.js
deleted file mode 100644 (file)
index 299a479..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import { DomainBubbleChart } from '../domain/bubble-chart';
-
-
-export class DuplicationsBubbleChart extends React.Component {
-  render () {
-    return <DomainBubbleChart {...this.props}
-        xMetric="ncloc"
-        yMetric="duplicated_blocks"
-        sizeMetrics={['duplicated_lines']}/>;
-  }
-}
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
deleted file mode 100644 (file)
index b1b9ec7..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-
-import { DomainMeasuresList } from '../domain/measures-list';
-
-
-const METRICS = [
-  'duplicated_blocks',
-  'duplicated_files',
-  'duplicated_lines',
-  'duplicated_lines_density'
-];
-
-
-export class DuplicationsDetails extends React.Component {
-  render () {
-    return <div className="overview-domain-section">
-      <h2 className="overview-title">Duplications</h2>
-      <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
-    </div>;
-  }
-}
index 02aa9eceecc350c81d3ab773dfa563a5be7ddbd1..a3aa1d482dd4968c34d8f49678a17af69c98dd2e 100644 (file)
+import d3 from 'd3';
 import React from 'react';
 
-import { DuplicationsDetails } from './duplications-details';
-import { DuplicationsBubbleChart } from './bubble-chart';
-import { DuplicationsTimeline } from './timeline';
-import { DuplicationsTreemap } from './treemap';
+import { getMeasuresAndVariations } from '../../../api/measures';
+import { DetailedMeasure } from '../common-components';
+import { DomainTimeline } from '../timeline/domain-timeline';
+import { DomainTreemap } from '../domain/treemap';
+import { DomainBubbleChart } from '../domain/bubble-chart';
+import { getPeriodLabel, getPeriodDate } from './../helpers/period-label';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { filterMetrics, filterMetricsForDomains } from '../helpers/metrics';
+import { Legend } from '../common-components';
+import { CHART_COLORS_RANGE_PERCENT } from '../../../helpers/constants';
 
 
-export default class extends React.Component {
-  render () {
-    return <div className="overview-detailed-page">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Duplications</h2>
-      </div>
+export const DuplicationsMain = React.createClass({
+  mixins: [TooltipsMixin],
+
+  getInitialState() {
+    return {
+      ready: false,
+      leakPeriodLabel: getPeriodLabel(this.props.component.periods, this.props.leakPeriodIndex),
+      leakPeriodDate: getPeriodDate(this.props.component.periods, this.props.leakPeriodIndex)
+    };
+  },
+
+  componentDidMount() {
+    this.requestMeasures().then(r => {
+      let measures = this.getMeasuresValues(r, 'value');
+      let leak = this.getMeasuresValues(r, 'var' + this.props.leakPeriodIndex);
+      this.setState({ ready: true, measures, leak });
+    });
+  },
+
+  getMeasuresValues (measures, fieldKey) {
+    let values = {};
+    Object.keys(measures).forEach(measureKey => {
+      values[measureKey] = measures[measureKey][fieldKey];
+    });
+    return values;
+  },
+
+  getMetricsForDomain() {
+    return this.props.metrics
+        .filter(metric => ['Duplication'].indexOf(metric.domain) !== -1)
+        .map(metric => metric.key);
+  },
+
+  getMetricsForTimeline() {
+    return filterMetricsForDomains(this.props.metrics, ['Duplication']);
+  },
+
+  getAllMetricsForTimeline() {
+    return filterMetrics(this.props.metrics);
+  },
 
-      <a className="overview-detailed-page-back" href="#">
-        <i className="icon-chevron-left"/>
-      </a>
+  requestMeasures () {
+    return getMeasuresAndVariations(this.props.component.key, this.getMetricsForDomain());
+  },
 
-      <DuplicationsTimeline {...this.props}/>
-      <div className="flex-columns">
-        <div className="flex-column flex-column-half">
-          <DuplicationsDetails {...this.props}/>
+  renderLoading () {
+    return <div className="text-center">
+      <i className="spinner spinner-margin"/>
+    </div>;
+  },
+
+  renderLegend () {
+    return <Legend leakPeriodDate={this.state.leakPeriodDate} leakPeriodLabel={this.state.leakPeriodLabel}/>;
+  },
+
+  renderMeasures() {
+    let metrics = filterMetricsForDomains(this.props.metrics, ['Duplication'])
+        .map(metric => {
+          return <DetailedMeasure key={metric.key} {...this.props} {...this.state} metric={metric.key}
+                                  type={metric.type}/>;
+        });
+    return <div>{metrics}</div>;
+  },
+
+  render () {
+    if (!this.state.ready) {
+      return this.renderLoading();
+    }
+    let treemapScale = d3.scale.linear()
+        .domain([0, 100])
+        .range(CHART_COLORS_RANGE_PERCENT);
+    return <div className="overview-detailed-page">
+      <div className="overview-domain-charts">
+        <div className="overview-domain">
+          <div className="overview-domain-header">
+            <div className="overview-title">Duplications Overview</div>
+            {this.renderLegend()}
+          </div>
+          <div className="overview-detailed-measures-list">
+            {this.renderMeasures()}
+          </div>
         </div>
+        <DomainBubbleChart {...this.props}
+            xMetric="ncloc"
+            yMetric="duplicated_blocks"
+            sizeMetrics={['duplicated_lines']}/>
       </div>
 
-      <DuplicationsBubbleChart {...this.props}/>
-      <DuplicationsTreemap {...this.props}/>
+      <div className="overview-domain-charts">
+        <DomainTimeline {...this.props} {...this.state}
+            initialMetric="duplicated_lines_density"
+            metrics={this.getMetricsForTimeline()}
+            allMetrics={this.getAllMetricsForTimeline()}/>
+        <DomainTreemap {...this.props}
+            sizeMetric="ncloc"
+            colorMetric="duplicated_lines_density"
+            scale={treemapScale}/>
+      </div>
     </div>;
+
   }
-}
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/timeline.js b/server/sonar-web/src/main/js/apps/overview/duplications/timeline.js
deleted file mode 100644 (file)
index abd6987..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-
-import { DomainTimeline } from '../domain/timeline';
-import { filterMetricsForDomains } from '../helpers/metrics';
-
-
-const DOMAINS = ['Duplication'];
-
-
-export class DuplicationsTimeline extends React.Component {
-  render () {
-    return <DomainTimeline {...this.props}
-        initialMetric="duplicated_lines_density"
-        metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/treemap.js b/server/sonar-web/src/main/js/apps/overview/duplications/treemap.js
deleted file mode 100644 (file)
index 019e78e..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-import d3 from 'd3';
-import React from 'react';
-
-import { DomainTreemap } from '../domain/treemap';
-
-
-const COLORS_5 = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000'];
-
-
-export class DuplicationsTreemap extends React.Component {
-  render () {
-    let scale = d3.scale.linear()
-        .domain([0, 25, 50, 75, 100])
-        .range(COLORS_5);
-    return <DomainTreemap {...this.props}
-        sizeMetric="ncloc"
-        colorMetric="duplicated_lines_density"
-        scale={scale}/>;
-  }
-}
index 4ae63e785bd34f780382d549a951ef935fabc57b..db4c6e6c32042ad5d7956763a35ea2cdc30471d3 100644 (file)
@@ -31,7 +31,7 @@ export const GeneralDuplications = React.createClass({
 
   render () {
     return <Domain>
-      <DomainHeader title="Duplications"/>
+      <DomainHeader title="Duplications" linkTo="/duplications"/>
 
       <DomainPanel domain="duplications">
         <DomainNutshell>
index dae5318a477bbaaa92d11f8b1d4deb86af417fd5..21c5eb3ccc096e54dcc974f6b237b158ecebc001 100644 (file)
@@ -4,6 +4,7 @@ import Gate from './main/gate/gate';
 import GeneralMain from './main/main';
 import Meta from './meta';
 import { SizeMain } from './size/main';
+import { DuplicationsMain } from './duplications/main';
 
 import { getMetrics } from '../../api/metrics';
 import { RouterMixin } from '../../components/router/router';
@@ -46,6 +47,12 @@ export const Overview = React.createClass({
     </div>;
   },
 
+  renderDuplications () {
+    return <div className="overview">
+      <DuplicationsMain {...this.props} {...this.state}/>
+    </div>;
+  },
+
   render () {
     if (!this.state.ready) {
       return this.renderLoading();
@@ -55,6 +62,8 @@ export const Overview = React.createClass({
         return this.renderMain();
       case '/size':
         return this.renderSize();
+      case '/duplications':
+        return this.renderDuplications();
       default:
         throw new Error('Unknown route: ' + this.state.route);
     }
index 2dd929c7c4c3c0d9f4ca2812f3135edc14edde41..391bf25c7865acf4f19ed3aed698a8bcaeff63ae 100644 (file)
@@ -1,3 +1,4 @@
+import _ from 'underscore';
 import d3 from 'd3';
 import React from 'react';
 
@@ -93,8 +94,7 @@ export const BubbleChart = React.createClass({
       let x = xScale(tick);
       let y1 = yScale.range()[0];
       let y2 = yScale.range()[1];
-      return <line key={index} x1={x} x2={x} y1={y1} y2={y2} className="bubble-chart-grid"
-                   shapeRendering="crispEdges" strokeWidth="0.3"/>;
+      return <line key={index} x1={x} x2={x} y1={y1} y2={y2} className="bubble-chart-grid"/>;
     });
 
     return <g ref="xGrid">{lines}</g>;
@@ -109,8 +109,7 @@ export const BubbleChart = React.createClass({
       let y = yScale(tick);
       let x1 = xScale.range()[0];
       let x2 = xScale.range()[1];
-      return <line key={index} x1={x1} x2={x2} y1={y} y2={y} className="bubble-chart-grid"
-                   shapeRendering="crispEdges" strokeWidth="0.3"/>;
+      return <line key={index} x1={x1} x2={x2} y1={y} y2={y} className="bubble-chart-grid"/>;
     });
 
     return <g ref="yGrid">{lines}</g>;
@@ -156,27 +155,27 @@ export const BubbleChart = React.createClass({
     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)}/>;
-                      });
+    let bubbles = _.sortBy(this.props.items, (a, b) => b.size - a.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]})`}>
index 392adf2202ac0425fc0850aa416abffea5692fa0..d1ba85c5395c0400cb0e6138857f21a7a9e61652 100644 (file)
@@ -1,2 +1,4 @@
 export const SEVERITIES = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
 export const STATUSES = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED'];
+
+export const CHART_COLORS_RANGE_PERCENT = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000'];
index 1379e215a947ac38df73dc445034d442c1469d5a..e85da65f77fa05f9bd528b9e6c910083eeb8f99e 100644 (file)
 
 .overview-detailed-page {
   flex: 1;
+  animation: fadeIn 0.5s forwards;
 }
 
 .overview-detailed-measures-list {
 .overview-domain-charts {
   display: flex;
 
-  .overview-domain-chart {
+  & > .overview-domain,
+  & > .overview-domain-chart {
     flex: 1;
   }
+
+  & > .overview-domain {
+    max-width: 560px;
+  }
 }
 
 .overview-domain-chart {
   align-items: center;
 }
 
+.overview-bubble-chart {
+  padding: 10px;
+  border: 1px solid @barBorderColor;
+  box-sizing: border-box;
+  background-color: #fff;
+
+  .bubble-chart-bubble {
+    fill: @blue;
+    fill-opacity: 0.2;
+    stroke: @blue;
+    cursor: pointer;
+    transition: all 0.2s ease;
+
+    &:hover { fill-opacity: 0.8; }
+  }
+
+  .bubble-chart-grid {
+    shape-rendering: crispedges;
+    stroke: #eee;
+  }
+
+  .bubble-chart-tick {
+    fill: @secondFontColor;
+    font-size: 11px;
+    text-anchor: middle;
+  }
+
+  .bubble-chart-tick-y {
+    text-anchor: end;
+  }
+}
+
 /*
  * Responsive Stuff
  */
index b9447a88eb59c563212e038beae7ffb8065c5c83..d1004193763d07e185b743c68e1c21f26d61e670 100644 (file)
@@ -61,7 +61,7 @@
             index: '<%= index -%>',
             mode: '<%= @snapshot.period_mode(index) -%>',
             modeParam: '<%= @snapshot.period_param(index) -%>',
-            date: '<%= @snapshot.period_datetime(index).to_date.strftime('%FT%T%z') -%>'
+            date: '<%= @snapshot.period_datetime(index).strftime('%FT%T%z') -%>'
           },
           <% end %>
           <% end %>