]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6360 add detailed "Code Coverage" panel for the "Overview" main page
authorStas Vilchik <vilchiks@gmail.com>
Mon, 9 Nov 2015 17:31:33 +0000 (18:31 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Tue, 10 Nov 2015 15:13:54 +0000 (16:13 +0100)
14 files changed:
server/sonar-web/src/main/js/apps/overview/common-components.js
server/sonar-web/src/main/js/apps/overview/coverage/bubble-chart.js [deleted file]
server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js [deleted file]
server/sonar-web/src/main/js/apps/overview/coverage/coverage-measure.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/coverage/main.js
server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js [deleted file]
server/sonar-web/src/main/js/apps/overview/coverage/timeline.js [deleted file]
server/sonar-web/src/main/js/apps/overview/coverage/treemap.js [deleted file]
server/sonar-web/src/main/js/apps/overview/main/coverage.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/components/charts/donut-chart.js [new file with mode: 0644]
server/sonar-web/src/main/less/init/misc.less
server/sonar-web/src/main/less/pages/overview.less

index da03da65c2a27ec95679f70a29ee4e9a3ce81465..30decc3c7c880c94a28d96d94d2634cecb966e68 100644 (file)
@@ -27,10 +27,12 @@ export const DetailedMeasure = React.createClass({
 
     return <div className="overview-detailed-measure">
       <div className="overview-detailed-measure-nutshell">
-        <span>{localizeMetric(this.props.metric)}</span>
-        <DrilldownLink component={this.props.component.key} metric={this.props.metric}>
-          <span className="overview-detailed-measure-value">{formatMeasure(measure, this.props.type)}</span>
-        </DrilldownLink>
+        <span className="overview-detailed-measure-name">{localizeMetric(this.props.metric)}</span>
+        <span className="overview-detailed-measure-value">
+          <DrilldownLink component={this.props.component.key} metric={this.props.metric}>
+            {formatMeasure(measure, this.props.type)}
+          </DrilldownLink>
+        </span>
         {this.props.children}
       </div>
       {this.renderLeak()}
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/coverage/bubble-chart.js
deleted file mode 100644 (file)
index 826aca9..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import { DomainBubbleChart } from '../domain/bubble-chart';
-
-
-export class CoverageBubbleChart extends React.Component {
-  render () {
-    return <DomainBubbleChart {...this.props}
-        xMetric="complexity"
-        yMetric="coverage"
-        sizeMetrics={['sqale_index']}/>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js b/server/sonar-web/src/main/js/apps/overview/coverage/coverage-details.js
deleted file mode 100644 (file)
index 3b00570..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-import React from 'react';
-
-import { getMeasures } from '../../../api/measures';
-import DrilldownLink from '../helpers/drilldown-link';
-import { formatMeasure } from '../../../helpers/measures';
-
-
-const METRICS = [
-  'coverage',
-  'line_coverage',
-  'branch_coverage',
-  'it_coverage',
-  'it_line_coverage',
-  'it_branch_coverage',
-  'overall_coverage',
-  'overall_line_coverage',
-  'overall_branch_coverage'
-];
-
-
-export class CoverageDetails extends React.Component {
-  constructor () {
-    super();
-    this.state = { measures: {} };
-  }
-
-  componentDidMount () {
-    this.requestDetails();
-  }
-
-  requestDetails () {
-    return getMeasures(this.props.component.key, METRICS).then(measures => {
-      this.setState({ measures });
-    });
-  }
-
-  renderValue (value, metricKey) {
-    if (value != null) {
-      return <DrilldownLink component={this.props.component.key} metric={metricKey}>
-        {formatMeasure(value, 'PERCENT')}
-      </DrilldownLink>;
-    } else {
-      return '—';
-    }
-  }
-
-  renderCoverage (coverage, lineCoverage, branchCoverage, prefix) {
-    return <table className="data zebra">
-      <tbody>
-      <tr>
-        <td>Coverage</td>
-        <td className="thin nowrap text-right">
-          {this.renderValue(coverage, prefix + 'coverage')}
-        </td>
-      </tr>
-      <tr>
-        <td>Line Coverage</td>
-        <td className="thin nowrap text-right">
-          {this.renderValue(lineCoverage, prefix + 'line_coverage')}
-        </td>
-      </tr>
-      <tr>
-        <td>Branch Coverage</td>
-        <td className="thin nowrap text-right">
-          {this.renderValue(branchCoverage, prefix + 'branch_coverage')}
-        </td>
-      </tr>
-      </tbody>
-    </table>;
-  }
-
-  renderUTCoverage () {
-    if (this.state.measures['coverage'] == null) {
-      return null;
-    }
-    return <div className="big-spacer-top">
-      <h4 className="spacer-bottom">Unit Tests</h4>
-      {this.renderCoverage(
-          this.state.measures['coverage'],
-          this.state.measures['line_coverage'],
-          this.state.measures['branch_coverage'],
-          '')}
-    </div>;
-  }
-
-  renderITCoverage () {
-    if (this.state.measures['it_coverage'] == null) {
-      return null;
-    }
-    return <div className="big-spacer-top">
-      <h4 className="spacer-bottom">Integration Tests</h4>
-      {this.renderCoverage(
-          this.state.measures['it_coverage'],
-          this.state.measures['it_line_coverage'],
-          this.state.measures['it_branch_coverage'],
-          'it_')}
-    </div>;
-  }
-
-  renderOverallCoverage () {
-    if (this.state.measures['coverage'] == null ||
-        this.state.measures['it_coverage'] == null ||
-        this.state.measures['overall_coverage'] == null) {
-      return null;
-    }
-    return <div className="big-spacer-top">
-      <h4 className="spacer-bottom">Overall</h4>
-      {this.renderCoverage(
-          this.state.measures['overall_coverage'],
-          this.state.measures['overall_line_coverage'],
-          this.state.measures['overall_branch_coverage'],
-          'overall_')}
-    </div>;
-  }
-
-  render () {
-    return <div className="overview-domain-section">
-      <h2 className="overview-title">Coverage Details</h2>
-      {this.renderUTCoverage()}
-      {this.renderITCoverage()}
-      {this.renderOverallCoverage()}
-    </div>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/coverage-measure.js b/server/sonar-web/src/main/js/apps/overview/coverage/coverage-measure.js
new file mode 100644 (file)
index 0000000..4ab3646
--- /dev/null
@@ -0,0 +1,85 @@
+import React from 'react';
+
+import { formatMeasure, formatMeasureVariation, localizeMetric } from '../../../helpers/measures';
+import DrilldownLink from '../helpers/drilldown-link';
+import { getShortType } from '../helpers/metrics';
+import { DonutChart } from '../../../components/charts/donut-chart';
+
+
+export const CoverageMeasure = React.createClass({
+  renderLeakVariation () {
+    if (!this.props.leakPeriodDate) {
+      return null;
+    }
+    let leak = this.props.leak[this.props.metric];
+    return <div className="overview-detailed-measure-leak">
+      <span className="overview-detailed-measure-value">
+        {formatMeasureVariation(leak, getShortType(this.props.type))}
+      </span>
+    </div>;
+  },
+
+  renderLeakValue () {
+    if (!this.props.leakPeriodDate) {
+      return null;
+    }
+
+    if (!this.props.leakMetric) {
+      return <div className="overview-detailed-measure-leak">&nbsp;</div>;
+    }
+
+    let leak = this.props.leak[this.props.leakMetric];
+
+    let donutData = [
+      { value: leak, fill: '#85bb43' },
+      { value: 100 - leak, fill: '#d4333f' }
+    ];
+
+    return <div className="overview-detailed-measure-leak">
+      <div className="overview-donut-chart">
+        <DonutChart width="20" height="20" thickness="3" data={donutData}/>
+      </div>
+      <span className="overview-detailed-measure-value">
+        <DrilldownLink component={this.props.component.key} metric={this.props.leakMetric}
+                       period={this.props.leakPeriodIndex}>
+          {formatMeasure(leak, this.props.type)}
+        </DrilldownLink>
+      </span>
+    </div>;
+  },
+
+  renderDonut (measure) {
+    if (this.props.metric !== 'PERCENT') {
+      return null;
+    }
+
+    let donutData = [
+      { value: measure, fill: '#85bb43' },
+      { value: 100 - measure, fill: '#d4333f' }
+    ];
+    return <div className="overview-donut-chart">
+      <DonutChart width="20" height="20" thickness="3" data={donutData}/>
+    </div>;
+  },
+
+  render () {
+    let measure = this.props.measures[this.props.metric];
+    if (measure == null) {
+      return null;
+    }
+
+    return <div className="overview-detailed-measure">
+      <div className="overview-detailed-measure-nutshell">
+        <span className="overview-detailed-measure-name">{localizeMetric(this.props.metric)}</span>
+        {this.renderDonut(measure)}
+        <span className="overview-detailed-measure-value">
+          <DrilldownLink component={this.props.component.key} metric={this.props.metric}>
+            {formatMeasure(measure, this.props.type)}
+          </DrilldownLink>
+        </span>
+      </div>
+      {this.renderLeakValue()}
+      {this.renderLeakVariation()}
+    </div>;
+  }
+});
index a4e8f90fa3728fb14ffd15721ddba192f4aab317..7971f22d1825ad85f6702394bae944aedc59cd7d 100644 (file)
+import _ from 'underscore';
+import d3 from 'd3';
 import React from 'react';
 
-import { CoverageDetails } from './coverage-details';
-import { TestsDetails } from './tests-details';
-import { CoverageBubbleChart } from './bubble-chart';
-import { CoverageTimeline } from './timeline';
-import { CoverageTreemap } 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';
+import { formatMeasure, formatMeasureVariation, localizeMetric } from '../../../helpers/measures';
+import { DonutChart } from '../../../components/charts/donut-chart';
+import DrilldownLink from '../helpers/drilldown-link';
+import { CoverageMeasure } from './coverage-measure';
 
 
-export default class extends React.Component {
-  render () {
-    return <div className="overview-detailed-page">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Coverage & Tests</h2>
-      </div>
+const UT_COVERAGE_METRICS = ['coverage', 'new_coverage', 'branch_coverage', 'line_coverage', 'uncovered_conditions',
+  'uncovered_lines'];
+const IT_COVERAGE_METRICS = ['it_coverage', 'new_it_coverage', 'it_branch_coverage', 'it_line_coverage',
+  'it_uncovered_conditions', 'it_uncovered_lines'];
+const OVERALL_COVERAGE_METRICS = ['overall_coverage', 'new_overall_coverage', 'overall_branch_coverage',
+  'overall_line_coverage', 'overall_uncovered_conditions', 'overall_uncovered_lines'];
+const TEST_METRICS = ['tests', 'test_execution_time', 'test_errors', 'test_failures', 'skipped_tests',
+  'test_success_density'];
+const KNOWN_METRICS = [].concat(TEST_METRICS, OVERALL_COVERAGE_METRICS, UT_COVERAGE_METRICS, IT_COVERAGE_METRICS);
 
-      <a className="overview-detailed-page-back" href="#">
-        <i className="icon-chevron-left"/>
-      </a>
 
-      <CoverageTimeline {...this.props}/>
+export const CoverageMain = React.createClass({
+  mixins: [TooltipsMixin],
 
-      <div className="flex-columns">
-        <div className="flex-column flex-column-half">
-          <CoverageDetails {...this.props}/>
-        </div>
-        <div className="flex-column flex-column-half">
-          <TestsDetails {...this.props}/>
+  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 => ['Tests', 'Tests (Integration)', 'Tests (Overall)'].indexOf(metric.domain) !== -1)
+        .map(metric => metric.key);
+  },
+
+  getMetricsForTimeline() {
+    return filterMetricsForDomains(this.props.metrics, ['Tests', 'Tests (Integration)', 'Tests (Overall)']);
+  },
+
+  getAllMetricsForTimeline() {
+    return filterMetrics(this.props.metrics);
+  },
+
+  requestMeasures () {
+    return getMeasuresAndVariations(this.props.component.key, this.getMetricsForDomain());
+  },
+
+  renderLoading () {
+    return <div className="text-center">
+      <i className="spinner spinner-margin"/>
+    </div>;
+  },
+
+  renderLegend () {
+    return <Legend leakPeriodDate={this.state.leakPeriodDate} leakPeriodLabel={this.state.leakPeriodLabel}/>;
+  },
+
+  renderOtherMeasures() {
+    let metrics = filterMetricsForDomains(this.props.metrics, ['Tests', 'Tests (Integration)', 'Tests (Overall)'])
+        .filter(metric => KNOWN_METRICS.indexOf(metric.key) === -1)
+        .map(metric => metric.key);
+    return this.renderListOfMeasures(metrics);
+  },
+
+  renderUTCoverage () {
+    let hasBothTypes = this.state.measures['coverage'] != null && this.state.measures['it_coverage'] != null;
+    if (!hasBothTypes) {
+      return null;
+    }
+    return <div className="overview-detailed-measures-list">
+      <CoverageMeasure {...this.props} {...this.state} metric="coverage" leakMetric="new_coverage" type="PERCENT"/>
+      <CoverageMeasure {...this.props} {...this.state} metric="line_coverage" leakMetric="new_line_coverage" type="PERCENT"/>
+      <CoverageMeasure {...this.props} {...this.state} metric="branch_coverage" leakMetric="new_branch_coverage" type="PERCENT"/>
+
+      <CoverageMeasure {...this.props} {...this.state} metric="uncovered_lines" type="INT"/>
+      <CoverageMeasure {...this.props} {...this.state} metric="uncovered_conditions" type="INT"/>
+    </div>;
+  },
+
+  renderITCoverage () {
+    let hasBothTypes = this.state.measures['coverage'] != null && this.state.measures['it_coverage'] != null;
+    if (!hasBothTypes) {
+      return null;
+    }
+    return <div className="overview-detailed-measures-list">
+      <CoverageMeasure {...this.props} {...this.state} metric="it_coverage" leakMetric="new_it_coverage" type="PERCENT"/>
+      <CoverageMeasure {...this.props} {...this.state} metric="it_line_coverage" leakMetric="new_it_line_coverage" type="PERCENT"/>
+      <CoverageMeasure {...this.props} {...this.state} metric="it_branch_coverage" leakMetric="new_it_branch_coverage" type="PERCENT"/>
+
+      <CoverageMeasure {...this.props} {...this.state} metric="it_uncovered_lines" type="INT"/>
+      <CoverageMeasure {...this.props} {...this.state} metric="it_uncovered_conditions" type="INT"/>
+    </div>;
+  },
+
+  renderOverallCoverage () {
+    return <div className="overview-detailed-measures-list">
+      <CoverageMeasure {...this.props} {...this.state} metric="overall_coverage" leakMetric="new_overall_coverage" type="PERCENT"/>
+      <CoverageMeasure {...this.props} {...this.state} metric="overall_line_coverage" leakMetric="new_overall_line_coverage" type="PERCENT"/>
+      <CoverageMeasure {...this.props} {...this.state} metric="overall_branch_coverage" leakMetric="new_overall_branch_coverage" type="PERCENT"/>
+
+      <CoverageMeasure {...this.props} {...this.state} metric="overall_uncovered_lines" type="INT"/>
+      <CoverageMeasure {...this.props} {...this.state} metric="overall_uncovered_conditions" type="INT"/>
+    </div>;
+  },
+
+  renderListOfMeasures(list) {
+    let metrics = list
+        .map(key => _.findWhere(this.props.metrics, { key }))
+        .map(metric => {
+          return <DetailedMeasure key={metric.key} {...this.props} {...this.state} metric={metric.key}
+                                  type={metric.type}/>;
+        });
+    return <div className="overview-detailed-measures-list">{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">Tests Overview</div>
+            {this.renderLegend()}
+          </div>
+          {this.renderOverallCoverage()}
+          {this.renderUTCoverage()}
+          {this.renderITCoverage()}
+          {this.renderListOfMeasures(TEST_METRICS)}
+          {this.renderOtherMeasures()}
         </div>
+        <DomainBubbleChart {...this.props}
+            xMetric="complexity"
+            yMetric="overall_coverage"
+            sizeMetrics={['overall_uncovered_lines']}/>
       </div>
 
-      <CoverageBubbleChart {...this.props}/>
-      <CoverageTreemap {...this.props}/>
+      <div className="overview-domain-charts">
+        <DomainTimeline {...this.props} {...this.state}
+            initialMetric="overall_coverage"
+            metrics={this.getMetricsForTimeline()}
+            allMetrics={this.getAllMetricsForTimeline()}/>
+        <DomainTreemap {...this.props}
+            sizeMetric="ncloc"
+            colorMetric="overall_coverage"
+            scale={treemapScale}/>
+      </div>
     </div>;
+
   }
-}
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js b/server/sonar-web/src/main/js/apps/overview/coverage/tests-details.js
deleted file mode 100644 (file)
index 0bc3769..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-
-import { DomainMeasuresList } from '../domain/measures-list';
-
-
-const METRICS = [
-  'tests',
-  'skipped_tests',
-  'test_errors',
-  'test_failures',
-  'test_execution_time',
-  'test_success_density'
-];
-
-
-export class TestsDetails extends React.Component {
-  render () {
-    return <div className="overview-domain-section">
-      <h2 className="overview-title">Tests</h2>
-      <DomainMeasuresList {...this.props} metricsToDisplay={METRICS}/>
-    </div>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/timeline.js b/server/sonar-web/src/main/js/apps/overview/coverage/timeline.js
deleted file mode 100644 (file)
index 60e14a0..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-
-import { DomainTimeline } from '../domain/timeline';
-import { filterMetricsForDomains } from '../helpers/metrics';
-
-
-const DOMAINS = [
-  'Tests',
-  'Tests (Integration)',
-  'Tests (Overall)'
-];
-
-
-export class CoverageTimeline extends React.Component {
-  render () {
-    return <DomainTimeline {...this.props}
-        initialMetric="coverage"
-        metrics={filterMetricsForDomains(this.props.metrics, DOMAINS)}/>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/treemap.js b/server/sonar-web/src/main/js/apps/overview/coverage/treemap.js
deleted file mode 100644 (file)
index 251d5f0..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-import d3 from 'd3';
-import React from 'react';
-
-import { DomainTreemap } from '../domain/treemap';
-
-
-const COLORS_5 = ['#ee0000', '#f77700', '#ffee00', '#80cc00', '#00aa00'];
-
-
-export class CoverageTreemap 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="coverage"
-        scale={scale}/>;
-  }
-}
index fa265140f0e69d5045cc10a287d1bea1044c9f17..48e9d888ba87f63973eafbdfdc8aecb64d5a8872 100644 (file)
@@ -45,7 +45,7 @@ export const GeneralCoverage = React.createClass({
     }
 
     return <Domain>
-      <DomainHeader title="Tests"/>
+      <DomainHeader title="Tests" linkTo="/tests"/>
 
       <DomainPanel domain="coverage">
         <DomainNutshell>
index 21c5eb3ccc096e54dcc974f6b237b158ecebc001..01cf406a54d0a7b5aba945dbfb3911e2c7aefca2 100644 (file)
@@ -5,6 +5,7 @@ import GeneralMain from './main/main';
 import Meta from './meta';
 import { SizeMain } from './size/main';
 import { DuplicationsMain } from './duplications/main';
+import { CoverageMain } from './coverage/main';
 
 import { getMetrics } from '../../api/metrics';
 import { RouterMixin } from '../../components/router/router';
@@ -53,6 +54,12 @@ export const Overview = React.createClass({
     </div>;
   },
 
+  renderTests () {
+    return <div className="overview">
+      <CoverageMain {...this.props} {...this.state}/>
+    </div>;
+  },
+
   render () {
     if (!this.state.ready) {
       return this.renderLoading();
@@ -64,6 +71,8 @@ export const Overview = React.createClass({
         return this.renderSize();
       case '/duplications':
         return this.renderDuplications();
+      case '/tests':
+        return this.renderTests();
       default:
         throw new Error('Unknown route: ' + this.state.route);
     }
index 391bf25c7865acf4f19ed3aed698a8bcaeff63ae..ab1d18333581c9f2b11fdd993c0c0b3744b66c25 100644 (file)
@@ -166,6 +166,9 @@ export const BubbleChart = React.createClass({
         .domain([0, d3.max(this.props.items, d => d.size)])
         .range(this.props.sizeRange);
 
+    let xScaleOriginal = xScale.copy();
+    let yScaleOriginal = yScale.copy();
+
     xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
     yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
 
@@ -180,9 +183,9 @@ export const BubbleChart = React.createClass({
     return <svg className="bubble-chart" width={this.state.width} height={this.state.height}>
       <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
         {this.renderXGrid(xScale, yScale)}
-        {this.renderXTicks(xScale, yScale)}
+        {this.renderXTicks(xScale, yScaleOriginal)}
         {this.renderYGrid(xScale, yScale)}
-        {this.renderYTicks(xScale, yScale)}
+        {this.renderYTicks(xScaleOriginal, yScale)}
         {bubbles}
       </g>
     </svg>;
diff --git a/server/sonar-web/src/main/js/components/charts/donut-chart.js b/server/sonar-web/src/main/js/components/charts/donut-chart.js
new file mode 100644 (file)
index 0000000..b54597d
--- /dev/null
@@ -0,0 +1,59 @@
+import d3 from 'd3';
+import React from 'react';
+
+import { ResizeMixin } from './../mixins/resize-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
+
+
+const Sector = React.createClass({
+  render() {
+    let arc = d3.svg.arc()
+        .outerRadius(this.props.radius)
+        .innerRadius(this.props.radius - this.props.thickness);
+    return <path d={arc(this.props.data)} style={{ fill: this.props.fill }}/>;
+  }
+});
+
+
+export const DonutChart = React.createClass({
+  mixins: [ResizeMixin, TooltipsMixin],
+
+  propTypes: {
+    data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
+  },
+
+  getDefaultProps() {
+    return { thickness: 6, padding: [0, 0, 0, 0] };
+  },
+
+  getInitialState() {
+    return { width: this.props.width, height: this.props.height };
+  },
+
+  render () {
+    if (!this.state.width || !this.state.height) {
+      return <div/>;
+    }
+
+    let availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3];
+    let availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2];
+
+    let size = Math.min(availableWidth, availableHeight),
+        radius = Math.floor(size / 2);
+
+    let pie = d3.layout.pie()
+        .sort(null)
+        .value(d => d.value);
+    let sectors = pie(this.props.data).map((d, i) => {
+      return <Sector key={i} data={d} radius={radius} fill={this.props.data[i].fill} thickness={this.props.thickness}/>;
+    });
+
+    return <svg className="donut-chart" width={this.state.width} height={this.state.height}>
+      <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+        <g transform={`translate(${radius}, ${radius})`}>
+          {sectors}
+        </g>
+      </g>
+    </svg>;
+  }
+});
index 272d75522835a60f0970abb496ab38e0384f1aea..bcd74fe38822b704e85303629c5250463fc73944 100644 (file)
@@ -96,6 +96,10 @@ td.big-spacer-top    { padding-top: 16px; }
   border-radius: 2px;
 }
 
+.flex-1 {
+  flex: 1;
+}
+
 
 // Background Color
 
index e85da65f77fa05f9bd528b9e6c910083eeb8f99e..5f5cee8629f0538fff1c3338f93e94f7c5a76867 100644 (file)
   overflow: hidden;
 }
 
+.overview-detailed-measures-list + .overview-detailed-measures-list {
+  margin-top: 40px;
+}
+
 .overview-detailed-measure {
   display: flex;
   background-color: #fff;
 }
 
 .overview-detailed-measure-nutshell,
-.overview-detailed-measure-leak {
+.overview-detailed-measure-leak,
+.overview-detailed-measure-chart {
   position: relative;
   padding: 7px 10px;
+
+  & & {
+    padding-right: 0;
+  }
 }
 
 .overview-detailed-measure-nutshell {
   text-align: center;
 }
 
+.overview-detailed-measure-name {
+  flex: 1;
+}
+
 .overview-detailed-measure-value {
   font-size: 16px;
 }
   }
 }
 
+.overview-donut-chart {
+  display: inline-block;
+  vertical-align: top;
+  margin-right: 8px;
+}
+
+
+
 /*
  * Responsive Stuff
  */