]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6361 add detailed "Duplications" panel for the "Overview" main page
authorStas Vilchik <vilchiks@gmail.com>
Fri, 23 Oct 2015 13:55:36 +0000 (15:55 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Fri, 23 Oct 2015 13:55:36 +0000 (15:55 +0200)
server/sonar-web/src/main/js/apps/overview/duplications/bubble-chart.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/duplications/duplications-details.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/duplications/main.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/duplications/timeline.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/duplications/treemap.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/formatting.js
server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js
server/sonar-web/src/main/js/apps/overview/main.js

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
new file mode 100644 (file)
index 0000000..4612cdf
--- /dev/null
@@ -0,0 +1,101 @@
+import _ from 'underscore';
+import React from 'react';
+import { BubbleChart } from '../../../components/charts/bubble-chart';
+import { getProjectUrl } from '../../../helpers/Url';
+import { getFiles } from '../../../api/components';
+import { formatMeasure } from '../formatting';
+
+
+const X_METRIC = 'ncloc';
+const Y_METRIC = 'duplicated_blocks';
+const SIZE_METRIC = 'duplicated_lines';
+const COMPONENTS_METRICS = [X_METRIC, Y_METRIC, SIZE_METRIC];
+const HEIGHT = 360;
+
+
+function formatInt (d) {
+  return window.formatMeasure(d, 'SHORT_INT');
+}
+
+function getMeasure (component, metric) {
+  return component.measures[metric] || 0;
+}
+
+
+export class DuplicationsBubbleChart extends React.Component {
+  constructor () {
+    super();
+    this.state = { loading: true, files: [] };
+  }
+
+  componentDidMount () {
+    this.requestFiles();
+  }
+
+  requestFiles () {
+    return getFiles(this.props.component.key, COMPONENTS_METRICS).then(r => {
+      let files = r.map(file => {
+        let measures = {};
+        (file.msr || []).forEach(measure => {
+          measures[measure.key] = measure.val;
+        });
+        return _.extend(file, { measures });
+      });
+      this.setState({ loading: false, files });
+    });
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderBubbleChart () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    let items = this.state.files.map(component => {
+      return {
+        x: getMeasure(component, X_METRIC),
+        y: getMeasure(component, Y_METRIC),
+        size: getMeasure(component, SIZE_METRIC),
+        link: getProjectUrl(component.key)
+      };
+    });
+    let xGrid = this.state.files.map(component => component.measures[X_METRIC]);
+    let tooltips = this.state.files.map(component => {
+      let inner = [
+        component.name,
+        `Lines of Code: ${formatMeasure(getMeasure(component, X_METRIC), X_METRIC)}`,
+        `Duplicated Blocks: ${formatMeasure(getMeasure(component, Y_METRIC), Y_METRIC)}`,
+        `Duplicated Lines: ${formatMeasure(getMeasure(component, SIZE_METRIC), SIZE_METRIC)}`
+      ].join('<br>');
+      return `<div class="text-left">${inner}</div>`;
+    });
+    return <BubbleChart items={items}
+                        xGrid={xGrid}
+                        tooltips={tooltips}
+                        height={HEIGHT}
+                        padding={[25, 30, 50, 60]}
+                        formatXTick={formatInt}
+                        formatYTick={formatInt}/>;
+  }
+
+  render () {
+    return <div className="overview-bubble-chart overview-domain-dark">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Project Files</h2>
+        <ul className="list-inline small">
+          <li>X: Lines of Code</li>
+          <li>Y: Duplicated Blocks</li>
+          <li>Size: Duplicated Lines</li>
+        </ul>
+      </div>
+      <div>
+        {this.renderBubbleChart()}
+      </div>
+    </div>;
+  }
+}
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
new file mode 100644 (file)
index 0000000..dace423
--- /dev/null
@@ -0,0 +1,63 @@
+import React from 'react';
+import { getMeasures } from '../../../api/measures';
+import { formatMeasure } from '../formatting';
+
+
+const METRICS = [
+  'duplicated_blocks',
+  'duplicated_files',
+  'duplicated_lines',
+  'duplicated_lines_density'
+];
+
+
+export class DuplicationsDetails extends React.Component {
+  constructor () {
+    super();
+    this.state = { measures: {} };
+  }
+
+  componentDidMount () {
+    this.requestDetails();
+  }
+
+  requestDetails () {
+    return getMeasures(this.props.component.key, METRICS).then(measures => {
+      this.setState({ measures });
+    });
+  }
+
+  render () {
+    return <div className="overview-domain-section">
+      <h2 className="overview-title">Details</h2>
+      <table className="data zebra">
+        <tbody>
+        <tr>
+          <td>Duplications</td>
+          <td className="thin nowrap text-right">
+            {formatMeasure(this.state.measures['duplicated_lines_density'], 'duplicated_lines_density')}
+          </td>
+        </tr>
+        <tr>
+          <td>Blocks</td>
+          <td className="thin nowrap text-right">
+            {formatMeasure(this.state.measures['duplicated_blocks'], 'duplicated_blocks')}
+          </td>
+        </tr>
+        <tr>
+          <td>Files</td>
+          <td className="thin nowrap text-right">
+            {formatMeasure(this.state.measures['duplicated_files'], 'duplicated_files')}
+          </td>
+        </tr>
+        <tr>
+          <td>Lines</td>
+          <td className="thin nowrap text-right">
+            {formatMeasure(this.state.measures['duplicated_lines'], 'duplicated_lines')}
+          </td>
+        </tr>
+        </tbody>
+      </table>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/duplications/main.js b/server/sonar-web/src/main/js/apps/overview/duplications/main.js
new file mode 100644 (file)
index 0000000..3cda843
--- /dev/null
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import { DuplicationsDetails } from './duplications-details';
+import { DuplicationsBubbleChart } from './bubble-chart';
+import { DuplicationsTimeline } from './timeline';
+import { DuplicationsTreemap } from './treemap';
+
+import { getSeverities, getTags, getAssignees } from '../../../api/issues';
+
+
+export default class DuplicationsDomain extends React.Component {
+  render () {
+    return <div className="overview-domain">
+      <DuplicationsTimeline {...this.props}/>
+      <div className="flex-columns">
+        <div className="flex-column flex-column-half">
+          <DuplicationsDetails {...this.props}/>
+        </div>
+      </div>
+
+      <DuplicationsBubbleChart {...this.props}/>
+      <DuplicationsTreemap {...this.props}/>
+    </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
new file mode 100644 (file)
index 0000000..81c8d63
--- /dev/null
@@ -0,0 +1,131 @@
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { LineChart } from '../../../components/charts/line-chart';
+import { formatMeasure } from '../formatting';
+import { getTimeMachineData } from '../../../api/time-machine';
+import { getEvents } from '../../../api/events';
+
+
+const DUPLICATIONS_METRICS = [
+  'duplicated_blocks',
+  'duplicated_files',
+  'duplicated_lines',
+  'duplicated_lines_density'
+];
+
+const HEIGHT = 280;
+
+
+export class DuplicationsTimeline extends React.Component {
+  constructor () {
+    super();
+    this.state = { loading: true, currentMetric: DUPLICATIONS_METRICS[0] };
+  }
+
+  componentDidMount () {
+    Promise.all([
+      this.requestTimeMachineData(),
+      this.requestEvents()
+    ]).then(() => this.setState({ loading: false }));
+  }
+
+  requestTimeMachineData () {
+    return getTimeMachineData(this.props.component.key, this.state.currentMetric).then(r => {
+      let snapshots = r[0].cells.map(cell => {
+        return { date: moment(cell.d).toDate(), value: cell.v[0] };
+      });
+      this.setState({ snapshots });
+    });
+  }
+
+  requestEvents () {
+    return getEvents(this.props.component.key, 'Version').then(r => {
+      let events = r.map(event => {
+        return { version: event.n, date: moment(event.dt).toDate() };
+      });
+      events = _.sortBy(events, 'date');
+      this.setState({ events });
+    });
+  }
+
+  prepareEvents () {
+    let events = this.state.events;
+    let snapshots = this.state.snapshots;
+    return events
+        .map(event => {
+          let snapshot = snapshots.find(s => s.date.getTime() === event.date.getTime());
+          event.value = snapshot && snapshot.value;
+          return event;
+        })
+        .filter(event => event.value != null);
+  }
+
+  handleMetricChange () {
+    let metric = React.findDOMNode(this.refs.metricSelect).value;
+    this.setState({ currentMetric: metric }, this.requestTimeMachineData);
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderLineChart () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    let events = this.prepareEvents();
+
+    let data = events.map((event, index) => {
+      return { x: index, y: event.value };
+    });
+
+    let xTicks = events.map(event => event.version.substr(0, 6));
+
+    let xValues = events.map(event => formatMeasure(event.value, this.state.currentMetric));
+
+    // TODO use leak period
+    let backdropConstraints = [
+      this.state.events.length - 2,
+      this.state.events.length - 1
+    ];
+
+    return <LineChart data={data}
+                      xTicks={xTicks}
+                      xValues={xValues}
+                      backdropConstraints={backdropConstraints}
+                      height={HEIGHT}
+                      interpolate="linear"
+                      padding={[25, 30, 50, 30]}/>;
+  }
+
+  renderTimelineMetricSelect () {
+    if (this.state.loading) {
+      return null;
+    }
+
+    let options = DUPLICATIONS_METRICS
+        .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
+
+    return <select ref="metricSelect"
+                   className="overview-timeline-select"
+                   onChange={this.handleMetricChange.bind(this)}
+                   value={this.state.currentMetric}>{options}</select>;
+  }
+
+  render () {
+    return <div className="overview-timeline overview-domain-dark">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Project History</h2>
+        {this.renderTimelineMetricSelect()}
+      </div>
+      <div>
+        {this.renderLineChart()}
+      </div>
+    </div>;
+  }
+}
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
new file mode 100644 (file)
index 0000000..0b3b00e
--- /dev/null
@@ -0,0 +1,88 @@
+import _ from 'underscore';
+import d3 from 'd3';
+import React from 'react';
+
+import { Treemap } from '../../../components/charts/treemap';
+import { formatMeasure } from '../formatting';
+import { getChildren } from '../../../api/components';
+
+const COMPONENTS_METRICS = [
+  'ncloc',
+  'duplicated_lines_density'
+];
+
+const HEIGHT = 360;
+
+const COLORS_5 = ['#00aa00', '#80cc00', '#ffee00', '#f77700', '#ee0000'];
+
+export class DuplicationsTreemap extends React.Component {
+  constructor () {
+    super();
+    this.state = { loading: true, components: [] };
+  }
+
+  componentDidMount () {
+    this.requestComponents();
+  }
+
+  requestComponents () {
+    return getChildren(this.props.component.key, COMPONENTS_METRICS).then(r => {
+      let components = r.map(component => {
+        let measures = {};
+        (component.msr || []).forEach(measure => {
+          measures[measure.key] = measure.val;
+        });
+        return _.extend(component, { measures });
+      });
+      this.setState({ loading: false, components });
+    });
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderTreemap () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    let colorScale = d3.scale.linear().domain([0, 25, 50, 75, 100]);
+    colorScale.range(COLORS_5);
+
+    let items = this.state.components.map(component => {
+      let duplications = component.measures['duplicated_lines_density'];
+      return {
+        size: component.measures['ncloc'],
+        color: duplications != null ? colorScale(duplications) : '#777'
+      };
+    });
+    let labels = this.state.components.map(component => component.name);
+    let tooltips = this.state.components.map(component => {
+      let inner = [
+        component.name,
+        `Lines of Code: ${formatMeasure(component.measures['ncloc'], 'ncloc')}`,
+        `Duplications: ${formatMeasure(component.measures['duplicated_lines_density'], 'duplicated_lines_density')}`
+      ].join('<br>');
+      return `<div class="text-left">${inner}</div>`;
+    });
+    return <Treemap items={items} labels={labels} tooltips={tooltips} height={HEIGHT}/>;
+  }
+
+  render () {
+    return <div className="overview-domain-section overview-treemap">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Project Components</h2>
+        <ul className="list-inline small">
+          <li>Size: Lines of Code</li>
+          <li>Color: Duplications</li>
+        </ul>
+      </div>
+      <div>
+        {this.renderTreemap()}
+      </div>
+    </div>;
+  }
+}
index 5baa7d2f13f9dd479abe7b3a51207bbefa5adbdd..c35de0f91fd72b190d0d45a9eac5292a06b38c72 100644 (file)
@@ -45,6 +45,11 @@ const METRIC_TYPES = {
   'test_execution_time': 'MILLISEC',
   'test_success_density': 'PERCENT',
 
+  'duplicated_blocks': 'INT',
+  'duplicated_files': 'INT',
+  'duplicated_lines': 'INT',
+  'duplicated_lines_density': 'PERCENT',
+
   'complexity': 'INT'
 };
 
index 71a4055b26a9fdfc361056f15806ed2d9cf7f468..92594c6c6ab9c184d22ea12a98c8d9fb7fa10be5 100644 (file)
@@ -18,8 +18,10 @@ export default React.createClass({
       return null;
     }
 
+    let active = this.props.section === 'duplications';
+
     return (
-        <Card>
+        <Card linkTo="duplications" active={active} onRoute={this.props.onRoute}>
           <div className="measures">
             <div className="measures-chart">
               <Donut data={donutData} size="47"/>
index 6f9ddcfe7de08483fde420df64432ee27db1c423..c0520880841c9b7d3f168bdcd3457c7bbb44aa66 100644 (file)
@@ -3,6 +3,7 @@ import offset from 'document-offset';
 import GeneralMain from './general/main';
 import IssuesMain from './issues/main';
 import CoverageMain from './coverage/main';
+import DuplicationsMain from './duplications/main';
 import Meta from './meta';
 
 export default class Overview extends React.Component {
@@ -31,6 +32,9 @@ export default class Overview extends React.Component {
       case 'coverage':
         child = <CoverageMain {...this.props}/>;
         break;
+      case 'duplications':
+        child = <DuplicationsMain {...this.props}/>;
+        break;
       default:
         child = null;
     }