]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6331 refactor overview code
authorStas Vilchik <vilchiks@gmail.com>
Thu, 12 Nov 2015 17:05:08 +0000 (18:05 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Fri, 13 Nov 2015 13:25:00 +0000 (14:25 +0100)
78 files changed:
server/sonar-web/package.json
server/sonar-web/src/main/js/apps/overview/common-components.js [deleted file]
server/sonar-web/src/main/js/apps/overview/components/complexity-distribution.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/coverage-measure.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/coverage-measures-list.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/detailed-measure.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/domain-bubble-chart.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/domain-treemap.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/issue-measure.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/issues-assignees.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/issues-tags.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/language-distribution.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/legend.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/timeline-chart.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/coverage/coverage-measure.js [deleted file]
server/sonar-web/src/main/js/apps/overview/coverage/main.js [deleted file]
server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js [deleted file]
server/sonar-web/src/main/js/apps/overview/domain/header.js [deleted file]
server/sonar-web/src/main/js/apps/overview/domain/measures-list.js [deleted file]
server/sonar-web/src/main/js/apps/overview/domain/timeline.js [deleted file]
server/sonar-web/src/main/js/apps/overview/domain/treemap.js [deleted file]
server/sonar-web/src/main/js/apps/overview/domains/coverage-domain.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/domains/debt-domain.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/domains/duplications-domain.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/domains/size-domain.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/duplications/main.js [deleted file]
server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate/gate.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/donut.js [deleted file]
server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js [deleted file]
server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js [deleted file]
server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js [deleted file]
server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js [deleted file]
server/sonar-web/src/main/js/apps/overview/helpers/measure.js [deleted file]
server/sonar-web/src/main/js/apps/overview/helpers/period-label.js [deleted file]
server/sonar-web/src/main/js/apps/overview/helpers/periods.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js [deleted file]
server/sonar-web/src/main/js/apps/overview/helpers/rating.js [deleted file]
server/sonar-web/src/main/js/apps/overview/issues/assignees.js [deleted file]
server/sonar-web/src/main/js/apps/overview/issues/issue-measure.js [deleted file]
server/sonar-web/src/main/js/apps/overview/issues/main.js [deleted file]
server/sonar-web/src/main/js/apps/overview/issues/severities.js [deleted file]
server/sonar-web/src/main/js/apps/overview/issues/tags.js [deleted file]
server/sonar-web/src/main/js/apps/overview/main/components.js
server/sonar-web/src/main/js/apps/overview/main/coverage.js
server/sonar-web/src/main/js/apps/overview/main/duplications.js
server/sonar-web/src/main/js/apps/overview/main/gate/gate-condition.js [deleted file]
server/sonar-web/src/main/js/apps/overview/main/gate/gate-conditions.js [deleted file]
server/sonar-web/src/main/js/apps/overview/main/gate/gate-empty.js [deleted file]
server/sonar-web/src/main/js/apps/overview/main/gate/gate.js [deleted file]
server/sonar-web/src/main/js/apps/overview/main/issues.js
server/sonar-web/src/main/js/apps/overview/main/main.js
server/sonar-web/src/main/js/apps/overview/main/size.js
server/sonar-web/src/main/js/apps/overview/meta.js
server/sonar-web/src/main/js/apps/overview/overview.js
server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js [deleted file]
server/sonar-web/src/main/js/apps/overview/size/language-distribution.js [deleted file]
server/sonar-web/src/main/js/apps/overview/size/main.js [deleted file]
server/sonar-web/src/main/js/apps/overview/timeline/domain-timeline.js [deleted file]
server/sonar-web/src/main/js/apps/overview/timeline/timeline-chart.js [deleted file]
server/sonar-web/src/main/js/components/shared/drilldown-link.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/issues-link.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/quality-gate-link.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/quality-profile-link.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/rating.js [new file with mode: 0644]
server/sonar-web/src/main/less/init/misc.less
server/sonar-web/src/main/less/pages/overview.less
server/sonar-web/tests/apps/overview-test.js [deleted file]
server/sonar-web/tests/apps/overview/components/complexity-distribution-test.js [new file with mode: 0644]
server/sonar-web/tests/apps/overview/components/issues-tags-test.js [new file with mode: 0644]
server/sonar-web/tests/apps/overview/components/language-distribution-test.js [new file with mode: 0644]
server/sonar-web/tests/apps/overview/components/legend-test.js [new file with mode: 0644]
server/sonar-web/tests/apps/overview/helpers/metrics-test.js [new file with mode: 0644]
server/sonar-web/tests/apps/overview/helpers/periods-test.js [new file with mode: 0644]
server/sonar-web/tests/apps/overview/overview-test.js [new file with mode: 0644]

index 87a6ac744b1047dda2a54e9ae565cd79024549e2..10b162c4ce7721f34c2e39bb22878bd8b712ff8e 100644 (file)
@@ -13,6 +13,7 @@
     "browserify": "11.2.0",
     "browserify-shim": "3.8.10",
     "chai": "3.3.0",
+    "chai-datetime": "^1.4.0",
     "classnames": "^2.2.0",
     "d3": "3.5.6",
     "del": "2.0.2",
diff --git a/server/sonar-web/src/main/js/apps/overview/common-components.js b/server/sonar-web/src/main/js/apps/overview/common-components.js
deleted file mode 100644 (file)
index d1299d5..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-
-import { formatMeasure, formatMeasureVariation, localizeMetric } from '../../helpers/measures';
-import DrilldownLink from './helpers/drilldown-link';
-import { getShortType } from './helpers/metrics';
-import { DomainLeakTitle } from './main/components';
-
-
-export const DetailedMeasure = React.createClass({
-  renderLeak () {
-    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>;
-  },
-
-  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>
-        <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()}
-    </div>;
-  }
-});
-
-
-export const Legend = React.createClass({
-  render() {
-    if (!this.props.leakPeriodDate) {
-      return null;
-    }
-    return <div className="overview-legend">
-      <span className="overview-legend-leak"/>
-      <DomainLeakTitle label={this.props.leakPeriodLabel} date={this.props.leakPeriodDate}/>
-    </div>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/complexity-distribution.js b/server/sonar-web/src/main/js/apps/overview/components/complexity-distribution.js
new file mode 100644 (file)
index 0000000..390273d
--- /dev/null
@@ -0,0 +1,38 @@
+import React from 'react';
+
+import { BarChart } from '../../../components/charts/bar-chart';
+import { formatMeasure } from '../../../helpers/measures';
+
+
+const HEIGHT = 80;
+
+
+export const ComplexityDistribution = React.createClass({
+  propTypes: {
+    distribution: React.PropTypes.string.isRequired
+  },
+
+  renderBarChart () {
+    let data = this.props.distribution.split(';').map((point, index) => {
+      let tokens = point.split('=');
+      return { x: index, y: parseInt(tokens[1], 10), value: parseInt(tokens[0], 10) };
+    });
+
+    let xTicks = data.map(point => point.value);
+
+    let xValues = data.map(point => formatMeasure(point.y, 'INT'));
+
+    return <BarChart data={data}
+                     xTicks={xTicks}
+                     xValues={xValues}
+                     height={HEIGHT}
+                     barsWidth={10}
+                     padding={[25, 0, 25, 0]}/>;
+  },
+
+  render () {
+    return <div className="overview-bar-chart">
+      {this.renderBarChart()}
+    </div>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/coverage-measure.js b/server/sonar-web/src/main/js/apps/overview/components/coverage-measure.js
new file mode 100644 (file)
index 0000000..df6206f
--- /dev/null
@@ -0,0 +1,53 @@
+import classNames from 'classnames';
+import React from 'react';
+
+import { formatMeasure, formatMeasureVariation, localizeMetric } from '../../../helpers/measures';
+import { DrilldownLink } from '../../../components/shared/drilldown-link';
+import { DonutChart } from '../../../components/charts/donut-chart';
+
+
+export const CoverageMeasure = React.createClass({
+  renderLeak () {
+    if (this.props.leak == null) {
+      return null;
+    }
+    return <div className="overview-detailed-measure-leak">
+      <span className="overview-detailed-measure-value">
+        {formatMeasureVariation(this.props.leak, 'PERCENT')}
+      </span>
+    </div>;
+  },
+
+  renderDonut () {
+    let donutData = [
+      { value: this.props.measure, fill: '#85bb43' },
+      { value: 100 - this.props.measure, fill: '#d4333f' }
+    ];
+    return <div className="overview-donut-chart">
+      <DonutChart width="90" height="90" thickness="3" data={donutData}/>
+      <div className="overview-detailed-measure-value">
+        <DrilldownLink component={this.props.component.key} metric={this.props.metric} period={this.props.period}>
+          {formatMeasure(this.props.measure, 'PERCENT')}
+        </DrilldownLink>
+      </div>
+    </div>;
+  },
+
+  render () {
+    if (this.props.measure == null) {
+      return null;
+    }
+
+    let className = classNames('overview-detailed-measure', {
+      'overview-leak': this.props.period
+    });
+
+    return <li className={className}>
+      <div className="overview-detailed-measure-nutshell space-between">
+        <span className="overview-detailed-measure-name">{localizeMetric(this.props.metric)}</span>
+        {this.renderDonut(this.props.measure)}
+      </div>
+      {this.renderLeak()}
+    </li>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/coverage-measures-list.js b/server/sonar-web/src/main/js/apps/overview/components/coverage-measures-list.js
new file mode 100644 (file)
index 0000000..898da4b
--- /dev/null
@@ -0,0 +1,123 @@
+import _ from 'underscore';
+import React from 'react';
+
+import { DetailedMeasure } from '../components/detailed-measure';
+import { filterMetricsForDomains } from '../helpers/metrics';
+import { CoverageMeasure } from '../components/coverage-measure';
+
+
+const TEST_DOMAINS = ['Tests', 'Tests (Integration)', 'Tests (Overall)'];
+
+const UT_COVERAGE_METRICS = ['coverage', 'line_coverage', 'branch_coverage'];
+const UT_NEW_COVERAGE_METRICS = ['new_coverage', 'new_line_coverage', 'new_branch_coverage'];
+
+const IT_COVERAGE_METRICS = ['it_coverage', 'it_line_coverage', 'it_branch_coverage'];
+const IT_NEW_COVERAGE_METRICS = ['new_it_coverage', 'new_it_line_coverage', 'new_it_branch_coverage'];
+
+const OVERALL_COVERAGE_METRICS = ['overall_coverage', 'overall_line_coverage', 'overall_branch_coverage'];
+const OVERALL_NEW_COVERAGE_METRICS = ['new_overall_coverage', 'new_overall_line_coverage',
+  'new_overall_branch_coverage'];
+
+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);
+
+
+export const CoverageMeasuresList = React.createClass({
+  renderOtherMeasures() {
+    let metrics = filterMetricsForDomains(this.props.metrics, TEST_DOMAINS)
+        .filter(metric => KNOWN_METRICS.indexOf(metric.key) === -1)
+        .map(metric => metric.key);
+    return this.renderListOfMeasures(metrics);
+  },
+
+  renderCoverage (metrics) {
+    let measures = metrics.map(metric => {
+      return <CoverageMeasure key={metric}
+                              metric={metric}
+                              measure={this.props.measures[metric]}
+                              leak={this.props.leak[metric]}
+                              component={this.props.component}/>;
+    });
+    return <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
+      {measures}
+    </div>;
+  },
+
+  shouldRenderTypedCoverage () {
+    return this.props.measures['coverage'] != null && this.props.measures['it_coverage'] != null;
+  },
+
+  renderTypedCoverage (metrics) {
+    return this.shouldRenderTypedCoverage() ? this.renderCoverage(metrics) : null;
+  },
+
+  renderNewCoverage (metrics) {
+    let measures = metrics.map(metric => {
+      return <CoverageMeasure key={metric}
+                              metric={metric}
+                              measure={this.props.leak[metric]}
+                              component={this.props.component}
+                              period={this.props.leakPeriodIndex}/>;
+    });
+    return <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
+      {measures}
+    </div>;
+  },
+
+  renderTypedNewCoverage (metrics) {
+    return this.shouldRenderTypedCoverage() ? this.renderNewCoverage(metrics) : null;
+  },
+
+  renderUTCoverage () {
+    return this.renderTypedCoverage(UT_COVERAGE_METRICS);
+  },
+
+  renderUTNewCoverage () {
+    return this.renderTypedNewCoverage(UT_NEW_COVERAGE_METRICS);
+  },
+
+  renderITCoverage () {
+    return this.renderTypedCoverage(IT_COVERAGE_METRICS);
+  },
+
+  renderITNewCoverage () {
+    return this.renderTypedNewCoverage(IT_NEW_COVERAGE_METRICS);
+  },
+
+  renderOverallCoverage () {
+    return this.renderCoverage(OVERALL_COVERAGE_METRICS);
+  },
+
+  renderOverallNewCoverage () {
+    return this.renderNewCoverage(OVERALL_NEW_COVERAGE_METRICS);
+  },
+
+  renderListOfMeasures(list) {
+    let metrics = list
+        .map(key => _.findWhere(this.props.metrics, { key }))
+        .map(metric => {
+          return <DetailedMeasure key={metric.key} {...this.props} {...this.props} metric={metric.key}
+                                  type={metric.type}/>;
+        });
+    return <div className="overview-detailed-measures-list">{metrics}</div>;
+  },
+
+  render () {
+    return <div>
+      {this.renderOverallCoverage()}
+      {this.renderOverallNewCoverage()}
+
+      {this.renderUTCoverage()}
+      {this.renderUTNewCoverage()}
+
+      {this.renderITCoverage()}
+      {this.renderITNewCoverage()}
+
+      {this.renderListOfMeasures(TEST_METRICS)}
+
+      {this.renderOtherMeasures()}
+    </div>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/detailed-measure.js b/server/sonar-web/src/main/js/apps/overview/components/detailed-measure.js
new file mode 100644 (file)
index 0000000..df0b8e2
--- /dev/null
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import { formatMeasure, formatMeasureVariation, localizeMetric } from '../../../helpers/measures';
+import { DrilldownLink } from '../../../components/shared/drilldown-link';
+import { getShortType } from '../helpers/metrics';
+
+
+export const DetailedMeasure = React.createClass({
+  renderLeak () {
+    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>;
+  },
+
+  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>
+        <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()}
+    </div>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/domain-bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/components/domain-bubble-chart.js
new file mode 100644 (file)
index 0000000..056863e
--- /dev/null
@@ -0,0 +1,125 @@
+import _ from 'underscore';
+import React from 'react';
+
+import { BubbleChart } from '../../../components/charts/bubble-chart';
+import { getComponentUrl } from '../../../helpers/urls';
+import { getFiles } from '../../../api/components';
+import { formatMeasure } from '../../../helpers/measures';
+
+
+const HEIGHT = 360;
+
+
+function getMeasure (component, metric) {
+  return component.measures[metric] || 0;
+}
+
+
+export class DomainBubbleChart extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      loading: true,
+      files: [],
+      xMetric: this.getMetricObject(props.metrics, props.xMetric),
+      yMetric: this.getMetricObject(props.metrics, props.yMetric),
+      sizeMetrics: props.sizeMetrics.map(this.getMetricObject.bind(null, props.metrics))
+    };
+  }
+
+  componentDidMount () {
+    this.requestFiles();
+  }
+
+  requestFiles () {
+    let metrics = [].concat(this.props.xMetric, this.props.yMetric, this.props.sizeMetrics);
+    return getFiles(this.props.component.key, 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 });
+    });
+  }
+
+  getMetricObject (metrics, metricKey) {
+    return _.findWhere(metrics, { key: metricKey });
+  }
+
+  getSizeMetricsValue (component) {
+    return this.props.sizeMetrics.reduce((previousValue, currentValue) => {
+      return previousValue + getMeasure(component, currentValue);
+    }, 0);
+  }
+
+  getSizeMetricsTitle () {
+    return this.state.sizeMetrics.map(metric => metric.name).join(' & ');
+  }
+
+  getTooltip (component) {
+    let sizeMetricsTitle = this.getSizeMetricsTitle();
+    let sizeMetricsType = this.state.sizeMetrics[0].type;
+
+    let inner = [
+      component.name,
+      `${this.state.xMetric.name}: ${formatMeasure(getMeasure(component, this.props.xMetric), this.state.xMetric.type)}`,
+      `${this.state.yMetric.name}: ${formatMeasure(getMeasure(component, this.props.yMetric), this.state.yMetric.type)}`,
+      `${sizeMetricsTitle}: ${formatMeasure(this.getSizeMetricsValue(component), sizeMetricsType)}`
+    ].join('<br>');
+    return `<div class="text-left">${inner}</div>`;
+  }
+
+  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, this.props.xMetric),
+        y: getMeasure(component, this.props.yMetric),
+        size: this.getSizeMetricsValue(component),
+        link: getComponentUrl(component.key),
+        tooltip: this.getTooltip(component)
+      };
+    });
+    let formatXTick = (tick) => formatMeasure(tick, this.state.xMetric.type);
+    let formatYTick = (tick) => formatMeasure(tick, this.state.yMetric.type);
+    return <BubbleChart items={items}
+                        height={HEIGHT}
+                        padding={[25, 30, 50, 60]}
+                        formatXTick={formatXTick}
+                        formatYTick={formatYTick}/>;
+  }
+
+  render () {
+    return <div className="overview-domain-chart">
+      <div className="overview-card-header">
+        <h2 className="overview-title">Project Files</h2>
+        <ul className="list-inline small">
+          <li>X: {this.state.xMetric.name}</li>
+          <li>Y: {this.state.yMetric.name}</li>
+          <li>Size: {this.getSizeMetricsTitle()}</li>
+        </ul>
+      </div>
+      <div className="overview-bubble-chart">
+        {this.renderBubbleChart()}
+      </div>
+    </div>;
+  }
+}
+
+DomainBubbleChart.propTypes = {
+  xMetric: React.PropTypes.string.isRequired,
+  yMetric: React.PropTypes.string.isRequired,
+  sizeMetrics: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
+};
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
new file mode 100644 (file)
index 0000000..62d4ab1
--- /dev/null
@@ -0,0 +1,202 @@
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { getTimeMachineData } from '../../../api/time-machine';
+import { getEvents } from '../../../api/events';
+import { formatMeasure, groupByDomain } from '../../../helpers/measures';
+import { getShortType } from '../helpers/metrics';
+import { Timeline } from './timeline-chart';
+
+
+const HEIGHT = 280;
+
+
+function parseValue (value, type) {
+  return type === 'RATING' && typeof value === 'string' ? value.charCodeAt(0) - 'A'.charCodeAt(0) + 1 : value;
+}
+
+
+export const DomainTimeline = React.createClass({
+  propTypes: {
+    allMetrics: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+    metrics: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+    initialMetric: React.PropTypes.string.isRequired
+  },
+
+  getInitialState() {
+    return {
+      loading: true,
+      currentMetric: this.props.initialMetric,
+      comparisonMetric: ''
+    };
+  },
+
+  componentDidMount () {
+    Promise.all([
+      this.requestTimeMachineData(this.state.currentMetric, this.state.comparisonMetric),
+      this.requestEvents()
+    ]).then(responses => {
+      this.setState({
+        loading: false,
+        snapshots: responses[0],
+        events: responses[1]
+      });
+    });
+  },
+
+  requestTimeMachineData (currentMetric, comparisonMetric) {
+    let metricsToRequest = [currentMetric];
+    if (comparisonMetric) {
+      metricsToRequest.push(comparisonMetric);
+    }
+    return getTimeMachineData(this.props.component.key, metricsToRequest.join()).then(r => {
+      return r[0].cells.map(cell => {
+        return { date: moment(cell.d).toDate(), values: cell.v };
+      });
+    });
+  },
+
+  requestEvents () {
+    return getEvents(this.props.component.key, 'Version').then(r => {
+      let events = r.map(event => {
+        return { version: event.n, date: moment(event.dt).toDate() };
+      });
+      return _.sortBy(events, 'date');
+    });
+  },
+
+  handleMetricChange (e) {
+    let newMetric = e.target.value,
+        comparisonMetric = this.state.comparisonMetric;
+    if (newMetric === comparisonMetric) {
+      comparisonMetric = '';
+    }
+    this.requestTimeMachineData(newMetric, comparisonMetric).then(snapshots => {
+      this.setState({ currentMetric: newMetric, comparisonMetric: comparisonMetric, snapshots });
+    });
+  },
+
+  handleComparisonMetricChange (e) {
+    let newMetric = e.target.value;
+    this.requestTimeMachineData(this.state.currentMetric, newMetric).then(snapshots => {
+      this.setState({ comparisonMetric: newMetric, snapshots });
+    });
+  },
+
+  groupMetricsByDomain () {
+    return groupByDomain(this.props.metrics);
+  },
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  },
+
+  renderWhenNoHistoricalData () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      There is no historical data.
+    </div>;
+  },
+
+  renderLineCharts () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+    return <div>
+      {this.renderLineChart(this.state.snapshots, this.state.currentMetric, 0)}
+      {this.renderLineChart(this.state.snapshots, this.state.comparisonMetric, 1)}
+    </div>;
+  },
+
+  renderLineChart (snapshots, metric, index) {
+    if (!metric) {
+      return null;
+    }
+
+    if (snapshots.length < 2) {
+      return this.renderWhenNoHistoricalData();
+    }
+
+    let metricType = _.findWhere(this.props.allMetrics, { key: metric }).type;
+    let data = snapshots.map(snapshot => {
+      return {
+        x: snapshot.date,
+        y: parseValue(snapshot.values[index], metricType)
+      };
+    });
+
+    let formatValue = (value) => formatMeasure(value, metricType);
+    let formatYTick = (tick) => formatMeasure(tick, getShortType(metricType));
+
+    return <div className={'overview-timeline-' + index}>
+      <Timeline key={metric}
+                data={data}
+                events={this.state.events}
+                height={HEIGHT}
+                interpolate="linear"
+                formatValue={formatValue}
+                formatYTick={formatYTick}
+                leakPeriodDate={this.props.leakPeriodDate}
+                padding={[25, 25, 25, 60]}/>
+    </div>;
+  },
+
+  renderMetricOption (metric) {
+    return <option key={metric.key} value={metric.key}>{metric.name}</option>;
+  },
+
+  renderMetricOptions (metrics) {
+    let groupedMetrics = groupByDomain(metrics);
+    return groupedMetrics.map(metricGroup => {
+      let options = metricGroup.metrics.map(this.renderMetricOption);
+      return <optgroup key={metricGroup.domain} label={metricGroup.domain}>{options}</optgroup>;
+    });
+  },
+
+  renderTimelineMetricSelect () {
+    if (this.state.loading) {
+      return null;
+    }
+    return <span>
+      <span className="overview-timeline-sample overview-timeline-sample-0"/>
+      <select ref="metricSelect"
+              className="overview-timeline-select"
+              onChange={this.handleMetricChange}
+              value={this.state.currentMetric}>{this.renderMetricOptions(this.props.metrics)}</select>
+    </span>;
+  },
+
+  renderComparisonMetricSelect () {
+    if (this.state.loading) {
+      return null;
+    }
+    let metrics = this.props.allMetrics.filter(metric => metric.key !== this.state.currentMetric);
+    return <span>
+      {this.state.comparisonMetric ? <span className="overview-timeline-sample overview-timeline-sample-1"/> : null}
+      <select ref="comparisonMetricSelect"
+              className="overview-timeline-select"
+              onChange={this.handleComparisonMetricChange}
+              value={this.state.comparisonMetric}>
+        <option value="">Compare with...</option>
+        {this.renderMetricOptions(metrics)}
+      </select>
+    </span>;
+  },
+
+  render () {
+    return <div className="overview-domain-chart">
+      <div className="overview-card-header">
+        <div>
+          <h2 className="overview-title">Timeline</h2>
+          {this.renderTimelineMetricSelect()}
+        </div>
+        {this.renderComparisonMetricSelect()}
+      </div>
+      <div className="overview-timeline">
+        {this.renderLineCharts()}
+      </div>
+    </div>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/domain-treemap.js b/server/sonar-web/src/main/js/apps/overview/components/domain-treemap.js
new file mode 100644 (file)
index 0000000..59ee367
--- /dev/null
@@ -0,0 +1,102 @@
+import _ from 'underscore';
+import React from 'react';
+
+import { Treemap } from '../../../components/charts/treemap';
+import { getChildren } from '../../../api/components';
+import { formatMeasure } from '../../../helpers/measures';
+
+
+const HEIGHT = 302;
+
+
+export class DomainTreemap extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      loading: true,
+      files: [],
+      sizeMetric: this.getMetricObject(props.metrics, props.sizeMetric),
+      colorMetric: props.colorMetric ? this.getMetricObject(props.metrics, props.colorMetric) : null
+    };
+  }
+
+  componentDidMount () {
+    this.requestComponents();
+  }
+
+  requestComponents () {
+    let metrics = [this.props.sizeMetric, this.props.colorMetric];
+    return getChildren(this.props.component.key, 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 });
+    });
+  }
+
+  getMetricObject (metrics, metricKey) {
+    return _.findWhere(metrics, { key: metricKey });
+  }
+
+  getTooltip (component) {
+    let inner = [
+      component.name,
+      `${this.state.sizeMetric.name}: ${formatMeasure(component.measures[this.props.sizeMetric], this.state.sizeMetric.type)}`
+    ];
+    if (this.state.colorMetric) {
+      inner.push(`${this.state.colorMetric.name}: ${formatMeasure(component.measures[this.props.colorMetric], this.state.colorMetric.type)}`);
+    }
+    inner = inner.join('<br>');
+    return `<div class="text-left">${inner}</div>`;
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderTreemap () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    // TODO filter out zero sized components
+    let items = this.state.components.map(component => {
+      let colorMeasure = this.props.colorMetric ? component.measures[this.props.colorMetric] : null;
+      return {
+        size: component.measures[this.props.sizeMetric],
+        color: colorMeasure != null ? this.props.scale(colorMeasure) : '#777',
+        tooltip: this.getTooltip(component),
+        label: component.name
+      };
+    });
+    return <Treemap items={items} height={HEIGHT}/>;
+  }
+
+  render () {
+    let color = this.props.colorMetric ? <li>Color: {this.state.colorMetric.name}</li> : null;
+    return <div className="overview-domain-chart">
+      <div className="overview-card-header">
+        <h2 className="overview-title">Treemap</h2>
+        <ul className="list-inline small">
+          <li>Size: {this.state.sizeMetric.name}</li>
+          {color}
+        </ul>
+      </div>
+      <div className="overview-treemap">
+        {this.renderTreemap()}
+      </div>
+    </div>;
+  }
+}
+
+DomainTreemap.propTypes = {
+  sizeMetric: React.PropTypes.string.isRequired,
+  colorMetric: React.PropTypes.string,
+  scale: React.PropTypes.func
+};
diff --git a/server/sonar-web/src/main/js/apps/overview/components/issue-measure.js b/server/sonar-web/src/main/js/apps/overview/components/issue-measure.js
new file mode 100644 (file)
index 0000000..8597f15
--- /dev/null
@@ -0,0 +1,276 @@
+import moment from 'moment';
+import React from 'react';
+
+import { formatMeasure, localizeMetric } from '../../../helpers/measures';
+import { DrilldownLink } from '../../../components/shared/drilldown-link';
+import { IssuesLink } from '../../../components/shared/issues-link';
+import { getShortType } from '../helpers/metrics';
+import SeverityHelper from '../../../components/shared/severity-helper';
+
+
+export const IssueMeasure = React.createClass({
+  renderLeak () {
+    if (!this.props.leakPeriodDate) {
+      return null;
+    }
+
+    let leak = this.props.leak[this.props.metric];
+    let added = this.props.leak[this.props.leakMetric];
+    let removed = added - leak;
+
+    return <div className="overview-detailed-measure-leak">
+      <ul className="list-inline">
+        <li className="text-danger">
+          <IssuesLink className="text-danger overview-detailed-measure-value"
+                      component={this.props.component.key} params={{ resolved: 'false' }}>
+            +{formatMeasure(added, getShortType(this.props.type))}
+          </IssuesLink>
+        </li>
+        <li className="text-success">
+          <span className="text-success overview-detailed-measure-value">
+            -{formatMeasure(removed, getShortType(this.props.type))}
+          </span>
+        </li>
+      </ul>
+    </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>
+        <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()}
+    </div>;
+  }
+});
+
+
+export const AddedRemovedMeasure = React.createClass({
+  renderLeak () {
+    if (!this.props.leakPeriodDate) {
+      return null;
+    }
+
+    let leak = this.props.leak[this.props.metric];
+    let added = this.props.leak[this.props.leakMetric];
+    let removed = added - leak;
+
+    return <div className="overview-detailed-measure-leak">
+      <ul>
+        <li style={{ display: 'flex', alignItems: 'baseline' }}>
+          <small className="flex-1 text-left">Added</small>
+          <IssuesLink className="text-danger"
+                      component={this.props.component.key} params={{ resolved: 'false' }}>
+            <span className="overview-detailed-measure-value">
+              {formatMeasure(added, getShortType(this.props.type))}
+            </span>
+          </IssuesLink>
+        </li>
+        <li className="little-spacer-top" style={{ display: 'flex', alignItems: 'baseline' }}>
+          <small className="flex-1 text-left">Removed</small>
+          <span className="text-success">
+            {formatMeasure(removed, getShortType(this.props.type))}
+          </span>
+        </li>
+      </ul>
+    </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>
+        <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()}
+    </div>;
+  }
+});
+
+
+export const AddedRemovedDebt = React.createClass({
+  renderLeak () {
+    if (!this.props.leakPeriodDate) {
+      return null;
+    }
+
+    let leak = this.props.leak[this.props.metric];
+    let added = this.props.leak[this.props.leakMetric];
+    let removed = added - leak;
+
+    return <div className="overview-detailed-measure-leak">
+      <ul>
+        <li style={{ display: 'flex', alignItems: 'baseline' }}>
+          <small className="flex-1 text-left">Added</small>
+          <DrilldownLink className="text-danger" component={this.props.component.key} metric={this.props.leakMetric}
+                         period={this.props.leakPeriodIndex}>
+            <span className="overview-detailed-measure-value">
+              {formatMeasure(added, getShortType(this.props.type))}
+            </span>
+          </DrilldownLink>
+        </li>
+        <li className="little-spacer-top" style={{ display: 'flex', alignItems: 'baseline' }}>
+          <small className="flex-1 text-left">Removed</small>
+          <span className="text-success">
+            {formatMeasure(removed, getShortType(this.props.type))}
+          </span>
+        </li>
+      </ul>
+    </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>
+        <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()}
+    </div>;
+  }
+});
+
+
+export const OnNewCodeMeasure = React.createClass({
+  renderLeak () {
+    if (!this.props.leakPeriodDate) {
+      return null;
+    }
+
+    let onNewCode = this.props.leak[this.props.leakMetric];
+
+    return <div className="overview-detailed-measure-leak">
+      <ul>
+        <li className="little-spacer-top" style={{ display: 'flex', alignItems: 'center' }}>
+          <small className="flex-1 text-left">On New Code</small>
+          <DrilldownLink component={this.props.component.key} metric={this.props.leakMetric}
+                         period={this.props.leakPeriodIndex}>
+            <span className="overview-detailed-measure-value">
+              {formatMeasure(onNewCode, getShortType(this.props.type))}
+            </span>
+          </DrilldownLink>
+        </li>
+      </ul>
+    </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>
+        <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()}
+    </div>;
+  }
+});
+
+
+export const SeverityMeasure = React.createClass({
+  getMetric () {
+    return this.props.severity.toLowerCase() + '_violations';
+  },
+
+  getNewMetric () {
+    return 'new_' + this.props.severity.toLowerCase() + '_violations';
+  },
+
+
+  renderLeak () {
+    if (!this.props.leakPeriodDate) {
+      return null;
+    }
+
+    let leak = this.props.leak[this.getMetric()];
+    let added = this.props.leak[this.getNewMetric()];
+    let removed = added - leak;
+
+    let createdAfter = moment(this.props.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+    return <div className="overview-detailed-measure-leak">
+      <ul>
+        <li style={{ display: 'flex', alignItems: 'baseline' }}>
+          <small className="flex-1 text-left">Added</small>
+          <IssuesLink className="text-danger"
+                      component={this.props.component.key}
+                      params={{ resolved: 'false', severities: this.props.severity, createdAfter: createdAfter }}>
+            <span className="overview-detailed-measure-value">
+              {formatMeasure(added, 'SHORT_INT')}
+            </span>
+          </IssuesLink>
+        </li>
+        <li className="little-spacer-top" style={{ display: 'flex', alignItems: 'baseline' }}>
+          <small className="flex-1 text-left">Removed</small>
+          <span className="text-success">
+            {formatMeasure(removed, 'SHORT_INT')}
+          </span>
+        </li>
+      </ul>
+    </div>;
+  },
+
+  render () {
+    let measure = this.props.measures[this.getMetric()];
+    if (measure == null) {
+      return null;
+    }
+
+    return <div className="overview-detailed-measure">
+      <div className="overview-detailed-measure-nutshell">
+        <span className="overview-detailed-measure-name">
+          <SeverityHelper severity={this.props.severity}/>
+        </span>
+        <span className="overview-detailed-measure-value">
+          <IssuesLink component={this.props.component.key}
+                      params={{ resolved: 'false', severities: this.props.severity }}>
+            {formatMeasure(measure, 'SHORT_INT')}
+          </IssuesLink>
+        </span>
+        {this.props.children}
+      </div>
+      {this.renderLeak()}
+    </div>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/issues-assignees.js b/server/sonar-web/src/main/js/apps/overview/components/issues-assignees.js
new file mode 100644 (file)
index 0000000..126830c
--- /dev/null
@@ -0,0 +1,31 @@
+import React from 'react';
+import Assignee from '../../../components/shared/assignee-helper';
+import { getComponentIssuesUrl } from '../../../helpers/urls';
+import { formatMeasure } from '../../../helpers/measures';
+
+
+export default class extends React.Component {
+  render () {
+    let rows = this.props.assignees.map(s => {
+      let params = { statuses: 'OPEN,REOPENED' };
+      if (s.val) {
+        params.assignees = s.val;
+      } else {
+        params.assigned = 'false';
+      }
+      let href = getComponentIssuesUrl(this.props.component.key, params);
+      return <tr key={s.val}>
+        <td>
+          <Assignee user={s.user}/>
+        </td>
+        <td className="thin text-right">
+          <a href={href}>{formatMeasure(s.count, 'SHORT_INT')}</a>
+        </td>
+      </tr>;
+    });
+
+    return <table className="data zebra">
+      <tbody>{rows}</tbody>
+    </table>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/issues-tags.js b/server/sonar-web/src/main/js/apps/overview/components/issues-tags.js
new file mode 100644 (file)
index 0000000..733ac50
--- /dev/null
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { WordCloud } from '../../../components/charts/word-cloud';
+import { getComponentIssuesUrl } from '../../../helpers/urls';
+import { formatMeasure } from '../../../helpers/measures';
+
+
+export const IssuesTags = React.createClass({
+  render () {
+    let tags = this.props.tags.map(tag => {
+      let link = getComponentIssuesUrl(this.props.component.key, { resolved: 'false', tags: tag.val });
+      let tooltip = `Issues: ${formatMeasure(tag.count, 'SHORT_INT')}`;
+      return { text: tag.val, size: tag.count, link, tooltip };
+    });
+    return <WordCloud items={tags}/>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/language-distribution.js b/server/sonar-web/src/main/js/apps/overview/components/language-distribution.js
new file mode 100644 (file)
index 0000000..732db2e
--- /dev/null
@@ -0,0 +1,57 @@
+import _ from 'underscore';
+import React from 'react';
+
+import { Histogram } from '../../../components/charts/histogram';
+import { formatMeasure } from '../../../helpers/measures';
+import { getLanguages } from '../../../api/languages';
+
+
+export const LanguageDistribution = React.createClass({
+  propTypes: {
+    distribution: React.PropTypes.string.isRequired,
+    lines: React.PropTypes.number.isRequired
+  },
+
+  componentDidMount () {
+    this.requestLanguages();
+  },
+
+  requestLanguages () {
+    getLanguages().then(languages => this.setState({ languages }));
+  },
+
+  getLanguageName (langKey) {
+    if (this.state && this.state.languages) {
+      let lang = _.findWhere(this.state.languages, { key: langKey });
+      return lang ? lang.name : window.t('unknown');
+    } else {
+      return langKey;
+    }
+  },
+
+  renderBarChart () {
+    let data = this.props.distribution.split(';').map((point, index) => {
+      let tokens = point.split('=');
+      return { x: parseInt(tokens[1], 10), y: index, value: tokens[0] };
+    });
+
+    data = _.sortBy(data, d => -d.x);
+
+    let yTicks = data.map(point => this.getLanguageName(point.value));
+
+    let yValues = data.map(point => formatMeasure(point.x / this.props.lines * 100, 'PERCENT'));
+
+    return <Histogram data={data}
+                      yTicks={yTicks}
+                      yValues={yValues}
+                      height={data.length * 25}
+                      barsWidth={10}
+                      padding={[0, 50, 0, 80]}/>;
+  },
+
+  render () {
+    return <div className="overview-bar-chart">
+      {this.renderBarChart()}
+    </div>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/components/legend.js b/server/sonar-web/src/main/js/apps/overview/components/legend.js
new file mode 100644 (file)
index 0000000..1c38b7d
--- /dev/null
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { DomainLeakTitle } from '../main/components';
+
+
+export const Legend = React.createClass({
+  render() {
+    if (!this.props.leakPeriodDate) {
+      return null;
+    }
+    return <div className="overview-legend">
+      <span className="overview-legend-leak"/>
+      <DomainLeakTitle label={this.props.leakPeriodLabel} date={this.props.leakPeriodDate}/>
+    </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
new file mode 100644 (file)
index 0000000..fc61b19
--- /dev/null
@@ -0,0 +1,132 @@
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { ResizeMixin } from '../../../components/mixins/resize-mixin';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+
+
+export const Timeline = React.createClass({
+  mixins: [ResizeMixin, TooltipsMixin],
+
+  propTypes: {
+    data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+    padding: React.PropTypes.arrayOf(React.PropTypes.number),
+    height: React.PropTypes.number,
+    interpolate: React.PropTypes.string
+  },
+
+  getDefaultProps() {
+    return {
+      padding: [10, 10, 10, 10],
+      interpolate: 'basis'
+    };
+  },
+
+  getInitialState() {
+    return { width: this.props.width, height: this.props.height };
+  },
+
+  renderHorizontalGrid (xScale, yScale) {
+    let ticks = yScale.ticks(4);
+    let grid = ticks.map(tick => {
+      let opts = {
+        x: xScale.range()[0],
+        y: yScale(tick)
+      };
+      return <g key={tick}>
+        <text className="line-chart-tick line-chart-tick-x" dx="-1em" dy="0.3em"
+              textAnchor="end" {...opts}>{this.props.formatYTick(tick)}</text>
+        <line className="line-chart-grid"
+              x1={xScale.range()[0]}
+              x2={xScale.range()[1]}
+              y1={yScale(tick)}
+              y2={yScale(tick)}/>
+      </g>;
+    });
+    return <g>{grid}</g>;
+  },
+
+  renderTicks (xScale, yScale) {
+    let format = xScale.tickFormat(7);
+    let ticks = xScale.ticks(7);
+    ticks = _.initial(ticks).map((tick, index) => {
+      let nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
+      let x = (xScale(tick) + xScale(nextTick)) / 2;
+      let y = yScale.range()[0];
+      return <text key={index} className="line-chart-tick" x={x} y={y} dy="1.5em">{format(tick)}</text>;
+    });
+    return <g>{ticks}</g>;
+  },
+
+  renderLeak (xScale, yScale) {
+    if (!this.props.leakPeriodDate) {
+      return null;
+    }
+    let opts = {
+      x: xScale(this.props.leakPeriodDate),
+      y: yScale.range()[1],
+      width: xScale.range()[1] - xScale(this.props.leakPeriodDate),
+      height: yScale.range()[0] - yScale.range()[1],
+      fill: '#fffae7'
+    };
+    return <rect {...opts}/>;
+  },
+
+  renderLine (xScale, yScale) {
+    let path = d3.svg.line()
+        .x(d => xScale(d.x))
+        .y(d => yScale(d.y))
+        .interpolate(this.props.interpolate);
+    return <path className="line-chart-path" d={path(this.props.data)}/>;
+  },
+
+  renderEvents(xScale, yScale) {
+    let points = this.props.events
+        .map(event => {
+          let snapshot = this.props.data.find(d => d.x.getTime() === event.date.getTime());
+          return _.extend(event, { snapshot });
+        })
+        .filter(event => event.snapshot)
+        .map(event => {
+          let key = `${event.date.getTime()}-${event.snapshot.y}`;
+          let tooltip = [
+            `<span class="nowrap">${event.version}</span>`,
+            `<span class="nowrap">${moment(event.date).format('LL')}</span>`,
+            `<span class="nowrap">${event.snapshot.y ? this.props.formatValue(event.snapshot.y) : '—'}</span>`
+          ].join('<br>');
+          return <circle key={key} className="line-chart-point"
+                         r="4" cx={xScale(event.snapshot.x)} cy={yScale(event.snapshot.y)}
+                         data-toggle="tooltip" data-title={tooltip}/>;
+        });
+    return <g>{points}</g>;
+  },
+
+  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 xScale = d3.time.scale()
+        .domain(d3.extent(this.props.data, d => d.x))
+        .range([0, availableWidth])
+        .clamp(true);
+    let yScale = d3.scale.linear()
+        .range([availableHeight, 0])
+        .domain([0, d3.max(this.props.data, d => d.y)])
+        .nice();
+
+    return <svg className="line-chart" width={this.state.width} height={this.state.height}>
+      <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+        {this.renderLeak(xScale, yScale)}
+        {this.renderHorizontalGrid(xScale, yScale)}
+        {this.renderTicks(xScale, yScale)}
+        {this.renderLine(xScale, yScale)}
+        {this.renderEvents(xScale, yScale)}
+      </g>
+    </svg>;
+  }
+});
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
deleted file mode 100644 (file)
index 00821bd..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-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.type !== '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>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/coverage/main.js b/server/sonar-web/src/main/js/apps/overview/coverage/main.js
deleted file mode 100644 (file)
index 1c5c62d..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-import _ from 'underscore';
-import d3 from 'd3';
-import React from 'react';
-
-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';
-
-
-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);
-
-
-export const CoverageMain = 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 => ['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 overview-domain-fixed-width">
-          <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>
-
-      <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/domain/bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/domain/bubble-chart.js
deleted file mode 100644 (file)
index e112d8a..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-import _ from 'underscore';
-import React from 'react';
-import { BubbleChart } from '../../../components/charts/bubble-chart';
-import { getComponentUrl } from '../../../helpers/urls';
-import { getFiles } from '../../../api/components';
-import { formatMeasure } from '../../../helpers/measures';
-
-
-const HEIGHT = 360;
-
-
-function getMeasure (component, metric) {
-  return component.measures[metric] || 0;
-}
-
-
-export class DomainBubbleChart extends React.Component {
-  constructor (props) {
-    super(props);
-    this.state = {
-      loading: true,
-      files: [],
-      xMetric: this.getMetricObject(props.metrics, props.xMetric),
-      yMetric: this.getMetricObject(props.metrics, props.yMetric),
-      sizeMetrics: props.sizeMetrics.map(this.getMetricObject.bind(null, props.metrics))
-    };
-  }
-
-  componentDidMount () {
-    this.requestFiles();
-  }
-
-  requestFiles () {
-    let metrics = [].concat(this.props.xMetric, this.props.yMetric, this.props.sizeMetrics);
-    return getFiles(this.props.component.key, 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 });
-    });
-  }
-
-  getMetricObject (metrics, metricKey) {
-    return _.findWhere(metrics, { key: metricKey });
-  }
-
-  getSizeMetricsValue (component) {
-    return this.props.sizeMetrics.reduce((previousValue, currentValue) => {
-      return previousValue + getMeasure(component, currentValue);
-    }, 0);
-  }
-
-  getSizeMetricsTitle () {
-    return this.state.sizeMetrics.map(metric => metric.name).join(' & ');
-  }
-
-  getTooltip (component) {
-    let sizeMetricsTitle = this.getSizeMetricsTitle();
-    let sizeMetricsType = this.state.sizeMetrics[0].type;
-
-    let inner = [
-      component.name,
-      `${this.state.xMetric.name}: ${formatMeasure(getMeasure(component, this.props.xMetric), this.state.xMetric.type)}`,
-      `${this.state.yMetric.name}: ${formatMeasure(getMeasure(component, this.props.yMetric), this.state.yMetric.type)}`,
-      `${sizeMetricsTitle}: ${formatMeasure(this.getSizeMetricsValue(component), sizeMetricsType)}`
-    ].join('<br>');
-    return `<div class="text-left">${inner}</div>`;
-  }
-
-  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, this.props.xMetric),
-        y: getMeasure(component, this.props.yMetric),
-        size: this.getSizeMetricsValue(component),
-        link: getComponentUrl(component.key),
-        tooltip: this.getTooltip(component)
-      };
-    });
-    let formatXTick = (tick) => formatMeasure(tick, this.state.xMetric.type);
-    let formatYTick = (tick) => formatMeasure(tick, this.state.yMetric.type);
-    return <BubbleChart items={items}
-                        height={HEIGHT}
-                        padding={[25, 30, 50, 60]}
-                        formatXTick={formatXTick}
-                        formatYTick={formatYTick}/>;
-  }
-
-  render () {
-    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">
-          <li>X: {this.state.xMetric.name}</li>
-          <li>Y: {this.state.yMetric.name}</li>
-          <li>Size: {this.getSizeMetricsTitle()}</li>
-        </ul>
-      </div>
-      <div className="overview-bubble-chart">
-        {this.renderBubbleChart()}
-      </div>
-    </div>;
-  }
-}
-
-DomainBubbleChart.propTypes = {
-  xMetric: React.PropTypes.string.isRequired,
-  yMetric: React.PropTypes.string.isRequired,
-  sizeMetrics: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
-};
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/header.js b/server/sonar-web/src/main/js/apps/overview/domain/header.js
deleted file mode 100644 (file)
index 16af08d..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react';
-
-export class DomainHeader extends React.Component {
-  render () {
-    return <h2 className="overview-title">{this.props.title}</h2>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js b/server/sonar-web/src/main/js/apps/overview/domain/measures-list.js
deleted file mode 100644 (file)
index d37d4b9..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-import _ from 'underscore';
-import React from 'react';
-
-import DrilldownLink from '../helpers/drilldown-link';
-import { getMeasures } from '../../../api/measures';
-import { formatMeasure } from '../../../helpers/measures';
-
-
-export class DomainMeasuresList extends React.Component {
-  constructor () {
-    super();
-    this.state = { measures: {} };
-  }
-
-  componentDidMount () {
-    this.requestDetails();
-  }
-
-  requestDetails () {
-    return getMeasures(this.props.component.key, this.props.metricsToDisplay).then(measures => {
-      this.setState({ measures });
-    });
-  }
-
-  getMetricObject (metricKey) {
-    return _.findWhere(this.props.metrics, { key: metricKey });
-  }
-
-  renderValue (value, metricKey, metricType) {
-    if (value != null) {
-      return <DrilldownLink component={this.props.component.key} metric={metricKey}>
-        {formatMeasure(value, metricType)}
-      </DrilldownLink>;
-    } else {
-      return '—';
-    }
-  }
-
-  render () {
-    let rows = this.props.metricsToDisplay.map(metric => {
-      let metricObject = this.getMetricObject(metric);
-      return <tr key={metric}>
-        <td>{metricObject.name}</td>
-        <td className="thin nowrap text-right">
-          {this.renderValue(this.state.measures[metric], metric, metricObject.type)}
-        </td>
-      </tr>;
-    });
-    return <table className="data zebra">
-      <tbody>{rows}</tbody>
-    </table>;
-  }
-}
-
-DomainMeasuresList.propTypes = {
-  metricsToDisplay: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
-};
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/timeline.js b/server/sonar-web/src/main/js/apps/overview/domain/timeline.js
deleted file mode 100644 (file)
index 9e5f5f8..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/server/sonar-web/src/main/js/apps/overview/domain/treemap.js b/server/sonar-web/src/main/js/apps/overview/domain/treemap.js
deleted file mode 100644 (file)
index e22345f..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-import _ from 'underscore';
-import React from 'react';
-
-import { Treemap } from '../../../components/charts/treemap';
-import { getChildren } from '../../../api/components';
-import { formatMeasure } from '../../../helpers/measures';
-
-const HEIGHT = 302;
-
-
-export class DomainTreemap extends React.Component {
-  constructor (props) {
-    super(props);
-    this.state = {
-      loading: true,
-      files: [],
-      sizeMetric: this.getMetricObject(props.metrics, props.sizeMetric),
-      colorMetric: props.colorMetric ? this.getMetricObject(props.metrics, props.colorMetric) : null
-    };
-  }
-
-  componentDidMount () {
-    this.requestComponents();
-  }
-
-  requestComponents () {
-    let metrics = [this.props.sizeMetric, this.props.colorMetric];
-    return getChildren(this.props.component.key, 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 });
-    });
-  }
-
-  getMetricObject (metrics, metricKey) {
-    return _.findWhere(metrics, { key: metricKey });
-  }
-
-  getTooltip (component) {
-    let inner = [
-      component.name,
-      `${this.state.sizeMetric.name}: ${formatMeasure(component.measures[this.props.sizeMetric], this.state.sizeMetric.type)}`
-    ];
-    if (this.state.colorMetric) {
-      inner.push(`${this.state.colorMetric.name}: ${formatMeasure(component.measures[this.props.colorMetric], this.state.colorMetric.type)}`);
-    }
-    inner = inner.join('<br>');
-    return `<div class="text-left">${inner}</div>`;
-  }
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  }
-
-  renderTreemap () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-
-    // TODO filter out zero sized components
-    let items = this.state.components.map(component => {
-      let colorMeasure = this.props.colorMetric ? component.measures[this.props.colorMetric] : null;
-      return {
-        size: component.measures[this.props.sizeMetric],
-        color: colorMeasure != null ? this.props.scale(colorMeasure) : '#777',
-        tooltip: this.getTooltip(component),
-        label: component.name
-      };
-    });
-    return <Treemap items={items} height={HEIGHT}/>;
-  }
-
-  render () {
-    let color = this.props.colorMetric ? <li>Color: {this.state.colorMetric.name}</li> : null;
-    return <div className="overview-domain overview-domain-chart">
-      <div className="overview-domain-header">
-        <h2 className="overview-title">Treemap</h2>
-        <ul className="list-inline small">
-          <li>Size: {this.state.sizeMetric.name}</li>
-          {color}
-        </ul>
-      </div>
-      <div className="overview-treemap">
-        {this.renderTreemap()}
-      </div>
-    </div>;
-  }
-}
-
-DomainTreemap.propTypes = {
-  sizeMetric: React.PropTypes.string.isRequired,
-  colorMetric: React.PropTypes.string,
-  scale: React.PropTypes.func
-};
diff --git a/server/sonar-web/src/main/js/apps/overview/domains/coverage-domain.js b/server/sonar-web/src/main/js/apps/overview/domains/coverage-domain.js
new file mode 100644 (file)
index 0000000..6df04be
--- /dev/null
@@ -0,0 +1,119 @@
+import d3 from 'd3';
+import React from 'react';
+
+import { getMeasuresAndVariations } from '../../../api/measures';
+import { DomainTimeline } from '../components/domain-timeline';
+import { DomainTreemap } from '../components/domain-treemap';
+import { DomainBubbleChart } from '../components/domain-bubble-chart';
+import { getPeriodLabel, getPeriodDate } from './../helpers/periods';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { filterMetrics, filterMetricsForDomains } from '../helpers/metrics';
+import { Legend } from '../components/legend';
+import { CHART_COLORS_RANGE_PERCENT } from '../../../helpers/constants';
+import { CoverageMeasuresList } from '../components/coverage-measures-list';
+
+
+const TEST_DOMAINS = ['Tests', 'Tests (Integration)', 'Tests (Overall)'];
+
+
+export const CoverageMain = 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 => TEST_DOMAINS.indexOf(metric.domain) !== -1)
+        .map(metric => metric.key);
+  },
+
+  getMetricsForTimeline() {
+    return filterMetricsForDomains(this.props.metrics, TEST_DOMAINS);
+  },
+
+  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}/>;
+  },
+
+  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-cards-list">
+        <div className="overview-card overview-card-fixed-width">
+          <div className="overview-card-header">
+            <div className="overview-title">Coverage Overview</div>
+            {this.renderLegend()}
+          </div>
+          <CoverageMeasuresList {...this.props} {...this.state}/>
+        </div>
+
+        <div className="overview-card">
+          <DomainBubbleChart {...this.props}
+              xMetric="complexity"
+              yMetric="overall_coverage"
+              sizeMetrics={['overall_uncovered_lines']}/>
+        </div>
+      </div>
+
+      <div className="overview-cards-list">
+        <div className="overview-card">
+          <DomainTimeline {...this.props} {...this.state}
+              initialMetric="overall_coverage"
+              metrics={this.getMetricsForTimeline()}
+              allMetrics={this.getAllMetricsForTimeline()}/>
+        </div>
+
+        <div className="overview-card">
+          <DomainTreemap {...this.props}
+              sizeMetric="ncloc"
+              colorMetric="overall_coverage"
+              scale={treemapScale}/>
+        </div>
+      </div>
+    </div>;
+
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/domains/debt-domain.js b/server/sonar-web/src/main/js/apps/overview/domains/debt-domain.js
new file mode 100644 (file)
index 0000000..bf7a73b
--- /dev/null
@@ -0,0 +1,200 @@
+import _ from 'underscore';
+import d3 from 'd3';
+import React from 'react';
+
+import { getMeasuresAndVariations } from '../../../api/measures';
+import { DetailedMeasure } from '../components/detailed-measure';
+import { DomainTimeline } from '../components/domain-timeline';
+import { DomainTreemap } from '../components/domain-treemap';
+import { DomainBubbleChart } from '../components/domain-bubble-chart';
+import { getPeriodLabel, getPeriodDate } from './../helpers/periods';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { filterMetrics, filterMetricsForDomains } from '../helpers/metrics';
+import { Legend } from '../components/legend';
+import { CHART_COLORS_RANGE_PERCENT } from '../../../helpers/constants';
+import { AddedRemovedMeasure, AddedRemovedDebt, OnNewCodeMeasure, SeverityMeasure } from './../components/issue-measure';
+import { IssuesTags } from './../components/issues-tags';
+import Assignees from './../components/issues-assignees';
+import { getFacets, extractAssignees } from '../../../api/issues';
+import StatusHelper from '../../../components/shared/status-helper';
+import { Rating } from '../../../components/shared/rating';
+import { DrilldownLink } from '../../../components/shared/drilldown-link';
+
+
+const KNOWN_METRICS = ['violations', 'sqale_index', 'sqale_rating', 'sqale_debt_ratio', 'blocker_violations',
+  'critical_violations', 'major_violations', 'minor_violations', 'info_violations', 'confirmed_issues'];
+
+
+export const IssuesMain = 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() {
+    Promise.all([
+      this.requestMeasures(),
+      this.requestIssues()
+    ]).then(responses => {
+      let measures = this.getMeasuresValues(responses[0], 'value');
+      let leak = this.getMeasuresValues(responses[0], 'var' + this.props.leakPeriodIndex);
+      let tags = this.getFacet(responses[1].facets, 'tags');
+      let assignees = extractAssignees(this.getFacet(responses[1].facets, 'assignees'), responses[1].response);
+      this.setState({ ready: true, measures, leak, tags, assignees });
+    });
+  },
+
+  getMeasuresValues (measures, fieldKey) {
+    let values = {};
+    Object.keys(measures).forEach(measureKey => {
+      values[measureKey] = measures[measureKey][fieldKey];
+    });
+    return values;
+  },
+
+  getMetricsForDomain() {
+    return this.props.metrics
+        .filter(metric => ['Issues', 'Technical Debt'].indexOf(metric.domain) !== -1)
+        .map(metric => metric.key);
+  },
+
+  getMetricsForTimeline() {
+    return filterMetricsForDomains(this.props.metrics, ['Issues', 'Technical Debt']);
+  },
+
+  getAllMetricsForTimeline() {
+    return filterMetrics(this.props.metrics);
+  },
+
+  requestMeasures () {
+    return getMeasuresAndVariations(this.props.component.key, this.getMetricsForDomain());
+  },
+
+  getFacet (facets, facetKey) {
+    return _.findWhere(facets, { property: facetKey }).values;
+  },
+
+  requestIssues () {
+    return getFacets({
+      componentUuids: this.props.component.id,
+      resolved: 'false'
+    }, ['tags', 'assignees']);
+  },
+
+  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, ['Issues', 'Technical Debt'])
+        .filter(metric => KNOWN_METRICS.indexOf(metric.key) === -1)
+        .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.ordinal()
+        .domain([1, 2, 3, 4, 5])
+        .range(CHART_COLORS_RANGE_PERCENT);
+
+    return <div className="overview-detailed-page">
+      <div className="overview-cards-list">
+        <div className="overview-card overview-card-fixed-width">
+          <div className="overview-card-header">
+            <div className="overview-title">Technical Debt Overview</div>
+            {this.renderLegend()}
+          </div>
+
+          <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
+            <div className="overview-detailed-measure overview-detailed-measure-rating">
+              <div className="overview-detailed-measure-nutshell">
+                <span className="overview-detailed-measure-value">
+                  <DrilldownLink component={this.props.component.key} metric="sqale_rating">
+                    <Rating value={this.state.measures['sqale_rating']}/>
+                  </DrilldownLink>
+                </span>
+              </div>
+            </div>
+            <AddedRemovedMeasure {...this.props} {...this.state}
+                metric="violations" leakMetric="new_violations" type="INT"/>
+            <AddedRemovedDebt {...this.props} {...this.state}
+                metric="sqale_index" leakMetric="new_technical_debt" type="WORK_DUR"/>
+            <OnNewCodeMeasure {...this.props} {...this.state}
+                metric="sqale_debt_ratio" leakMetric="new_sqale_debt_ratio" type="PERCENT"/>
+          </div>
+
+          <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
+            <SeverityMeasure {...this.props} {...this.state} severity="BLOCKER"/>
+            <SeverityMeasure {...this.props} {...this.state} severity="CRITICAL"/>
+            <SeverityMeasure {...this.props} {...this.state} severity="MAJOR"/>
+            <SeverityMeasure {...this.props} {...this.state} severity="MINOR"/>
+            <SeverityMeasure {...this.props} {...this.state} severity="INFO"/>
+          </div>
+
+          <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
+            <div className="overview-detailed-measure">
+              <div className="overview-detailed-measure-nutshell">
+                <IssuesTags {...this.props} tags={this.state.tags}/>
+              </div>
+            </div>
+            <div className="overview-detailed-measure">
+              <div className="overview-detailed-measure-nutshell">
+                <div className="overview-detailed-measure-name">
+                  <StatusHelper status="OPEN"/> & <StatusHelper status="REOPENED"/> Issues
+                </div>
+                <div className="spacer-top">
+                  <Assignees {...this.props} assignees={this.state.assignees}/>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div className="overview-detailed-measures-list">
+            {this.renderOtherMeasures()}
+          </div>
+        </div>
+
+        <div className="overview-card">
+          <DomainBubbleChart {...this.props}
+              xMetric="violations"
+              yMetric="sqale_index"
+              sizeMetrics={['blocker_violations', 'critical_violations']}/>
+        </div>
+      </div>
+
+      <div className="overview-cards-list">
+        <div className="overview-card">
+          <DomainTimeline {...this.props} {...this.state}
+              initialMetric="sqale_index"
+              metrics={this.getMetricsForTimeline()}
+              allMetrics={this.getAllMetricsForTimeline()}/>
+        </div>
+        <div className="overview-card">
+          <DomainTreemap {...this.props}
+              sizeMetric="ncloc"
+              colorMetric="sqale_rating"
+              scale={treemapScale}/>
+        </div>
+      </div>
+    </div>;
+
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/domains/duplications-domain.js b/server/sonar-web/src/main/js/apps/overview/domains/duplications-domain.js
new file mode 100644 (file)
index 0000000..e1e3977
--- /dev/null
@@ -0,0 +1,123 @@
+import d3 from 'd3';
+import React from 'react';
+
+import { getMeasuresAndVariations } from '../../../api/measures';
+import { DetailedMeasure } from '../components/detailed-measure';
+import { DomainTimeline } from '../components/domain-timeline';
+import { DomainTreemap } from '../components/domain-treemap';
+import { DomainBubbleChart } from '../components/domain-bubble-chart';
+import { getPeriodLabel, getPeriodDate } from './../helpers/periods';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { filterMetrics, filterMetricsForDomains } from '../helpers/metrics';
+import { Legend } from '../components/legend';
+import { CHART_COLORS_RANGE_PERCENT } from '../../../helpers/constants';
+
+
+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);
+  },
+
+  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}/>;
+  },
+
+  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-cards-list">
+        <div className="overview-card overview-card-fixed-width">
+          <div className="overview-card-header">
+            <div className="overview-title">Duplications Overview</div>
+            {this.renderLegend()}
+          </div>
+          <div className="overview-detailed-measures-list">
+            {this.renderMeasures()}
+          </div>
+        </div>
+        <div className="overview-card">
+          <DomainBubbleChart {...this.props}
+              xMetric="ncloc"
+              yMetric="duplicated_lines"
+              sizeMetrics={['duplicated_blocks']}/>
+        </div>
+      </div>
+
+      <div className="overview-cards-list">
+        <div className="overview-card">
+          <DomainTimeline {...this.props} {...this.state}
+              initialMetric="duplicated_lines_density"
+              metrics={this.getMetricsForTimeline()}
+              allMetrics={this.getAllMetricsForTimeline()}/>
+        </div>
+        <div className="overview-card">
+          <DomainTreemap {...this.props}
+              sizeMetric="ncloc"
+              colorMetric="duplicated_lines_density"
+              scale={treemapScale}/>
+        </div>
+      </div>
+    </div>;
+
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/domains/size-domain.js b/server/sonar-web/src/main/js/apps/overview/domains/size-domain.js
new file mode 100644 (file)
index 0000000..e65f012
--- /dev/null
@@ -0,0 +1,151 @@
+import React from 'react';
+
+import { LanguageDistribution } from './../components/language-distribution';
+import { ComplexityDistribution } from './../components/complexity-distribution';
+import { getMeasuresAndVariations } from '../../../api/measures';
+import { DetailedMeasure } from '../components/detailed-measure';
+import { DomainTimeline } from '../components/domain-timeline';
+import { DomainTreemap } from '../components/domain-treemap';
+import { getPeriodLabel, getPeriodDate } from './../helpers/periods';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { filterMetrics, filterMetricsForDomains } from '../helpers/metrics';
+import { Legend } from '../components/legend';
+
+
+export const SizeMain = 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 => ['Size', 'Complexity', 'Documentation'].indexOf(metric.domain) !== -1)
+        .map(metric => metric.key);
+  },
+
+  getMetricsForTimeline() {
+    return filterMetricsForDomains(this.props.metrics, ['Size', 'Complexity', 'Documentation']);
+  },
+
+  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(domain, hiddenMetrics) {
+    let metrics = filterMetricsForDomains(this.props.metrics, [domain])
+        .filter(metric => hiddenMetrics.indexOf(metric.key) === -1)
+        .map(metric => {
+          return <DetailedMeasure key={metric.key} {...this.props} {...this.state} metric={metric.key}
+                                  type={metric.type}/>;
+        });
+    return <div>{metrics}</div>;
+  },
+
+  renderOtherSizeMeasures() {
+    return this.renderOtherMeasures('Size', ['ncloc']);
+  },
+
+  renderOtherComplexityMeasures() {
+    return this.renderOtherMeasures('Complexity',
+        ['complexity', 'function_complexity', 'file_complexity', 'class_complexity']);
+  },
+
+  renderOtherDocumentationMeasures() {
+    return this.renderOtherMeasures('Documentation', []);
+  },
+
+  render () {
+    if (!this.state.ready) {
+      return this.renderLoading();
+    }
+    return <div className="overview-detailed-page">
+      <div className="overview-card">
+        <div className="overview-card-header">
+          <div className="overview-title">Size Overview</div>
+          {this.renderLegend()}
+        </div>
+
+        <div className="overview-detailed-layout-size">
+          <div className="overview-detailed-layout-column">
+            <div className="overview-detailed-measures-list">
+              <DetailedMeasure {...this.props} {...this.state} metric="ncloc" type="INT">
+                <LanguageDistribution lines={this.state.measures['ncloc']}
+                                      distribution={this.state.measures['ncloc_language_distribution']}/>
+              </DetailedMeasure>
+              {this.renderOtherSizeMeasures()}
+            </div>
+          </div>
+
+          <div className="overview-detailed-layout-column">
+            <div className="overview-detailed-measures-list">
+              <DetailedMeasure {...this.props} {...this.state} metric="complexity" type="INT"/>
+              <DetailedMeasure {...this.props} {...this.state} metric="function_complexity" type="FLOAT">
+                <ComplexityDistribution distribution={this.state.measures['function_complexity_distribution']}/>
+              </DetailedMeasure>
+              <DetailedMeasure {...this.props} {...this.state} metric="file_complexity" type="FLOAT">
+                <ComplexityDistribution distribution={this.state.measures['file_complexity_distribution']}/>
+              </DetailedMeasure>
+              <DetailedMeasure {...this.props} {...this.state} metric="class_complexity" type="FLOAT"/>
+              {this.renderOtherComplexityMeasures()}
+            </div>
+          </div>
+
+          <div className="overview-detailed-layout-column">
+            <div className="overview-detailed-measures-list">
+              {this.renderOtherDocumentationMeasures()}
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div className="overview-cards-list">
+        <div className="overview-card">
+          <DomainTimeline {...this.props} {...this.state}
+              initialMetric="ncloc"
+              metrics={this.getMetricsForTimeline()}
+              allMetrics={this.getAllMetricsForTimeline()}/>
+        </div>
+        <div className="overview-card">
+          <DomainTreemap {...this.props} sizeMetric="ncloc"/>
+        </div>
+      </div>
+    </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
deleted file mode 100644 (file)
index d85d379..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-import d3 from 'd3';
-import React from 'react';
-
-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 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);
-  },
-
-  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}/>;
-  },
-
-  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 overview-domain-fixed-width">
-          <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_lines"
-            sizeMetrics={['duplicated_blocks']}/>
-      </div>
-
-      <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/gate/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js
new file mode 100644 (file)
index 0000000..6d88348
--- /dev/null
@@ -0,0 +1,52 @@
+import React from 'react';
+
+import { getPeriodLabel, getPeriodDate } from '../helpers/periods';
+import { DrilldownLink } from '../../../components/shared/drilldown-link';
+import { formatMeasure } from '../../../helpers/measures';
+
+
+const Measure = React.createClass({
+  render() {
+    if (this.props.value == null || isNaN(this.props.value)) {
+      return null;
+    }
+    let formatted = formatMeasure(this.props.value, this.props.type);
+    return <span>{formatted}</span>;
+  }
+});
+
+
+export default React.createClass({
+  render() {
+    let metricName = window.t('metric', this.props.condition.metric.name, 'name'),
+        threshold = this.props.condition.level === 'ERROR' ?
+                    this.props.condition.error : this.props.condition.warning,
+        period = this.props.condition.period ?
+                 getPeriodLabel(this.props.component.periods, this.props.condition.period) : null,
+        periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period);
+
+    let classes = 'alert_' + this.props.condition.level.toUpperCase();
+
+    return (
+        <li className="overview-gate-condition">
+          <div className="little-spacer-bottom">{period}</div>
+
+          <div style={{ display: 'flex', alignItems: 'center' }}>
+            <div className="overview-gate-condition-value">
+              <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name}
+                             period={this.props.condition.period} periodDate={periodDate}>
+              <span className={classes}>
+                <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/>
+              </span>
+              </DrilldownLink>&nbsp;
+            </div>
+
+            <div className="overview-gate-condition-metric">
+              <div>{metricName}</div>
+              <div>{window.t('quality_gates.operator', this.props.condition.op, 'short')} <Measure value={threshold} type={this.props.condition.metric.type}/></div>
+            </div>
+          </div>
+        </li>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js
new file mode 100644 (file)
index 0000000..adefecb
--- /dev/null
@@ -0,0 +1,16 @@
+import React from 'react';
+import GateCondition from './gate-condition';
+
+export default React.createClass({
+  propTypes: {
+    gate: React.PropTypes.object.isRequired,
+    component: React.PropTypes.object.isRequired
+  },
+
+  render() {
+    let conditions = this.props.gate.conditions
+        .filter(c => c.level !== 'OK')
+        .map(c => <GateCondition key={c.metric.name} condition={c} component={this.props.component}/>);
+    return <ul className="overview-gate-conditions-list">{conditions}</ul>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js
new file mode 100644 (file)
index 0000000..6134718
--- /dev/null
@@ -0,0 +1,15 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    let qualityGatesUrl = window.baseUrl + '/quality_gates';
+
+    return (
+        <div className="overview-gate">
+          <h2 className="overview-title">{window.t('overview.quality_gate')}</h2>
+          <p className="overview-gate-warning">
+            You should <a href={qualityGatesUrl}>define</a> a quality gate on this project.</p>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate.js b/server/sonar-web/src/main/js/apps/overview/gate/gate.js
new file mode 100644 (file)
index 0000000..076cbcd
--- /dev/null
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import GateConditions from './gate-conditions';
+import GateEmpty from './gate-empty';
+
+
+export default React.createClass({
+  render() {
+    if (!this.props.gate || !this.props.gate.level) {
+      return this.props.component.qualifier === 'TRK' ? <GateEmpty/> : null;
+    }
+
+    let level = this.props.gate.level.toLowerCase(),
+        badgeClassName = 'badge badge-' + level,
+        badgeText = window.t('overview.gate', this.props.gate.level);
+
+    return (
+        <div className="overview-gate">
+          <h2 className="overview-title">
+            {window.t('overview.quality_gate')}
+            <span className={badgeClassName}>{badgeText}</span>
+          </h2>
+          <GateConditions gate={this.props.gate} component={this.props.component}/>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/donut.js b/server/sonar-web/src/main/js/apps/overview/helpers/donut.js
deleted file mode 100644 (file)
index 368a522..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import d3 from 'd3';
-import React from 'react';
-
-let 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 default React.createClass({
-  getDefaultProps() {
-    return {
-      size: 30,
-      thickness: 6
-    };
-  },
-
-  render() {
-    let radius = this.props.size / 2;
-    let pie = d3.layout.pie()
-        .sort(null)
-        .value(d => d.value);
-    let data = this.props.data;
-    let sectors = pie(data).map((d, i) => {
-      return <Sector key={i} data={d} fill={data[i].fill} radius={radius} thickness={this.props.thickness}/>;
-    });
-    return <svg width={this.props.size} height={this.props.size}>
-      <g transform={`translate(${radius}, ${radius})`}>{sectors}</g>
-    </svg>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js
deleted file mode 100644 (file)
index 51f18ea..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-import _ from 'underscore';
-import moment from 'moment';
-import React from 'react';
-import IssuesLink from './issues-link';
-import { getComponentDrilldownUrl } from '../../../helpers/urls';
-
-
-export default React.createClass({
-  isIssueMeasure() {
-    const ISSUE_MEASURES = [
-      'violations',
-      'blocker_violations',
-      'critical_violations',
-      'major_violations',
-      'minor_violations',
-      'info_violations',
-      'new_blocker_violations',
-      'new_critical_violations',
-      'new_major_violations',
-      'new_minor_violations',
-      'new_info_violations',
-      'open_issues',
-      'reopened_issues',
-      'confirmed_issues',
-      'false_positive_issues'
-    ];
-    return ISSUE_MEASURES.indexOf(this.props.metric) !== -1;
-  },
-
-  propsToIssueParams() {
-    let params = {};
-    if (this.props.periodDate) {
-      params.createdAfter = moment(this.props.periodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
-    }
-    switch (this.props.metric) {
-      case 'blocker_violations':
-      case 'new_blocker_violations':
-        _.extend(params, { resolved: 'false', severities: 'BLOCKER' });
-        break;
-      case 'critical_violations':
-      case 'new_critical_violations':
-        _.extend(params, { resolved: 'false', severities: 'CRITICAL' });
-        break;
-      case 'major_violations':
-      case 'new_major_violations':
-        _.extend(params, { resolved: 'false', severities: 'MAJOR' });
-        break;
-      case 'minor_violations':
-      case 'new_minor_violations':
-        _.extend(params, { resolved: 'false', severities: 'MINOR' });
-        break;
-      case 'info_violations':
-      case 'new_info_violations':
-        _.extend(params, { resolved: 'false', severities: 'INFO' });
-        break;
-      case 'open_issues':
-        _.extend(params, { resolved: 'false', statuses: 'OPEN' });
-        break;
-      case 'reopened_issues':
-        _.extend(params, { resolved: 'false', statuses: 'REOPENED' });
-        break;
-      case 'confirmed_issues':
-        _.extend(params, { resolved: 'false', statuses: 'CONFIRMED' });
-        break;
-      case 'false_positive_issues':
-        _.extend(params, { resolutions: 'FALSE-POSITIVE' });
-        break;
-      default:
-        _.extend(params, { resolved: 'false' });
-    }
-    return params;
-  },
-
-  renderIssuesLink() {
-    return <IssuesLink component={this.props.component} params={this.propsToIssueParams()}>
-      {this.props.children}
-    </IssuesLink>;
-  },
-
-  render() {
-    if (this.isIssueMeasure()) {
-      return this.renderIssuesLink();
-    }
-
-    let url = getComponentDrilldownUrl(this.props.component, this.props.metric, this.props.period);
-    return <a href={url}>{this.props.children}</a>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js
deleted file mode 100644 (file)
index 79878ac..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  render() {
-    let url = `${baseUrl}/quality_gates/show/${this.props.gate}`;
-    return <a href={url}>{this.props.children}</a>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js
deleted file mode 100644 (file)
index c3543f5..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-import { getComponentIssuesUrl } from '../../../helpers/urls';
-
-
-export default React.createClass({
-  render() {
-    let url = getComponentIssuesUrl(this.props.component, this.props.params);
-    return <a className={this.props.className} href={url}>{this.props.children}</a>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js b/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js
deleted file mode 100644 (file)
index e6ea091..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-import { formatMeasureVariation } from '../../../helpers/measures';
-
-
-export default React.createClass({
-  render() {
-    if (this.props.value == null || isNaN(this.props.value)) {
-      return null;
-    }
-    let formatted = formatMeasureVariation(this.props.value, this.props.type);
-    return <span>{formatted}</span>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/measure.js b/server/sonar-web/src/main/js/apps/overview/helpers/measure.js
deleted file mode 100644 (file)
index 3d4eb5e..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import { formatMeasure } from '../../../helpers/measures';
-
-export default React.createClass({
-  render() {
-    if (this.props.value == null || isNaN(this.props.value)) {
-      return null;
-    }
-    let formatted = formatMeasure(this.props.value, this.props.type);
-    return <span>{formatted}</span>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js
deleted file mode 100644 (file)
index 5b85989..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import _ from 'underscore';
-import moment from 'moment';
-
-export let getPeriodLabel = (periods, periodIndex) => {
-  let period = _.findWhere(periods, { index: periodIndex });
-  if (!period) {
-    return null;
-  }
-  if (period.mode === 'previous_version' && !period.modeParam) {
-    return window.t('overview.period.previous_version_only_date');
-  }
-  return window.tp(`overview.period.${period.mode}`, period.modeParam);
-};
-
-export let getPeriodDate = (periods, periodIndex) => {
-  let period = _.findWhere(periods, { index: periodIndex });
-  if (!period) {
-    return null;
-  }
-  return moment(period.date).toDate();
-};
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/periods.js b/server/sonar-web/src/main/js/apps/overview/helpers/periods.js
new file mode 100644 (file)
index 0000000..f4e38dc
--- /dev/null
@@ -0,0 +1,23 @@
+import _ from 'underscore';
+import moment from 'moment';
+
+
+export function getPeriodLabel (periods, periodIndex) {
+  let period = _.findWhere(periods, { index: periodIndex });
+  if (!period) {
+    return null;
+  }
+  if (period.mode === 'previous_version' && !period.modeParam) {
+    return window.t('overview.period.previous_version_only_date');
+  }
+  return window.tp(`overview.period.${period.mode}`, period.modeParam);
+}
+
+
+export function getPeriodDate (periods, periodIndex) {
+  let period = _.findWhere(periods, { index: periodIndex });
+  if (!period) {
+    return null;
+  }
+  return moment(period.date).toDate();
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js
deleted file mode 100644 (file)
index 22065ab..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  render() {
-    let url = `${baseUrl}/profiles/show?key=${encodeURIComponent(this.props.profile)}`;
-    return <a href={url}>{this.props.children}</a>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/rating.js b/server/sonar-web/src/main/js/apps/overview/helpers/rating.js
deleted file mode 100644 (file)
index 5568dc5..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-import { formatMeasure } from '../../../helpers/measures';
-
-export default React.createClass({
-  render() {
-    if (this.props.value == null || isNaN(this.props.value)) {
-      return null;
-    }
-    let formatted = formatMeasure(this.props.value, 'RATING');
-    let className = 'rating rating-' + formatted;
-    return <span className={className}>{formatted}</span>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/assignees.js b/server/sonar-web/src/main/js/apps/overview/issues/assignees.js
deleted file mode 100644 (file)
index 1d143a8..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import Assignee from '../../../components/shared/assignee-helper';
-import { DomainHeader } from '../domain/header';
-import { getComponentIssuesUrl } from '../../../helpers/urls';
-import { formatMeasure } from '../../../helpers/measures';
-
-
-export default class extends React.Component {
-  render () {
-    let rows = this.props.assignees.map(s => {
-      let href = getComponentIssuesUrl(this.props.component.key, { statuses: 'OPEN,REOPENED', assignees: s.val });
-      return <tr key={s.val}>
-        <td>
-          <Assignee user={s.user}/>
-        </td>
-        <td className="thin text-right">
-          <a href={href}>{formatMeasure(s.count, 'SHORT_INT')}</a>
-        </td>
-      </tr>;
-    });
-
-    return <table className="data zebra">
-      <tbody>{rows}</tbody>
-    </table>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/issue-measure.js b/server/sonar-web/src/main/js/apps/overview/issues/issue-measure.js
deleted file mode 100644 (file)
index 7234696..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-import React from 'react';
-
-import { formatMeasure, formatMeasureVariation, localizeMetric } from '../../../helpers/measures';
-import DrilldownLink from '../helpers/drilldown-link';
-import IssuesLink from '../helpers/issues-link';
-import { getShortType } from '../helpers/metrics';
-import SeverityHelper from '../../../components/shared/severity-helper';
-
-
-export const IssueMeasure = React.createClass({
-  renderLeak () {
-    if (!this.props.leakPeriodDate) {
-      return null;
-    }
-
-    let leak = this.props.leak[this.props.metric];
-    let added = this.props.leak[this.props.leakMetric];
-    let removed = added - leak;
-
-    return <div className="overview-detailed-measure-leak">
-      <ul className="list-inline">
-        <li className="text-danger">
-          <IssuesLink className="text-danger overview-detailed-measure-value"
-                      component={this.props.component.key} params={{ resolved: 'false' }}>
-            +{formatMeasure(added, getShortType(this.props.type))}
-          </IssuesLink>
-        </li>
-        <li className="text-success">
-          <span className="text-success overview-detailed-measure-value">
-            -{formatMeasure(removed, getShortType(this.props.type))}
-          </span>
-        </li>
-      </ul>
-    </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>
-        <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()}
-    </div>;
-  }
-});
-
-
-export const AddedRemovedMeasure = React.createClass({
-  renderLeak () {
-    if (!this.props.leakPeriodDate) {
-      return null;
-    }
-
-    let leak = this.props.leak[this.props.metric];
-    let added = this.props.leak[this.props.leakMetric];
-    let removed = added - leak;
-
-    return <div className="overview-detailed-measure-leak">
-      <ul>
-        <li style={{ display: 'flex', alignItems: 'baseline' }}>
-          <small className="flex-1 text-left">Added</small>
-          <IssuesLink className="text-danger"
-                      component={this.props.component.key} params={{ resolved: 'false' }}>
-            <span className="overview-detailed-measure-value">
-              {formatMeasure(added, getShortType(this.props.type))}
-            </span>
-          </IssuesLink>
-        </li>
-        <li className="little-spacer-top" style={{ display: 'flex', alignItems: 'baseline' }}>
-          <small className="flex-1 text-left">Removed</small>
-          <span className="text-success">
-            {formatMeasure(removed, getShortType(this.props.type))}
-          </span>
-        </li>
-      </ul>
-    </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>
-        <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()}
-    </div>;
-  }
-});
-
-
-export const OnNewCodeMeasure = React.createClass({
-  renderLeak () {
-    if (!this.props.leakPeriodDate) {
-      return null;
-    }
-
-    let onNewCode = this.props.leak[this.props.leakMetric];
-
-    return <div className="overview-detailed-measure-leak">
-      <ul>
-        <li className="little-spacer-top" style={{ display: 'flex', alignItems: 'center' }}>
-          <small className="flex-1 text-left">On New Code</small>
-          <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}>
-            <span className="overview-detailed-measure-value">
-              {formatMeasure(onNewCode, getShortType(this.props.type))}
-            </span>
-          </IssuesLink>
-        </li>
-      </ul>
-    </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>
-        <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()}
-    </div>;
-  }
-});
-
-
-export const SeverityMeasure = React.createClass({
-  getMetric () {
-    return this.props.severity.toLowerCase() + '_violations';
-  },
-
-  getNewMetric () {
-    return 'new_' + this.props.severity.toLowerCase() + '_violations';
-  },
-
-
-  renderLeak () {
-    if (!this.props.leakPeriodDate) {
-      return null;
-    }
-
-    let leak = this.props.leak[this.getMetric()];
-    let added = this.props.leak[this.getNewMetric()];
-    let removed = added - leak;
-
-    return <div className="overview-detailed-measure-leak">
-      <ul>
-        <li style={{ display: 'flex', alignItems: 'baseline' }}>
-          <small className="flex-1 text-left">Added</small>
-          <IssuesLink className="text-danger"
-                      component={this.props.component.key} params={{ resolved: 'false' }}>
-            <span className="overview-detailed-measure-value">
-              {formatMeasure(added, 'SHORT_INT')}
-            </span>
-          </IssuesLink>
-        </li>
-        <li className="little-spacer-top" style={{ display: 'flex', alignItems: 'baseline' }}>
-          <small className="flex-1 text-left">Removed</small>
-          <span className="text-success">
-            {formatMeasure(removed, 'SHORT_INT')}
-          </span>
-        </li>
-      </ul>
-    </div>;
-  },
-
-  render () {
-    let measure = this.props.measures[this.getMetric()];
-    if (measure == null) {
-      return null;
-    }
-
-    return <div className="overview-detailed-measure">
-      <div className="overview-detailed-measure-nutshell">
-        <span className="overview-detailed-measure-name">
-          <SeverityHelper severity={this.props.severity}/>
-        </span>
-        <span className="overview-detailed-measure-value">
-          <DrilldownLink component={this.props.component.key} metric={this.getMetric()}>
-            {formatMeasure(measure, 'SHORT_INT')}
-          </DrilldownLink>
-        </span>
-        {this.props.children}
-      </div>
-      {this.renderLeak()}
-    </div>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/main.js b/server/sonar-web/src/main/js/apps/overview/issues/main.js
deleted file mode 100644 (file)
index d5fd68c..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-import _ from 'underscore';
-import d3 from 'd3';
-import React from 'react';
-
-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 { IssueMeasure, AddedRemovedMeasure, OnNewCodeMeasure, SeverityMeasure } from './issue-measure';
-import { formatMeasure, formatMeasureVariation, localizeMetric } from '../../../helpers/measures';
-import IssuesLink from '../helpers/issues-link';
-import { Measure } from '../main/components';
-import { getMetricName } from '../helpers/metrics';
-import Tags from './tags';
-import Assignees from './assignees';
-import { getFacets, extractAssignees } from '../../../api/issues';
-import StatusHelper from '../../../components/shared/status-helper';
-import Rating from '../helpers/rating';
-import DrilldownLink from '../helpers/drilldown-link';
-
-
-const KNOWN_METRICS = ['violations', 'sqale_index', 'sqale_rating', 'sqale_debt_ratio', 'blocker_violations',
-  'critical_violations', 'major_violations', 'minor_violations', 'info_violations', 'confirmed_issues'];
-
-
-export const IssuesMain = 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() {
-    Promise.all([
-      this.requestMeasures(),
-      this.requestIssues()
-    ]).then(responses => {
-      let measures = this.getMeasuresValues(responses[0], 'value');
-      let leak = this.getMeasuresValues(responses[0], 'var' + this.props.leakPeriodIndex);
-      let tags = this.getFacet(responses[1].facets, 'tags');
-      let assignees = extractAssignees(this.getFacet(responses[1].facets, 'assignees'), responses[1].response);
-      this.setState({ ready: true, measures, leak, tags, assignees });
-    });
-  },
-
-  getMeasuresValues (measures, fieldKey) {
-    let values = {};
-    Object.keys(measures).forEach(measureKey => {
-      values[measureKey] = measures[measureKey][fieldKey];
-    });
-    return values;
-  },
-
-  getMetricsForDomain() {
-    return this.props.metrics
-        .filter(metric => ['Issues', 'Technical Debt'].indexOf(metric.domain) !== -1)
-        .map(metric => metric.key);
-  },
-
-  getMetricsForTimeline() {
-    return filterMetricsForDomains(this.props.metrics, ['Issues', 'Technical Debt']);
-  },
-
-  getAllMetricsForTimeline() {
-    return filterMetrics(this.props.metrics);
-  },
-
-  requestMeasures () {
-    return getMeasuresAndVariations(this.props.component.key, this.getMetricsForDomain());
-  },
-
-  getFacet (facets, facetKey) {
-    return _.findWhere(facets, { property: facetKey }).values;
-  },
-
-  requestIssues () {
-    return getFacets({
-      componentUuids: this.props.component.id,
-      resolved: 'false'
-    }, ['tags', 'assignees']);
-  },
-
-  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, ['Issues', 'Technical Debt'])
-        .filter(metric => KNOWN_METRICS.indexOf(metric.key) === -1)
-        .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.ordinal()
-        .domain([1, 2, 3, 4, 5])
-        .range(CHART_COLORS_RANGE_PERCENT);
-
-    let rating = formatMeasure(this.state.measures['sqale_rating'], 'RATING');
-
-    return <div className="overview-detailed-page">
-      <div className="overview-domain-charts">
-        <div className="overview-domain overview-domain-fixed-width">
-          <div className="overview-domain-header">
-            <div className="overview-title">Technical Debt Overview</div>
-            {this.renderLegend()}
-          </div>
-
-          <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
-            <div className="overview-detailed-measure">
-              <div className="overview-detailed-measure-nutshell">
-                <span className="overview-detailed-measure-name">SQALE Rating</span>
-                <span className="overview-detailed-measure-value">
-                  <DrilldownLink component={this.props.component.key} metric="sqale_rating">
-                    <Rating value={this.state.measures['sqale_rating']}/>
-                  </DrilldownLink>
-                </span>
-              </div>
-            </div>
-            <AddedRemovedMeasure {...this.props} {...this.state}
-                metric="violations" leakMetric="new_violations" type="INT"/>
-            <AddedRemovedMeasure {...this.props} {...this.state}
-                metric="sqale_index" leakMetric="new_technical_debt" type="WORK_DUR"/>
-            <OnNewCodeMeasure {...this.props} {...this.state}
-                metric="sqale_debt_ratio" leakMetric="new_sqale_debt_ratio" type="PERCENT"/>
-          </div>
-
-          <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
-            <SeverityMeasure {...this.props} {...this.state} severity="BLOCKER"/>
-            <SeverityMeasure {...this.props} {...this.state} severity="CRITICAL"/>
-            <SeverityMeasure {...this.props} {...this.state} severity="MAJOR"/>
-            <SeverityMeasure {...this.props} {...this.state} severity="MINOR"/>
-            <SeverityMeasure {...this.props} {...this.state} severity="INFO"/>
-          </div>
-
-          <div className="overview-detailed-measures-list overview-detailed-measures-list-inline">
-            <div className="overview-detailed-measure">
-              <div className="overview-detailed-measure-nutshell">
-                <Tags {...this.props} tags={this.state.tags}/>
-              </div>
-            </div>
-            <div className="overview-detailed-measure">
-              <div className="overview-detailed-measure-nutshell">
-                <div className="overview-detailed-measure-name">
-                  <StatusHelper status="OPEN"/> & <StatusHelper status="REOPENED"/> Issues
-                </div>
-                <div className="spacer-top">
-                  <Assignees {...this.props} assignees={this.state.assignees}/>
-                </div>
-              </div>
-            </div>
-          </div>
-
-          <div className="overview-detailed-measures-list">
-            {this.renderOtherMeasures()}
-          </div>
-        </div>
-        <DomainBubbleChart {...this.props}
-            xMetric="violations"
-            yMetric="sqale_index"
-            sizeMetrics={['blocker_violations', 'critical_violations']}/>
-      </div>
-
-      <div className="overview-domain-charts">
-        <DomainTimeline {...this.props} {...this.state}
-            initialMetric="sqale_index"
-            metrics={this.getMetricsForTimeline()}
-            allMetrics={this.getAllMetricsForTimeline()}/>
-        <DomainTreemap {...this.props}
-            sizeMetric="ncloc"
-            colorMetric="sqale_rating"
-            scale={treemapScale}/>
-      </div>
-    </div>;
-
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/severities.js b/server/sonar-web/src/main/js/apps/overview/issues/severities.js
deleted file mode 100644 (file)
index 26008e4..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-import _ from 'underscore';
-import React from 'react';
-import SeverityHelper from '../../../components/shared/severity-helper';
-import { DomainHeader } from '../domain/header';
-import { getComponentIssuesUrl } from '../../../helpers/urls';
-import { formatMeasure } from '../../../helpers/measures';
-
-
-export default class extends React.Component {
-  sortedSeverities () {
-    return _.sortBy(this.props.severities, s => window.severityComparator(s.val));
-  }
-
-  render () {
-    let rows = this.sortedSeverities().map(s => {
-      let href = getComponentIssuesUrl(this.props.component.key, { resolved: 'false', severities: s.val });
-      return <tr key={s.val}>
-        <td>
-          <SeverityHelper severity={s.val}/>
-        </td>
-        <td className="thin text-right">
-          <a className="cell-link" href={href}>
-            {formatMeasure(s.count, 'SHORT_INT')}
-          </a>
-        </td>
-      </tr>;
-    });
-
-    return <div className="overview-domain-section">
-      <DomainHeader title="Prioritized Issues"/>
-      <table className="data zebra">
-        <tbody>{rows}</tbody>
-      </table>
-    </div>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/tags.js b/server/sonar-web/src/main/js/apps/overview/issues/tags.js
deleted file mode 100644 (file)
index 8d1644d..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import { DomainHeader } from '../domain/header';
-import { WordCloud } from '../../../components/charts/word-cloud';
-import { getComponentIssuesUrl } from '../../../helpers/urls';
-import { formatMeasure } from '../../../helpers/measures';
-
-
-export default class extends React.Component {
-  renderWordCloud () {
-    let tags = this.props.tags.map(tag => {
-      let link = getComponentIssuesUrl(this.props.component.key, { resolved: 'false', tags: tag.val });
-      let tooltip = `Issues: ${formatMeasure(tag.count, 'SHORT_INT')}`;
-      return { text: tag.val, size: tag.count, link, tooltip };
-    });
-    return <WordCloud items={tags}/>;
-  }
-
-  render () {
-    return this.renderWordCloud();
-  }
-}
index b8bdf111c531b18f3c3fc53c8a5f61a3353c23d0..68a5cd0670d62267ba252eec49f7fa2bd627e3ce 100644 (file)
@@ -3,12 +3,12 @@ import React from 'react';
 
 import { Timeline } from './timeline';
 import { navigate } from '../../../components/router/router';
-import { Legend } from '../common-components';
+import { Legend } from '../components/legend';
 
 
 export const Domain = React.createClass({
   render () {
-    return <div className="overview-domain">{this.props.children}</div>;
+    return <div className="overview-card">{this.props.children}</div>;
   }
 });
 
@@ -50,7 +50,7 @@ export const DomainLeakTitle = React.createClass({
 
 export const DomainHeader = React.createClass({
   render () {
-    return <div className="overview-domain-header">
+    return <div className="overview-card-header">
       <DomainTitle linkTo={this.props.linkTo}>{this.props.title}</DomainTitle>
       <Legend leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
     </div>;
index 056876f3e545d143d97acd46cd4aa11be6a14fcf..09752ad62a83e3457de12f7b701ee15727c74111 100644 (file)
@@ -1,7 +1,7 @@
 import React from 'react';
 
 import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
-import DrilldownLink from '../helpers/drilldown-link';
+import { DrilldownLink } from '../../../components/shared/drilldown-link';
 import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
 import { getMetricName } from '../helpers/metrics';
 import { formatMeasure } from '../../../helpers/measures';
index db4c6e6c32042ad5d7956763a35ea2cdc30471d3..06583cf5f8ea82cd78a5c7458bce50217ee1c31c 100644 (file)
@@ -1,7 +1,7 @@
 import React from 'react';
 
 import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
-import DrilldownLink from '../helpers/drilldown-link';
+import { DrilldownLink } from '../../../components/shared/drilldown-link';
 import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
 import { getMetricName } from '../helpers/metrics';
 import { formatMeasure, formatMeasureVariation } from '../../../helpers/measures';
diff --git a/server/sonar-web/src/main/js/apps/overview/main/gate/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/main/gate/gate-condition.js
deleted file mode 100644 (file)
index 0819d60..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-
-import Measure from '../../helpers/measure';
-import { getPeriodLabel, getPeriodDate } from '../../helpers/period-label';
-import DrilldownLink from '../../helpers/drilldown-link';
-
-
-export default React.createClass({
-  render() {
-    let metricName = window.t('metric', this.props.condition.metric.name, 'name'),
-        threshold = this.props.condition.level === 'ERROR' ?
-                    this.props.condition.error : this.props.condition.warning,
-        period = this.props.condition.period ?
-                 getPeriodLabel(this.props.component.periods, this.props.condition.period) : null,
-        periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period);
-
-    let classes = 'alert_' + this.props.condition.level.toUpperCase();
-
-    return (
-        <li className="overview-gate-condition">
-          <div className="little-spacer-bottom">{period}</div>
-
-          <div style={{ display: 'flex', alignItems: 'center' }}>
-            <div className="overview-gate-condition-value">
-              <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name}
-                             period={this.props.condition.period} periodDate={periodDate}>
-              <span className={classes}>
-                <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/>
-              </span>
-              </DrilldownLink>&nbsp;
-            </div>
-
-            <div className="overview-gate-condition-metric">
-              <div>{metricName}</div>
-              <div>{window.t('quality_gates.operator', this.props.condition.op, 'short')} <Measure value={threshold} type={this.props.condition.metric.type}/></div>
-            </div>
-          </div>
-        </li>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/main/gate/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/main/gate/gate-conditions.js
deleted file mode 100644 (file)
index adefecb..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-import GateCondition from './gate-condition';
-
-export default React.createClass({
-  propTypes: {
-    gate: React.PropTypes.object.isRequired,
-    component: React.PropTypes.object.isRequired
-  },
-
-  render() {
-    let conditions = this.props.gate.conditions
-        .filter(c => c.level !== 'OK')
-        .map(c => <GateCondition key={c.metric.name} condition={c} component={this.props.component}/>);
-    return <ul className="overview-gate-conditions-list">{conditions}</ul>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/main/gate/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/main/gate/gate-empty.js
deleted file mode 100644 (file)
index 6134718..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  render() {
-    let qualityGatesUrl = window.baseUrl + '/quality_gates';
-
-    return (
-        <div className="overview-gate">
-          <h2 className="overview-title">{window.t('overview.quality_gate')}</h2>
-          <p className="overview-gate-warning">
-            You should <a href={qualityGatesUrl}>define</a> a quality gate on this project.</p>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/main/gate/gate.js b/server/sonar-web/src/main/js/apps/overview/main/gate/gate.js
deleted file mode 100644 (file)
index 076cbcd..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from 'react';
-
-import GateConditions from './gate-conditions';
-import GateEmpty from './gate-empty';
-
-
-export default React.createClass({
-  render() {
-    if (!this.props.gate || !this.props.gate.level) {
-      return this.props.component.qualifier === 'TRK' ? <GateEmpty/> : null;
-    }
-
-    let level = this.props.gate.level.toLowerCase(),
-        badgeClassName = 'badge badge-' + level,
-        badgeText = window.t('overview.gate', this.props.gate.level);
-
-    return (
-        <div className="overview-gate">
-          <h2 className="overview-title">
-            {window.t('overview.quality_gate')}
-            <span className={badgeClassName}>{badgeText}</span>
-          </h2>
-          <GateConditions gate={this.props.gate} component={this.props.component}/>
-        </div>
-    );
-  }
-});
index f281f85d08de5ad0c3c118d7f13aa43dc9601ac8..5a500cda8e494ebdde2c63eca3a62d5a1e233a13 100644 (file)
@@ -2,16 +2,13 @@ import moment from 'moment';
 import React from 'react';
 
 import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
-import Rating from './../helpers/rating';
-import IssuesLink from '../helpers/issues-link';
-import DrilldownLink from '../helpers/drilldown-link';
-import SeverityHelper from '../../../components/shared/severity-helper';
+import { Rating } from './../../../components/shared/rating';
+import { IssuesLink } from '../../../components/shared/issues-link';
+import { DrilldownLink } from '../../../components/shared/drilldown-link';
 import SeverityIcon from '../../../components/shared/severity-icon';
-import StatusIcon from '../../../components/shared/status-icon';
 import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
 import { getMetricName } from '../helpers/metrics';
-import { SEVERITIES } from '../../../helpers/constants';
-import { formatMeasure, formatMeasureVariation } from '../../../helpers/measures';
+import { formatMeasure } from '../../../helpers/measures';
 
 
 export const GeneralIssues = React.createClass({
index d23555cb0e086fbac10803168eb2dcf40dbda795..993876b600c2f18238a858be76938190ad8212c6 100644 (file)
@@ -6,7 +6,7 @@ import { GeneralIssues } from './issues';
 import { GeneralCoverage } from './coverage';
 import { GeneralDuplications } from './duplications';
 import { GeneralSize } from './size';
-import { getPeriodLabel, getPeriodDate } from './../helpers/period-label';
+import { getPeriodLabel, getPeriodDate } from './../helpers/periods';
 import { getMeasuresAndVariations } from '../../../api/measures';
 import { getFacet, getIssuesCount } from '../../../api/issues';
 import { getTimeMachineData } from '../../../api/time-machine';
@@ -157,7 +157,7 @@ export default React.createClass({
 
     let props = _.extend({}, this.props, this.state);
 
-    return <div className="overview-domains">
+    return <div className="overview-domains-list">
       <GeneralIssues {...props} history={this.state.history['sqale_index']}/>
       <GeneralCoverage {...props} history={this.state.history['overall_coverage']}/>
       <GeneralDuplications {...props} history={this.state.history['duplicated_lines_density']}/>
index 1c2f9fd653633ebebcef9795e167948d8b9d7157..627a85697578f4394869288ade7ecf87f8c0e91f 100644 (file)
@@ -1,11 +1,11 @@
 import React from 'react';
 
 import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
-import DrilldownLink from '../helpers/drilldown-link';
+import { DrilldownLink } from '../../../components/shared/drilldown-link';
 import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
 import { getMetricName } from '../helpers/metrics';
 import { formatMeasure, formatMeasureVariation } from '../../../helpers/measures';
-import { LanguageDistribution } from '../size/language-distribution';
+import { LanguageDistribution } from '../components/language-distribution';
 
 
 export const GeneralSize = React.createClass({
index e3e632a30fa72c37795278d3b0b4c34febd535b7..e6a48e71e09193d01424957ce8d13f34fd7ecfee 100644 (file)
@@ -1,7 +1,7 @@
 import _ from 'underscore';
 import React from 'react';
-import ProfileLink from './helpers/profile-link';
-import GateLink from './helpers/gate-link';
+import { QualityProfileLink } from './../../components/shared/quality-profile-link';
+import { QualityGateLink } from './../../components/shared/quality-gate-link';
 
 export default React.createClass({
   render() {
@@ -10,7 +10,7 @@ export default React.createClass({
           return (
               <li key={profile.key}>
                 <span className="note spacer-right">({profile.language})</span>
-                <ProfileLink profile={profile.key}>{profile.name}</ProfileLink>
+                <QualityProfileLink profile={profile.key}>{profile.name}</QualityProfileLink>
               </li>
           );
         }),
@@ -50,7 +50,7 @@ export default React.createClass({
                 <li>
                   {this.props.component.gate.isDefault ?
                       <span className="note spacer-right">(Default)</span> : null}
-                  <GateLink gate={this.props.component.gate.key}>{this.props.component.gate.name}</GateLink>
+                  <QualityGateLink gate={this.props.component.gate.key}>{this.props.component.gate.name}</QualityGateLink>
                 </li>
               </ul>
             </div>
index 8b36967694dd0fcacb3c738492c2061822ccb884..8301fd6add4b61a37d715f4ac421f9e79916a0a6 100644 (file)
@@ -1,12 +1,12 @@
 import React from 'react';
 
-import Gate from './main/gate/gate';
+import Gate from './gate/gate';
 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 { IssuesMain } from './issues/main';
+import { SizeMain } from './domains/size-domain';
+import { DuplicationsMain } from './domains/duplications-domain';
+import { CoverageMain } from './domains/coverage-domain';
+import { IssuesMain } from './domains/debt-domain';
 
 import { getMetrics } from '../../api/metrics';
 import { RouterMixin } from '../../components/router/router';
diff --git a/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js b/server/sonar-web/src/main/js/apps/overview/size/complexity-distribution.js
deleted file mode 100644 (file)
index 2c82df4..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-
-import { BarChart } from '../../../components/charts/bar-chart';
-import { formatMeasure } from '../../../helpers/measures';
-
-
-const HEIGHT = 80;
-
-
-export class ComplexityDistribution extends React.Component {
-  renderBarChart () {
-    let data = this.props.distribution.split(';').map((point, index) => {
-      let tokens = point.split('=');
-      return { x: index, y: parseInt(tokens[1], 10), value: parseInt(tokens[0], 10) };
-    });
-
-    let xTicks = data.map(point => point.value);
-
-    let xValues = data.map(point => formatMeasure(point.y, 'INT'));
-
-    return <BarChart data={data}
-                     xTicks={xTicks}
-                     xValues={xValues}
-                     height={HEIGHT}
-                     barsWidth={10}
-                     padding={[25, 0, 25, 0]}/>;
-  }
-
-  render () {
-    return <div className="overview-bar-chart">
-      {this.renderBarChart()}
-    </div>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js b/server/sonar-web/src/main/js/apps/overview/size/language-distribution.js
deleted file mode 100644 (file)
index d3bde4d..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-import _ from 'underscore';
-import React from 'react';
-
-import { Histogram } from '../../../components/charts/histogram';
-import { formatMeasure } from '../../../helpers/measures';
-import { getLanguages } from '../../../api/languages';
-
-
-export class LanguageDistribution extends React.Component {
-  componentDidMount () {
-    this.requestLanguages();
-  }
-
-  requestLanguages () {
-    getLanguages().then(languages => this.setState({ languages }));
-  }
-
-  getLanguageName (langKey) {
-    if (this.state && this.state.languages) {
-      let lang = _.findWhere(this.state.languages, { key: langKey });
-      return lang ? lang.name : window.t('unknown');
-    } else {
-      return langKey;
-    }
-  }
-
-  renderBarChart () {
-    let data = this.props.distribution.split(';').map((point, index) => {
-      let tokens = point.split('=');
-      return { x: parseInt(tokens[1], 10), y: index, value: tokens[0] };
-    });
-
-    data = _.sortBy(data, d => -d.x);
-
-    let yTicks = data.map(point => this.getLanguageName(point.value));
-
-    let yValues = data.map(point => formatMeasure(point.x / this.props.lines * 100, 'PERCENT'));
-
-    return <Histogram data={data}
-                      yTicks={yTicks}
-                      yValues={yValues}
-                      height={data.length * 25}
-                      barsWidth={10}
-                      padding={[0, 50, 0, 80]}/>;
-  }
-
-  render () {
-    return <div className="overview-bar-chart">
-      {this.renderBarChart()}
-    </div>;
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/size/main.js b/server/sonar-web/src/main/js/apps/overview/size/main.js
deleted file mode 100644 (file)
index d54f389..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-import React from 'react';
-
-import { DomainLeakTitle } from '../main/components';
-import { LanguageDistribution } from './language-distribution';
-import { ComplexityDistribution } from './complexity-distribution';
-import { getMeasuresAndVariations } from '../../../api/measures';
-import { DetailedMeasure } from '../common-components';
-import { DomainTimeline } from '../timeline/domain-timeline';
-import { DomainTreemap } from '../domain/treemap';
-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';
-
-
-export const SizeMain = 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 => ['Size', 'Complexity', 'Documentation'].indexOf(metric.domain) !== -1)
-        .map(metric => metric.key);
-  },
-
-  getMetricsForTimeline() {
-    return filterMetricsForDomains(this.props.metrics, ['Size', 'Complexity', 'Documentation']);
-  },
-
-  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(domain, hiddenMetrics) {
-    let metrics = filterMetricsForDomains(this.props.metrics, [domain])
-        .filter(metric => hiddenMetrics.indexOf(metric.key) === -1)
-        .map(metric => {
-          return <DetailedMeasure key={metric.key} {...this.props} {...this.state} metric={metric.key}
-                                  type={metric.type}/>;
-        });
-    return <div>{metrics}</div>;
-  },
-
-  renderOtherSizeMeasures() {
-    return this.renderOtherMeasures('Size', ['ncloc']);
-  },
-
-  renderOtherComplexityMeasures() {
-    return this.renderOtherMeasures('Complexity',
-        ['complexity', 'function_complexity', 'file_complexity', 'class_complexity']);
-  },
-
-  renderOtherDocumentationMeasures() {
-    return this.renderOtherMeasures('Documentation', []);
-  },
-
-  render () {
-    if (!this.state.ready) {
-      return this.renderLoading();
-    }
-    return <div className="overview-detailed-page">
-      <div className="overview-domain">
-        <div className="overview-domain-header">
-          <div className="overview-title">Size Overview</div>
-          {this.renderLegend()}
-        </div>
-
-        <div className="overview-detailed-layout-size">
-          <div className="overview-detailed-layout-column">
-            <div className="overview-detailed-measures-list">
-              <DetailedMeasure {...this.props} {...this.state} metric="ncloc" type="INT">
-                <LanguageDistribution lines={this.state.measures['ncloc']}
-                                      distribution={this.state.measures['ncloc_language_distribution']}/>
-              </DetailedMeasure>
-              {this.renderOtherSizeMeasures()}
-            </div>
-          </div>
-
-          <div className="overview-detailed-layout-column">
-            <div className="overview-detailed-measures-list">
-              <DetailedMeasure {...this.props} {...this.state} metric="complexity" type="INT"/>
-              <DetailedMeasure {...this.props} {...this.state} metric="function_complexity" type="FLOAT">
-                <ComplexityDistribution distribution={this.state.measures['function_complexity_distribution']}/>
-              </DetailedMeasure>
-              <DetailedMeasure {...this.props} {...this.state} metric="file_complexity" type="FLOAT">
-                <ComplexityDistribution distribution={this.state.measures['file_complexity_distribution']}/>
-              </DetailedMeasure>
-              <DetailedMeasure {...this.props} {...this.state} metric="class_complexity" type="FLOAT"/>
-              {this.renderOtherComplexityMeasures()}
-            </div>
-          </div>
-
-          <div className="overview-detailed-layout-column">
-            <div className="overview-detailed-measures-list">
-              {this.renderOtherDocumentationMeasures()}
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <div className="overview-domain-charts">
-        <DomainTimeline {...this.props} {...this.state}
-            initialMetric="ncloc"
-            metrics={this.getMetricsForTimeline()}
-            allMetrics={this.getAllMetricsForTimeline()}/>
-        <DomainTreemap {...this.props} sizeMetric="ncloc"/>
-      </div>
-    </div>;
-
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/timeline/domain-timeline.js b/server/sonar-web/src/main/js/apps/overview/timeline/domain-timeline.js
deleted file mode 100644 (file)
index 59e5280..0000000
+++ /dev/null
@@ -1,201 +0,0 @@
-import _ from 'underscore';
-import moment from 'moment';
-import React from 'react';
-
-import { getTimeMachineData } from '../../../api/time-machine';
-import { getEvents } from '../../../api/events';
-import { formatMeasure, groupByDomain } from '../../../helpers/measures';
-import { getShortType } from '../helpers/metrics';
-import { Timeline } from './timeline-chart';
-
-
-const HEIGHT = 280;
-
-function parseValue (value, type) {
-  return type === 'RATING' && typeof value === 'string' ? value.charCodeAt(0) - 'A'.charCodeAt(0) + 1 : value;
-}
-
-
-export const DomainTimeline = React.createClass({
-  propTypes: {
-    allMetrics: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
-    metrics: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
-    initialMetric: React.PropTypes.string.isRequired
-  },
-
-  getInitialState() {
-    return {
-      loading: true,
-      currentMetric: this.props.initialMetric,
-      comparisonMetric: ''
-    };
-  },
-
-  componentDidMount () {
-    Promise.all([
-      this.requestTimeMachineData(this.state.currentMetric, this.state.comparisonMetric),
-      this.requestEvents()
-    ]).then(responses => {
-      this.setState({
-        loading: false,
-        snapshots: responses[0],
-        events: responses[1]
-      });
-    });
-  },
-
-  requestTimeMachineData (currentMetric, comparisonMetric) {
-    let metricsToRequest = [currentMetric];
-    if (comparisonMetric) {
-      metricsToRequest.push(comparisonMetric);
-    }
-    return getTimeMachineData(this.props.component.key, metricsToRequest.join()).then(r => {
-      return r[0].cells.map(cell => {
-        return { date: moment(cell.d).toDate(), values: cell.v };
-      });
-    });
-  },
-
-  requestEvents () {
-    return getEvents(this.props.component.key, 'Version').then(r => {
-      let events = r.map(event => {
-        return { version: event.n, date: moment(event.dt).toDate() };
-      });
-      return _.sortBy(events, 'date');
-    });
-  },
-
-  handleMetricChange (e) {
-    let newMetric = e.target.value,
-        comparisonMetric = this.state.comparisonMetric;
-    if (newMetric === comparisonMetric) {
-      comparisonMetric = '';
-    }
-    this.requestTimeMachineData(newMetric, comparisonMetric).then(snapshots => {
-      this.setState({ currentMetric: newMetric, comparisonMetric: comparisonMetric, snapshots });
-    });
-  },
-
-  handleComparisonMetricChange (e) {
-    let newMetric = e.target.value;
-    this.requestTimeMachineData(this.state.currentMetric, newMetric).then(snapshots => {
-      this.setState({ comparisonMetric: newMetric, snapshots });
-    });
-  },
-
-  groupMetricsByDomain () {
-    return groupByDomain(this.props.metrics);
-  },
-
-  renderLoading () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      <i className="spinner"/>
-    </div>;
-  },
-
-  renderWhenNoHistoricalData () {
-    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
-      There is no historical data.
-    </div>;
-  },
-
-  renderLineCharts () {
-    if (this.state.loading) {
-      return this.renderLoading();
-    }
-    return <div>
-      {this.renderLineChart(this.state.snapshots, this.state.currentMetric, 0)}
-      {this.renderLineChart(this.state.snapshots, this.state.comparisonMetric, 1)}
-    </div>;
-  },
-
-  renderLineChart (snapshots, metric, index) {
-    if (!metric) {
-      return null;
-    }
-
-    if (snapshots.length < 2) {
-      return this.renderWhenNoHistoricalData();
-    }
-
-    let metricType = _.findWhere(this.props.allMetrics, { key: metric }).type;
-    let data = snapshots.map(snapshot => {
-      return {
-        x: snapshot.date,
-        y: parseValue(snapshot.values[index], metricType)
-      };
-    });
-
-    let formatValue = (value) => formatMeasure(value, metricType);
-    let formatYTick = (tick) => formatMeasure(tick, getShortType(metricType));
-
-    return <div className={'overview-timeline-' + index}>
-      <Timeline key={metric}
-                data={data}
-                events={this.state.events}
-                height={HEIGHT}
-                interpolate="linear"
-                formatValue={formatValue}
-                formatYTick={formatYTick}
-                leakPeriodDate={this.props.leakPeriodDate}
-                padding={[25, 25, 25, 60]}/>
-    </div>;
-  },
-
-  renderMetricOption (metric) {
-    return <option key={metric.key} value={metric.key}>{metric.name}</option>;
-  },
-
-  renderMetricOptions (metrics) {
-    let groupedMetrics = groupByDomain(metrics);
-    return groupedMetrics.map(metricGroup => {
-      let options = metricGroup.metrics.map(this.renderMetricOption);
-      return <optgroup key={metricGroup.domain} label={metricGroup.domain}>{options}</optgroup>;
-    });
-  },
-
-  renderTimelineMetricSelect () {
-    if (this.state.loading) {
-      return null;
-    }
-    return <span>
-      <span className="overview-timeline-sample overview-timeline-sample-0"/>
-      <select ref="metricSelect"
-              className="overview-timeline-select"
-              onChange={this.handleMetricChange}
-              value={this.state.currentMetric}>{this.renderMetricOptions(this.props.metrics)}</select>
-    </span>;
-  },
-
-  renderComparisonMetricSelect () {
-    if (this.state.loading) {
-      return null;
-    }
-    let metrics = this.props.allMetrics.filter(metric => metric.key !== this.state.currentMetric);
-    return <span>
-      {this.state.comparisonMetric ? <span className="overview-timeline-sample overview-timeline-sample-1"/> : null}
-      <select ref="comparisonMetricSelect"
-              className="overview-timeline-select"
-              onChange={this.handleComparisonMetricChange}
-              value={this.state.comparisonMetric}>
-        <option value="">Compare with...</option>
-        {this.renderMetricOptions(metrics)}
-      </select>
-    </span>;
-  },
-
-  render () {
-    return <div className="overview-domain overview-domain-chart">
-      <div className="overview-domain-header">
-        <div>
-          <h2 className="overview-title">Timeline</h2>
-          {this.renderTimelineMetricSelect()}
-        </div>
-        {this.renderComparisonMetricSelect()}
-      </div>
-      <div className="overview-timeline">
-        {this.renderLineCharts()}
-      </div>
-    </div>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/timeline/timeline-chart.js b/server/sonar-web/src/main/js/apps/overview/timeline/timeline-chart.js
deleted file mode 100644 (file)
index fc61b19..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-import _ from 'underscore';
-import moment from 'moment';
-import React from 'react';
-
-import { ResizeMixin } from '../../../components/mixins/resize-mixin';
-import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
-
-
-export const Timeline = React.createClass({
-  mixins: [ResizeMixin, TooltipsMixin],
-
-  propTypes: {
-    data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
-    padding: React.PropTypes.arrayOf(React.PropTypes.number),
-    height: React.PropTypes.number,
-    interpolate: React.PropTypes.string
-  },
-
-  getDefaultProps() {
-    return {
-      padding: [10, 10, 10, 10],
-      interpolate: 'basis'
-    };
-  },
-
-  getInitialState() {
-    return { width: this.props.width, height: this.props.height };
-  },
-
-  renderHorizontalGrid (xScale, yScale) {
-    let ticks = yScale.ticks(4);
-    let grid = ticks.map(tick => {
-      let opts = {
-        x: xScale.range()[0],
-        y: yScale(tick)
-      };
-      return <g key={tick}>
-        <text className="line-chart-tick line-chart-tick-x" dx="-1em" dy="0.3em"
-              textAnchor="end" {...opts}>{this.props.formatYTick(tick)}</text>
-        <line className="line-chart-grid"
-              x1={xScale.range()[0]}
-              x2={xScale.range()[1]}
-              y1={yScale(tick)}
-              y2={yScale(tick)}/>
-      </g>;
-    });
-    return <g>{grid}</g>;
-  },
-
-  renderTicks (xScale, yScale) {
-    let format = xScale.tickFormat(7);
-    let ticks = xScale.ticks(7);
-    ticks = _.initial(ticks).map((tick, index) => {
-      let nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
-      let x = (xScale(tick) + xScale(nextTick)) / 2;
-      let y = yScale.range()[0];
-      return <text key={index} className="line-chart-tick" x={x} y={y} dy="1.5em">{format(tick)}</text>;
-    });
-    return <g>{ticks}</g>;
-  },
-
-  renderLeak (xScale, yScale) {
-    if (!this.props.leakPeriodDate) {
-      return null;
-    }
-    let opts = {
-      x: xScale(this.props.leakPeriodDate),
-      y: yScale.range()[1],
-      width: xScale.range()[1] - xScale(this.props.leakPeriodDate),
-      height: yScale.range()[0] - yScale.range()[1],
-      fill: '#fffae7'
-    };
-    return <rect {...opts}/>;
-  },
-
-  renderLine (xScale, yScale) {
-    let path = d3.svg.line()
-        .x(d => xScale(d.x))
-        .y(d => yScale(d.y))
-        .interpolate(this.props.interpolate);
-    return <path className="line-chart-path" d={path(this.props.data)}/>;
-  },
-
-  renderEvents(xScale, yScale) {
-    let points = this.props.events
-        .map(event => {
-          let snapshot = this.props.data.find(d => d.x.getTime() === event.date.getTime());
-          return _.extend(event, { snapshot });
-        })
-        .filter(event => event.snapshot)
-        .map(event => {
-          let key = `${event.date.getTime()}-${event.snapshot.y}`;
-          let tooltip = [
-            `<span class="nowrap">${event.version}</span>`,
-            `<span class="nowrap">${moment(event.date).format('LL')}</span>`,
-            `<span class="nowrap">${event.snapshot.y ? this.props.formatValue(event.snapshot.y) : '—'}</span>`
-          ].join('<br>');
-          return <circle key={key} className="line-chart-point"
-                         r="4" cx={xScale(event.snapshot.x)} cy={yScale(event.snapshot.y)}
-                         data-toggle="tooltip" data-title={tooltip}/>;
-        });
-    return <g>{points}</g>;
-  },
-
-  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 xScale = d3.time.scale()
-        .domain(d3.extent(this.props.data, d => d.x))
-        .range([0, availableWidth])
-        .clamp(true);
-    let yScale = d3.scale.linear()
-        .range([availableHeight, 0])
-        .domain([0, d3.max(this.props.data, d => d.y)])
-        .nice();
-
-    return <svg className="line-chart" width={this.state.width} height={this.state.height}>
-      <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
-        {this.renderLeak(xScale, yScale)}
-        {this.renderHorizontalGrid(xScale, yScale)}
-        {this.renderTicks(xScale, yScale)}
-        {this.renderLine(xScale, yScale)}
-        {this.renderEvents(xScale, yScale)}
-      </g>
-    </svg>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/shared/drilldown-link.js b/server/sonar-web/src/main/js/components/shared/drilldown-link.js
new file mode 100644 (file)
index 0000000..d149a3d
--- /dev/null
@@ -0,0 +1,91 @@
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { IssuesLink } from './issues-link';
+import { getComponentDrilldownUrl } from '../../helpers/urls';
+
+
+const ISSUE_MEASURES = [
+  'violations',
+  'blocker_violations',
+  'critical_violations',
+  'major_violations',
+  'minor_violations',
+  'info_violations',
+  'new_blocker_violations',
+  'new_critical_violations',
+  'new_major_violations',
+  'new_minor_violations',
+  'new_info_violations',
+  'open_issues',
+  'reopened_issues',
+  'confirmed_issues',
+  'false_positive_issues'
+];
+
+
+export const DrilldownLink = React.createClass({
+  isIssueMeasure() {
+    return ISSUE_MEASURES.indexOf(this.props.metric) !== -1;
+  },
+
+  propsToIssueParams() {
+    let params = {};
+    if (this.props.periodDate) {
+      params.createdAfter = moment(this.props.periodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+    }
+    switch (this.props.metric) {
+      case 'blocker_violations':
+      case 'new_blocker_violations':
+        _.extend(params, { resolved: 'false', severities: 'BLOCKER' });
+        break;
+      case 'critical_violations':
+      case 'new_critical_violations':
+        _.extend(params, { resolved: 'false', severities: 'CRITICAL' });
+        break;
+      case 'major_violations':
+      case 'new_major_violations':
+        _.extend(params, { resolved: 'false', severities: 'MAJOR' });
+        break;
+      case 'minor_violations':
+      case 'new_minor_violations':
+        _.extend(params, { resolved: 'false', severities: 'MINOR' });
+        break;
+      case 'info_violations':
+      case 'new_info_violations':
+        _.extend(params, { resolved: 'false', severities: 'INFO' });
+        break;
+      case 'open_issues':
+        _.extend(params, { resolved: 'false', statuses: 'OPEN' });
+        break;
+      case 'reopened_issues':
+        _.extend(params, { resolved: 'false', statuses: 'REOPENED' });
+        break;
+      case 'confirmed_issues':
+        _.extend(params, { resolved: 'false', statuses: 'CONFIRMED' });
+        break;
+      case 'false_positive_issues':
+        _.extend(params, { resolutions: 'FALSE-POSITIVE' });
+        break;
+      default:
+        _.extend(params, { resolved: 'false' });
+    }
+    return params;
+  },
+
+  renderIssuesLink() {
+    return <IssuesLink component={this.props.component} params={this.propsToIssueParams()}>
+      {this.props.children}
+    </IssuesLink>;
+  },
+
+  render() {
+    if (this.isIssueMeasure()) {
+      return this.renderIssuesLink();
+    }
+
+    let url = getComponentDrilldownUrl(this.props.component, this.props.metric, this.props.period);
+    return <a className={this.props.className} href={url}>{this.props.children}</a>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/shared/issues-link.js b/server/sonar-web/src/main/js/components/shared/issues-link.js
new file mode 100644 (file)
index 0000000..230ead7
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+
+import { getComponentIssuesUrl } from '../../helpers/urls';
+
+
+export const IssuesLink = React.createClass({
+  render() {
+    let url = getComponentIssuesUrl(this.props.component, this.props.params);
+    return <a className={this.props.className} href={url}>{this.props.children}</a>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/shared/quality-gate-link.js b/server/sonar-web/src/main/js/components/shared/quality-gate-link.js
new file mode 100644 (file)
index 0000000..5b05c8c
--- /dev/null
@@ -0,0 +1,9 @@
+import React from 'react';
+
+
+export const QualityGateLink = React.createClass({
+  render() {
+    let url = `${baseUrl}/quality_gates/show/${this.props.gate}`;
+    return <a href={url}>{this.props.children}</a>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/shared/quality-profile-link.js b/server/sonar-web/src/main/js/components/shared/quality-profile-link.js
new file mode 100644 (file)
index 0000000..c7c296b
--- /dev/null
@@ -0,0 +1,9 @@
+import React from 'react';
+
+
+export const QualityProfileLink = React.createClass({
+  render() {
+    let url = `${baseUrl}/profiles/show?key=${encodeURIComponent(this.props.profile)}`;
+    return <a href={url}>{this.props.children}</a>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/shared/rating.js b/server/sonar-web/src/main/js/components/shared/rating.js
new file mode 100644 (file)
index 0000000..791466f
--- /dev/null
@@ -0,0 +1,15 @@
+import React from 'react';
+
+import { formatMeasure } from '../../helpers/measures';
+
+
+export const Rating = React.createClass({
+  render() {
+    if (this.props.value == null || isNaN(this.props.value)) {
+      return null;
+    }
+    let formatted = formatMeasure(this.props.value, 'RATING');
+    let className = 'rating rating-' + formatted;
+    return <span className={className}>{formatted}</span>;
+  }
+});
index bcd74fe38822b704e85303629c5250463fc73944..803bc92954cc7dd7ab8a4da1fbe9b4c88e8a0c9a 100644 (file)
@@ -100,6 +100,10 @@ td.big-spacer-top    { padding-top: 16px; }
   flex: 1;
 }
 
+.space-between {
+  justify-content: space-between !important;
+}
+
 
 // Background Color
 
index 99a414b5448e53b8edde2da7a23cbe0b75b30094..972b04cbee033b1b82a188923f8970f6853ca8a1 100644 (file)
@@ -3,8 +3,6 @@
 @import (reference) "../init/type";
 @import (reference) "../init/links";
 
-@side-padding: 20px;
-
 .overview {
   display: flex;
   flex-wrap: wrap;
@@ -32,7 +30,7 @@
   background-color: @barBackgroundColor;
 
   .overview-title {
-    margin: 0 @side-padding;
+    margin: 0 20px;
   }
 }
 
 }
 
 .overview-gate-condition {
-  padding: 10px @side-padding;
-}
-
-.overview-gate-condition-metric {
-
+  padding: 10px 20px;
 }
 
 .overview-gate-condition-value {
@@ -56,7 +50,7 @@
 }
 
 .overview-gate-warning {
-  margin: 15px @side-padding 0;
+  margin: 15px 20px 0;
 }
 
 /*
   }
 }
 
-/*
- * Cards
- * TODO drop it
- */
-
-.overview-cards {
-  display: flex;
-  flex-wrap: wrap;
-}
-
 /*
  * Meta
  */
@@ -99,7 +83,7 @@
 
 .overview-meta-card {
   min-width: 200px;
-  padding: @side-padding;
+  padding: 20px;
   box-sizing: border-box;
 }
 
  * Domain
  */
 
-.overview-domains {
+.overview-domains-list {
   animation: fadeIn 0.5s forwards;
 }
 
-.overview-domain {
-  margin: 30px @side-padding;
+.overview-cards-list {
+  display: flex;
+
+  & > .overview-card,
+  & > .overview-domain-chart {
+    flex: 1;
+  }
+}
+
+.overview-card {
+  margin: 30px 20px;
 }
 
-.overview-domain-fixed-width {
+.overview-card-fixed-width {
   max-width: 560px;
 }
 
-.overview-domain-header {
+.overview-card-header {
   display: flex;
   align-items: baseline;
   justify-content: space-between;
 }
 
 .overview-domain-measure {
-
 }
 
 .overview-domain-measure-value {
     margin-bottom: 8px;
     flex: 0 1 auto;
     font-weight: 500;
+    text-align: center;
   }
 
   .overview-detailed-measure + .overview-detailed-measure {
   background-color: #fff;
 }
 
+.overview-detailed-measure-rating {
+  border: none !important;
+  background: none;
+
+  .overview-detailed-measure-value { font-size: 30px; }
+}
+
 .overview-detailed-measure-nutshell,
 .overview-detailed-measure-leak,
 .overview-detailed-measure-chart {
  * Charts
  */
 
-.overview-domain-charts {
-  display: flex;
-
-  & > .overview-domain,
-  & > .overview-domain-chart {
-    flex: 1;
-  }
-}
-
 .overview-domain-chart {
   .overview-title {
     display: inline-block;
   }
 }
 
+.overview-domain-chart + .overview-domain-chart {
+  margin-top: 60px;
+}
+
 .overview-bar-chart {
   width: 100%;
   padding-top: 10px;
   padding-bottom: 15px;
-}
 
-.bar-chart-bar {
-  fill: @blue;
-}
+  .bar-chart-bar {
+    fill: @blue;
+  }
 
-.bar-chart-tick {
-  fill: @secondFontColor;
-  font-size: 11px;
-  text-anchor: middle;
-}
+  .bar-chart-tick {
+    fill: @secondFontColor;
+    font-size: 11px;
+    text-anchor: middle;
+  }
 
-.histogram-tick {
-  text-anchor: end;
-}
+  .histogram-tick {
+    text-anchor: end;
+  }
 
-.histogram-value {
-  text-anchor: start;
+  .histogram-value {
+    text-anchor: start;
+  }
 }
 
 .overview-timeline {
     cursor: pointer;
     transition: all 0.2s ease;
 
-    &:hover { fill-opacity: 0.8; }
+    &:hover {
+      fill-opacity: 0.8;
+    }
   }
 
   .bubble-chart-grid {
 }
 
 .overview-donut-chart {
-  display: inline-block;
-  vertical-align: top;
-  margin-right: 8px;
+  position: relative;
+  text-align: center;
+
+  .overview-detailed-measure-value {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+  }
 }
 
+/*
+ * Misc
+ */
 
+.overview-nutshell {
+  background-color: #fff;
+}
+
+.overview-leak {
+  background-color: #fffae7;
+}
 
 /*
  * Responsive Stuff
diff --git a/server/sonar-web/tests/apps/overview-test.js b/server/sonar-web/tests/apps/overview-test.js
deleted file mode 100644 (file)
index 2bfea7f..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import TestUtils from 'react-addons-test-utils';
-import { expect } from 'chai';
-
-import Gate from '../../src/main/js/apps/overview/main/gate/gate';
-import GateConditions from '../../src/main/js/apps/overview/main/gate/gate-conditions';
-import GateCondition from '../../src/main/js/apps/overview/main/gate/gate-condition';
-
-describe('Overview', function () {
-
-  describe('Quality Gate', function () {
-    it('should display a badge', function () {
-      let output = TestUtils.renderIntoDocument(<Gate gate={{ level: 'ERROR', conditions: [] }} component={{ }}/>);
-      TestUtils.findRenderedDOMComponentWithClass(output, 'badge-error');
-    });
-
-    it('should not be displayed', function () {
-      let output = TestUtils.renderIntoDocument(<Gate component={{ }}/>);
-      expect(TestUtils.scryRenderedDOMComponentsWithClass(output, 'overview-gate')).to.be.empty;
-    });
-
-    it('should display empty gate', function () {
-      let output = TestUtils.renderIntoDocument(<Gate component={{ qualifier: 'TRK' }}/>);
-      TestUtils.findRenderedDOMComponentWithClass(output, 'overview-gate');
-      TestUtils.findRenderedDOMComponentWithClass(output, 'overview-gate-warning');
-    });
-
-    it('should filter out passed conditions', function () {
-      const conditions = [
-        { level: 'OK' },
-        { level: 'ERROR', metric: { name: 'error metric' } },
-        { level: 'WARN', metric: { name: 'warn metric' } },
-        { level: 'OK' }
-      ];
-
-      let renderer = TestUtils.createRenderer();
-      renderer.render(<GateConditions gate={{ conditions }} component={{}}/>);
-      let output = renderer.getRenderOutput();
-      expect(output.props.children).to.have.length(2);
-    });
-  });
-
-});
diff --git a/server/sonar-web/tests/apps/overview/components/complexity-distribution-test.js b/server/sonar-web/tests/apps/overview/components/complexity-distribution-test.js
new file mode 100644 (file)
index 0000000..a79cdbf
--- /dev/null
@@ -0,0 +1,41 @@
+import { expect } from 'chai';
+import React from 'react';
+import TestUtils from 'react-addons-test-utils';
+
+import { ComplexityDistribution } from '../../../../src/main/js/apps/overview/components/complexity-distribution';
+
+
+const DISTRIBUTION = '1=11950;2=86;4=77;6=43;8=17;10=12;12=3';
+
+
+describe('ComplexityDistribution', function () {
+  let props;
+
+  beforeEach(function () {
+    let renderer = TestUtils.createRenderer();
+    renderer.render(<ComplexityDistribution distribution={DISTRIBUTION}/>);
+    let output = renderer.getRenderOutput();
+    let child = React.Children.only(output.props.children);
+    props = child.props;
+  });
+
+  it('should pass right data', function () {
+    expect(props.data).to.deep.equal([
+      { x: 0, y: 11950, value: 1 },
+      { x: 1, y: 86, value: 2 },
+      { x: 2, y: 77, value: 4 },
+      { x: 3, y: 43, value: 6 },
+      { x: 4, y: 17, value: 8 },
+      { x: 5, y: 12, value: 10 },
+      { x: 6, y: 3, value: 12 }
+    ]);
+  });
+
+  it('should pass right xTicks', function () {
+    expect(props.xTicks).to.deep.equal([1, 2, 4, 6, 8, 10, 12]);
+  });
+
+  it('should pass right xValues', function () {
+    expect(props.xValues).to.deep.equal(['11,950', '86', '77', '43', '17', '12', '3']);
+  });
+});
diff --git a/server/sonar-web/tests/apps/overview/components/issues-tags-test.js b/server/sonar-web/tests/apps/overview/components/issues-tags-test.js
new file mode 100644 (file)
index 0000000..67d90c7
--- /dev/null
@@ -0,0 +1,45 @@
+import { expect } from 'chai';
+import React from 'react';
+import TestUtils from 'react-addons-test-utils';
+
+import { IssuesTags } from '../../../../src/main/js/apps/overview/components/issues-tags';
+import { WordCloud } from '../../../../src/main/js/components/charts/word-cloud';
+
+
+const COMPONENT = { key: 'component-key' };
+
+const TAGS = [
+  { val: 'first', count: 3 },
+  { val: 'second', count: 7000 },
+  { val: 'third', count: 2 }
+];
+
+
+describe('IssuesTags', function () {
+  it('should pass right data', function () {
+    let renderer = TestUtils.createRenderer();
+    renderer.render(<IssuesTags tags={TAGS} component={COMPONENT}/>);
+    let output = renderer.getRenderOutput();
+    expect(output.type).to.equal(WordCloud);
+    expect(output.props.items).to.deep.equal([
+      {
+        "link": '/component_issues?id=component-key#resolved=false|tags=first',
+        "size": 3,
+        "text": 'first',
+        "tooltip": 'Issues: 3'
+      },
+      {
+        "link": '/component_issues?id=component-key#resolved=false|tags=second',
+        "size": 7000,
+        "text": 'second',
+        "tooltip": 'Issues: 7k'
+      },
+      {
+        "link": '/component_issues?id=component-key#resolved=false|tags=third',
+        "size": 2,
+        "text": 'third',
+        "tooltip": 'Issues: 2'
+      }
+    ]);
+  });
+});
diff --git a/server/sonar-web/tests/apps/overview/components/language-distribution-test.js b/server/sonar-web/tests/apps/overview/components/language-distribution-test.js
new file mode 100644 (file)
index 0000000..a54ba2b
--- /dev/null
@@ -0,0 +1,38 @@
+import { expect } from 'chai';
+import React from 'react';
+import TestUtils from 'react-addons-test-utils';
+
+import { LanguageDistribution } from '../../../../src/main/js/apps/overview/components/language-distribution';
+
+
+const DISTRIBUTION = '<null>=17345;java=194342;js=20984';
+const LINES = 1000000;
+
+
+describe('LanguageDistribution', function () {
+  let props;
+
+  beforeEach(function () {
+    let renderer = TestUtils.createRenderer();
+    renderer.render(<LanguageDistribution distribution={DISTRIBUTION} lines={LINES}/>);
+    let output = renderer.getRenderOutput();
+    let child = React.Children.only(output.props.children);
+    props = child.props;
+  });
+
+  it('should pass right data', function () {
+    expect(props.data).to.deep.equal([
+      { x: 194342, y: 1, value: 'java' },
+      { x: 20984, y: 2, value: 'js' },
+      { x: 17345, y: 0, value: '<null>' }
+    ]);
+  });
+
+  it('should pass right yTicks', function () {
+    expect(props.yTicks).to.deep.equal(['java', 'js', '<null>']);
+  });
+
+  it('should pass right yValues', function () {
+    expect(props.yValues).to.deep.equal(['19.4%', '2.1%', '1.7%']);
+  });
+});
diff --git a/server/sonar-web/tests/apps/overview/components/legend-test.js b/server/sonar-web/tests/apps/overview/components/legend-test.js
new file mode 100644 (file)
index 0000000..d4c877e
--- /dev/null
@@ -0,0 +1,26 @@
+import { expect } from 'chai';
+import React from 'react';
+import TestUtils from 'react-addons-test-utils';
+
+import { Legend } from '../../../../src/main/js/apps/overview/components/legend';
+
+
+const DATE = new Date(2015, 3, 7);
+const LABEL = 'since 1.0';
+
+
+describe('Legend', function () {
+  it('should not render', function () {
+    let renderer = TestUtils.createRenderer();
+    renderer.render(<Legend/>);
+    let output = renderer.getRenderOutput();
+    expect(output).to.be.null;
+  });
+
+  it('should render', function () {
+    let renderer = TestUtils.createRenderer();
+    renderer.render(<Legend leakPeriodDate={DATE} leakPeriodLabel={LABEL}/>);
+    let output = renderer.getRenderOutput();
+    expect(output).to.not.be.null;
+  });
+});
diff --git a/server/sonar-web/tests/apps/overview/helpers/metrics-test.js b/server/sonar-web/tests/apps/overview/helpers/metrics-test.js
new file mode 100644 (file)
index 0000000..e090ea5
--- /dev/null
@@ -0,0 +1,110 @@
+import { expect } from 'chai';
+
+import { filterMetrics, filterMetricsForDomains, getShortType, getMetricName } from
+    '../../../../src/main/js/apps/overview/helpers/metrics';
+
+
+const METRICS = [
+  { key: 'normal_metric', type: 'INT', hidden: false },
+  { key: 'hidden_metric', type: 'INT', hidden: true },
+  { key: 'DATA_metric', type: 'DATA', hidden: false },
+  { key: 'DISTRIB_metric', type: 'DISTRIB', hidden: false },
+  { key: 'new_metric', type: 'FLOAT', hidden: false }
+];
+
+
+describe('Overview Helpers', function () {
+  describe('Metrics', function () {
+
+    describe('#filterMetrics', function () {
+      it('should filter out hidden metrics', function () {
+        let metrics = [
+          { key: 'normal_metric', type: 'INT', hidden: false },
+          { key: 'hidden_metric', type: 'INT', hidden: true }
+        ];
+        expect(filterMetrics(metrics)).to.have.length(1);
+      });
+
+      it('should filter out DATA and DISTRIB metrics', function () {
+        let metrics = [
+          { key: 'normal_metric', type: 'INT', hidden: false },
+          { key: 'DATA_metric', type: 'DATA', hidden: false },
+          { key: 'DISTRIB_metric', type: 'DISTRIB', hidden: false }
+        ];
+        expect(filterMetrics(metrics)).to.have.length(1);
+      });
+
+      it('should filter out differential metrics', function () {
+        let metrics = [
+          { key: 'normal_metric', type: 'INT', hidden: false },
+          { key: 'new_metric', type: 'FLOAT', hidden: false }
+        ];
+        expect(filterMetrics(metrics)).to.have.length(1);
+      });
+    });
+
+    describe('#filterMetricsForDomains', function () {
+      it('should filter out hidden metrics', function () {
+        let metrics = [
+          { key: 'normal_metric', type: 'INT', hidden: false, domain: 'first' },
+          { key: 'hidden_metric', type: 'INT', hidden: true, domain: 'first' }
+        ];
+        expect(filterMetricsForDomains(metrics, ['first'])).to.have.length(1);
+      });
+
+      it('should filter out DATA and DISTRIB metrics', function () {
+        let metrics = [
+          { key: 'normal_metric', type: 'INT', hidden: false, domain: 'first' },
+          { key: 'DATA_metric', type: 'DATA', hidden: false, domain: 'first' },
+          { key: 'DISTRIB_metric', type: 'DISTRIB', hidden: false, domain: 'first' }
+        ];
+        expect(filterMetricsForDomains(metrics, ['first'])).to.have.length(1);
+      });
+
+      it('should filter out differential metrics', function () {
+        let metrics = [
+          { key: 'normal_metric', type: 'INT', hidden: false, domain: 'first' },
+          { key: 'new_metric', type: 'FLOAT', hidden: false, domain: 'first' }
+        ];
+        expect(filterMetricsForDomains(metrics, ['first'])).to.have.length(1);
+      });
+
+      it('should filter metrics by domains', function () {
+        let metrics = [
+          { key: 'normal_metric', type: 'INT', hidden: false, domain: 'first' },
+          { key: 'normal_metric1', type: 'INT', hidden: false, domain: 'second' },
+          { key: 'normal_metric2', type: 'INT', hidden: false, domain: 'third' },
+          { key: 'normal_metric3', type: 'INT', hidden: false, domain: 'second' }
+        ];
+        expect(filterMetricsForDomains(metrics, ['first', 'second'])).to.have.length(3);
+      });
+    });
+
+
+    describe('#getShortType', function () {
+      it('should shorten INT', function () {
+        expect(getShortType('INT')).to.equal('SHORT_INT');
+      });
+
+      it('should shorten WORK_DUR', function () {
+        expect(getShortType('WORK_DUR')).to.equal('SHORT_WORK_DUR');
+      });
+
+      it('should not shorten FLOAT', function () {
+        expect(getShortType('FLOAT')).to.equal('FLOAT');
+      });
+
+      it('should not shorten PERCENT', function () {
+        expect(getShortType('PERCENT')).to.equal('PERCENT');
+      });
+    });
+
+
+    describe('#getMetricName', function () {
+      it('should return metric name', function () {
+        expect(getMetricName('metric_name')).to.equal('overview.metric.metric_name');
+      });
+    });
+
+  });
+});
diff --git a/server/sonar-web/tests/apps/overview/helpers/periods-test.js b/server/sonar-web/tests/apps/overview/helpers/periods-test.js
new file mode 100644 (file)
index 0000000..6dcd1ff
--- /dev/null
@@ -0,0 +1,57 @@
+import chai from 'chai';
+import { expect } from 'chai';
+chai.use(require('chai-datetime'));
+
+import { getPeriodDate, getPeriodLabel } from '../../../../src/main/js/apps/overview/helpers/periods';
+
+
+const PERIOD = {
+  date: '2015-09-09T00:00:00+0200',
+  index: '1',
+  mode: 'previous_version',
+  modeParam: '1.7'
+};
+
+const PERIOD_WITHOUT_VERSION = {
+  date: '2015-09-09T00:00:00+0200',
+  index: '1',
+  mode: 'previous_version',
+  modeParam: ''
+};
+
+
+describe('Overview Helpers', function () {
+  describe('Periods', function () {
+
+    describe('#getPeriodDate', function () {
+      it('should return date', function () {
+        let result = getPeriodDate([PERIOD], PERIOD.index);
+        expect(result).to.equalDate(new Date(2015, 8, 9));
+      });
+
+      it('should return null', function () {
+        let result = getPeriodDate([], '1');
+        expect(result).to.be.null;
+      });
+    });
+
+
+    describe('#getPeriodLabel', function () {
+      it('should return label', function () {
+        let result = getPeriodLabel([PERIOD], PERIOD.index);
+        expect(result).to.equal('overview.period.previous_version.1.7');
+      });
+
+      it('should return "since previous version"', function () {
+        let result = getPeriodLabel([PERIOD_WITHOUT_VERSION], PERIOD_WITHOUT_VERSION.index);
+        expect(result).to.equal('overview.period.previous_version_only_date');
+      });
+
+      it('should return null', function () {
+        let result = getPeriodLabel([], '1');
+        expect(result).to.be.null;
+      });
+    });
+
+  });
+});
diff --git a/server/sonar-web/tests/apps/overview/overview-test.js b/server/sonar-web/tests/apps/overview/overview-test.js
new file mode 100644 (file)
index 0000000..2b7ae01
--- /dev/null
@@ -0,0 +1,51 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import TestUtils from 'react-addons-test-utils';
+import { expect } from 'chai';
+
+import Gate from '../../../src/main/js/apps/overview/gate/gate';
+import GateConditions from '../../../src/main/js/apps/overview/gate/gate-conditions';
+import GateCondition from '../../../src/main/js/apps/overview/gate/gate-condition';
+
+describe('Overview', function () {
+
+  describe('Quality Gate', function () {
+    it('should display a badge', function () {
+      let output = TestUtils.renderIntoDocument(<Gate gate={{ level: 'ERROR', conditions: [] }} component={{ }}/>);
+      TestUtils.findRenderedDOMComponentWithClass(output, 'badge-error');
+    });
+
+    it('should not be displayed', function () {
+      let output = TestUtils.renderIntoDocument(<Gate component={{ }}/>);
+      expect(TestUtils.scryRenderedDOMComponentsWithClass(output, 'overview-gate')).to.be.empty;
+    });
+
+    it('should display empty gate', function () {
+      let output = TestUtils.renderIntoDocument(<Gate component={{ qualifier: 'TRK' }}/>);
+      TestUtils.findRenderedDOMComponentWithClass(output, 'overview-gate');
+      TestUtils.findRenderedDOMComponentWithClass(output, 'overview-gate-warning');
+    });
+
+    it('should filter out passed conditions', function () {
+      const conditions = [
+        { level: 'OK' },
+        { level: 'ERROR', metric: { name: 'error metric' } },
+        { level: 'WARN', metric: { name: 'warn metric' } },
+        { level: 'OK' }
+      ];
+
+      let renderer = TestUtils.createRenderer();
+      renderer.render(<GateConditions gate={{ conditions }} component={{}}/>);
+      let output = renderer.getRenderOutput();
+      expect(output.props.children).to.have.length(2);
+    });
+  });
+
+
+  describe('Helpers', function () {
+    describe('Periods', function () {
+
+    });
+  });
+
+});