]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6359 add detailed "Issues & Technical Debt" panel for the "Overview" main page
authorStas Vilchik <vilchiks@gmail.com>
Tue, 20 Oct 2015 14:54:26 +0000 (16:54 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Fri, 23 Oct 2015 09:17:30 +0000 (11:17 +0200)
63 files changed:
server/sonar-web/package.json
server/sonar-web/src/main/js/api/components.js
server/sonar-web/src/main/js/api/events.js [new file with mode: 0644]
server/sonar-web/src/main/js/api/issues.js [new file with mode: 0644]
server/sonar-web/src/main/js/api/time-machine.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/app.js
server/sonar-web/src/main/js/apps/overview/card.js [deleted file]
server/sonar-web/src/main/js/apps/overview/cards.js [deleted file]
server/sonar-web/src/main/js/apps/overview/domain/header.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/empty.js [deleted file]
server/sonar-web/src/main/js/apps/overview/formatting.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate-condition.js [deleted file]
server/sonar-web/src/main/js/apps/overview/gate-conditions.js [deleted file]
server/sonar-web/src/main/js/apps/overview/gate-empty.js [deleted file]
server/sonar-web/src/main/js/apps/overview/gate.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/card.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/cards.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/empty.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/gate-condition.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/gate-empty.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/gate.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/leak-dups.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/leak-issues.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/leak-size.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/leak.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/main.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/nutshell.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/issues/assignees.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/issues/bubble-chart.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/issues/main.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/issues/severities.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/issues/tags.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/issues/timeline.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/issues/treemap.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak-coverage.js [deleted file]
server/sonar-web/src/main/js/apps/overview/leak-dups.js [deleted file]
server/sonar-web/src/main/js/apps/overview/leak-issues.js [deleted file]
server/sonar-web/src/main/js/apps/overview/leak-size.js [deleted file]
server/sonar-web/src/main/js/apps/overview/leak.js [deleted file]
server/sonar-web/src/main/js/apps/overview/main.js
server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js [deleted file]
server/sonar-web/src/main/js/apps/overview/nutshell-dups.js [deleted file]
server/sonar-web/src/main/js/apps/overview/nutshell-issues.js [deleted file]
server/sonar-web/src/main/js/apps/overview/nutshell-size.js [deleted file]
server/sonar-web/src/main/js/apps/overview/nutshell.js [deleted file]
server/sonar-web/src/main/js/components/charts/bubble-chart.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/line-chart.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/timeline.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/treemap.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/word-cloud.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/assignee-helper.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/severity-helper.js
server/sonar-web/src/main/js/helpers/Url.js
server/sonar-web/src/main/less/components/columns.less
server/sonar-web/src/main/less/components/graphics.less
server/sonar-web/src/main/less/components/tooltips.less
server/sonar-web/src/main/less/pages/overview.less

index 2d21537cd6d80d2b3874ac5f6c8e3633bdbdcbee..ba159c70afbd05c811998abbdaf723e3e5c127a0 100644 (file)
@@ -13,7 +13,9 @@
     "browserify": "11.2.0",
     "browserify-shim": "3.8.10",
     "chai": "3.3.0",
+    "classnames": "^2.2.0",
     "del": "2.0.2",
+    "document-offset": "^1.0.4",
     "event-stream": "3.3.1",
     "glob": "5.0.15",
     "gulp": "3.9.0",
index 8fe69f9c6d2619a20e3f64c5f0c8b9fdd691bc8c..a4eb949a2d7fa43a184f239b814e300ba17ed980 100644 (file)
@@ -1,3 +1,4 @@
+import { getJSON } from '../helpers/request.js';
 import $ from 'jquery';
 
 export function getComponents (data) {
@@ -25,3 +26,21 @@ export function createProject (options) {
   options.type = 'POST';
   return $.ajax(options);
 }
+
+export function getChildren (componentKey, metrics = []) {
+  let url = baseUrl + '/api/resources/index';
+  let data = { resource: componentKey, metrics: metrics.join(','), depth: 1 };
+  return getJSON(url, data);
+}
+
+export function getFiles (componentKey, metrics = []) {
+  // due to the limitation of the WS we can not ask qualifiers=FIL,
+  // in this case the WS does not return measures
+  // so the filtering by a qualifier is done manually
+
+  let url = baseUrl + '/api/resources/index';
+  let data = { resource: componentKey, metrics: metrics.join(','), depth: -1 };
+  return getJSON(url, data).then(r => {
+    return r.filter(component => component.qualifier === 'FIL');
+  });
+}
diff --git a/server/sonar-web/src/main/js/api/events.js b/server/sonar-web/src/main/js/api/events.js
new file mode 100644 (file)
index 0000000..11852be
--- /dev/null
@@ -0,0 +1,7 @@
+import { getJSON } from '../helpers/request.js';
+
+export function getEvents (componentKey, categories) {
+  let url = baseUrl + '/api/events';
+  let data = { resource: componentKey, categories };
+  return getJSON(url, data);
+}
diff --git a/server/sonar-web/src/main/js/api/issues.js b/server/sonar-web/src/main/js/api/issues.js
new file mode 100644 (file)
index 0000000..5d2259e
--- /dev/null
@@ -0,0 +1,27 @@
+import _ from 'underscore';
+import { getJSON } from '../helpers/request.js';
+
+function getFacet (query, facet) {
+  let url = baseUrl + '/api/issues/search';
+  let data = _.extend({}, query, { facets: facet, ps: 1, additionalFields: '_all' });
+  return getJSON(url, data).then(r => {
+    return { facet: r.facets[0].values, response: r };
+  });
+}
+
+export function getSeverities (query) {
+  return getFacet(query, 'severities').then(r => r.facet);
+}
+
+export function getTags (query) {
+  return getFacet(query, 'tags').then(r => r.facet);
+}
+
+export function getAssignees (query) {
+  return getFacet(query, 'assignees').then(r => {
+    return r.facet.map(item => {
+      let user = _.findWhere(r.response.users, { login: item.val });
+      return _.extend(item, { user });
+    });
+  });
+}
diff --git a/server/sonar-web/src/main/js/api/time-machine.js b/server/sonar-web/src/main/js/api/time-machine.js
new file mode 100644 (file)
index 0000000..200cfaa
--- /dev/null
@@ -0,0 +1,7 @@
+import { getJSON } from '../helpers/request.js';
+
+export function getTimeMachineData (componentKey, metrics) {
+  let url = baseUrl + '/api/timemachine/index';
+  let data = { resource: componentKey, metrics };
+  return getJSON(url, data);
+}
index 80bc990a9656672838156cbeb2e9c76d98cca7aa..6b661b9b592f8207936de87502ebd01da17e6b84 100644 (file)
@@ -2,24 +2,14 @@ import $ from 'jquery';
 import _ from 'underscore';
 import React from 'react';
 import Main from './main';
-import Empty from './empty';
 
 class App {
-  start(options) {
+  start (options) {
     let opts = _.extend({}, options, window.sonarqube.overview);
     _.extend(opts.component, options.component);
     $('html').toggleClass('dashboard-page', opts.component.hasSnapshot);
-    window.requestMessages().done(() => {
-      let el = document.querySelector(opts.el);
-      let inner = opts.component.hasSnapshot ? (
-          <Main
-              component={opts.component}
-              gate={opts.gate}
-              measures={opts.measures}
-              leak={opts.leak}/>
-      ) : <Empty/>;
-      React.render(inner, el);
-    });
+    let el = document.querySelector(opts.el);
+    React.render(<Main {...opts}/>, el);
   }
 }
 
diff --git a/server/sonar-web/src/main/js/apps/overview/card.js b/server/sonar-web/src/main/js/apps/overview/card.js
deleted file mode 100644 (file)
index a221462..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  render() {
-    return <li className="overview-card">{this.props.children}</li>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/cards.js b/server/sonar-web/src/main/js/apps/overview/cards.js
deleted file mode 100644 (file)
index 3d69cf8..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  render() {
-    return <ul className="overview-cards">{this.props.children}</ul>;
-  }
-});
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
new file mode 100644 (file)
index 0000000..16af08d
--- /dev/null
@@ -0,0 +1,7 @@
+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/empty.js b/server/sonar-web/src/main/js/apps/overview/empty.js
deleted file mode 100644 (file)
index 78c5320..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  render() {
-    return (
-        <div className="panel">
-          <div className="alert alert-warning">
-            {window.t('provisioning.no_analysis')}
-          </div>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/formatting.js b/server/sonar-web/src/main/js/apps/overview/formatting.js
new file mode 100644 (file)
index 0000000..7a91cbc
--- /dev/null
@@ -0,0 +1,21 @@
+const METRIC_TYPES = {
+  'violations': 'SHORT_INT',
+  'blocker_violations': 'SHORT_INT',
+  'critical_violations': 'SHORT_INT',
+  'major_violations': 'SHORT_INT',
+  'minor_violations': 'SHORT_INT',
+  'info_violations': 'SHORT_INT',
+  'confirmed_issues': 'SHORT_INT',
+  'false_positive_issues': 'SHORT_INT',
+  'open_issues': 'SHORT_INT',
+  'reopened_issues': 'SHORT_INT',
+  'sqale_index': 'SHORT_WORK_DUR',
+  'sqale_debt_ratio': 'PERCENT',
+  'sqale_rating': 'RATING',
+  'lines': 'SHORT_INT'
+};
+
+export function formatMeasure (value, metric) {
+  let type = METRIC_TYPES[metric];
+  return type ? window.formatMeasure(value, type) : value;
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/gate-condition.js
deleted file mode 100644 (file)
index 2cc4282..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import Measure from './helpers/measure';
-import { periodLabel, 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,
-        iconClassName = 'icon-alert-' + this.props.condition.level.toLowerCase(),
-        period = this.props.condition.period ?
-            `(${periodLabel(this.props.component.periods, this.props.condition.period)})` : null,
-        periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period);
-
-    return (
-        <div>
-          <h4 className="overview-gate-condition-metric">{metricName}<br/><span className="nowrap">{period}</span></h4>
-          <div className="overview-gate-condition-value">
-            <i className={iconClassName}/>&nbsp;
-            <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name}
-                           period={this.props.condition.period} periodDate={periodDate}>
-              <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/>
-            </DrilldownLink>&nbsp;
-            <span className="overview-gate-condition-itself">
-              {window.t('quality_gates.operator', this.props.condition.op, 'short')}&nbsp;
-              <Measure value={threshold} type={this.props.condition.metric.type}/>
-            </span>
-          </div>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/gate-conditions.js
deleted file mode 100644 (file)
index 65b92e4..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import Cards from './cards';
-import Card from './card';
-import GateCondition from './gate-condition';
-
-export default React.createClass({
-  render() {
-    let conditions = this.props.gate.conditions
-        .filter((c) => {
-          return c.level !== 'OK';
-        })
-        .map((c) => {
-          return (
-              <Card key={c.metric.name}>
-                <GateCondition condition={c} component={this.props.component}/>
-              </Card>
-          );
-        });
-    return <Cards>{conditions}</Cards>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/gate-empty.js
deleted file mode 100644 (file)
index 56cdb55..0000000
+++ /dev/null
@@ -1,14 +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="big-spacer-top">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.js b/server/sonar-web/src/main/js/apps/overview/gate.js
deleted file mode 100644 (file)
index 2436d2d..0000000
+++ /dev/null
@@ -1,25 +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
-        badgeClassName = 'badge badge-' + this.props.gate.level.toLowerCase(),
-        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/general/card.js b/server/sonar-web/src/main/js/apps/overview/general/card.js
new file mode 100644 (file)
index 0000000..1d1d563
--- /dev/null
@@ -0,0 +1,19 @@
+import React from 'react';
+import classNames from 'classnames';
+
+export default React.createClass({
+  handleClick() {
+    if (this.props.linkTo) {
+      let tab = React.findDOMNode(this);
+      this.props.onRoute(this.props.linkTo, tab);
+    }
+  },
+
+  render() {
+    let classes = classNames('overview-card', {
+      'overview-card-section': this.props.linkTo,
+      'active': this.props.active
+    });
+    return <li onClick={this.handleClick} className={classes}>{this.props.children}</li>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/cards.js b/server/sonar-web/src/main/js/apps/overview/general/cards.js
new file mode 100644 (file)
index 0000000..3d69cf8
--- /dev/null
@@ -0,0 +1,7 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    return <ul className="overview-cards">{this.props.children}</ul>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/empty.js b/server/sonar-web/src/main/js/apps/overview/general/empty.js
new file mode 100644 (file)
index 0000000..78c5320
--- /dev/null
@@ -0,0 +1,13 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    return (
+        <div className="panel">
+          <div className="alert alert-warning">
+            {window.t('provisioning.no_analysis')}
+          </div>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/general/gate-condition.js
new file mode 100644 (file)
index 0000000..eb947c8
--- /dev/null
@@ -0,0 +1,33 @@
+import React from 'react';
+import Measure from './../helpers/measure';
+import { periodLabel, 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,
+        iconClassName = 'icon-alert-' + this.props.condition.level.toLowerCase(),
+        period = this.props.condition.period ?
+            `(${periodLabel(this.props.component.periods, this.props.condition.period)})` : null,
+        periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period);
+
+    return (
+        <div>
+          <h4 className="overview-gate-condition-metric">{metricName}<br/><span className="nowrap">{period}</span></h4>
+          <div className="overview-gate-condition-value">
+            <i className={iconClassName}/>&nbsp;
+            <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name}
+                           period={this.props.condition.period} periodDate={periodDate}>
+              <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/>
+            </DrilldownLink>&nbsp;
+            <span className="overview-gate-condition-itself">
+              {window.t('quality_gates.operator', this.props.condition.op, 'short')}&nbsp;
+              <Measure value={threshold} type={this.props.condition.metric.type}/>
+            </span>
+          </div>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js
new file mode 100644 (file)
index 0000000..65b92e4
--- /dev/null
@@ -0,0 +1,21 @@
+import React from 'react';
+import Cards from './cards';
+import Card from './card';
+import GateCondition from './gate-condition';
+
+export default React.createClass({
+  render() {
+    let conditions = this.props.gate.conditions
+        .filter((c) => {
+          return c.level !== 'OK';
+        })
+        .map((c) => {
+          return (
+              <Card key={c.metric.name}>
+                <GateCondition condition={c} component={this.props.component}/>
+              </Card>
+          );
+        });
+    return <Cards>{conditions}</Cards>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/general/gate-empty.js
new file mode 100644 (file)
index 0000000..56cdb55
--- /dev/null
@@ -0,0 +1,14 @@
+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="big-spacer-top">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/general/gate.js b/server/sonar-web/src/main/js/apps/overview/general/gate.js
new file mode 100644 (file)
index 0000000..2436d2d
--- /dev/null
@@ -0,0 +1,25 @@
+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
+        badgeClassName = 'badge badge-' + this.props.gate.level.toLowerCase(),
+        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/general/leak-coverage.js b/server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js
new file mode 100644 (file)
index 0000000..5af186b
--- /dev/null
@@ -0,0 +1,46 @@
+import React from 'react';
+import Card from './card';
+import Measure from './../helpers/measure';
+import MeasureVariation from './../helpers/measure-variation';
+import DrilldownLink from './../helpers/drilldown-link';
+import Donut from './../helpers/donut';
+
+export default React.createClass({
+  render() {
+    let
+        newCoverage = parseInt(this.props.leak.newCoverage, 10),
+        tests = this.props.leak.tests,
+        donutData = [
+          { value: newCoverage, fill: '#85bb43' },
+          { value: 100 - newCoverage, fill: '#d4333f' }
+        ];
+
+    if (newCoverage == null || isNaN(newCoverage)) {
+      return null;
+    }
+
+    return (
+        <Card>
+          <div className="measures">
+            <div className="measures-chart">
+              <Donut data={donutData} size="47"/>
+            </div>
+            <div className="measure measure-big" data-metric="new_coverage">
+              <span className="measure-value">
+                <DrilldownLink component={this.props.component.key} metric="new_coverage" period="3">
+                  <Measure value={newCoverage} type="PERCENT"/>
+                </DrilldownLink>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.new_coverage')}</span>
+            </div>
+          </div>
+          <ul className="list-inline big-spacer-top measures-chart-indent">
+            <li>
+              <span><MeasureVariation value={tests} type="SHORT_INT"/></span>&nbsp;
+              <span>{window.t('overview.metric.tests')}</span>
+            </li>
+          </ul>
+        </Card>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-dups.js b/server/sonar-web/src/main/js/apps/overview/general/leak-dups.js
new file mode 100644 (file)
index 0000000..bdd2188
--- /dev/null
@@ -0,0 +1,42 @@
+import React from 'react';
+import Card from './card';
+import MeasureVariation from './../helpers/measure-variation';
+import Donut from './../helpers/donut';
+
+export default React.createClass({
+  render() {
+    let
+        density = this.props.leak.duplications,
+        lines = this.props.leak.duplicatedLines,
+        donutData = [
+          { value: density, fill: '#f3ca8e' },
+          { value: 100 - density, fill: '#e6e6e6' }
+        ];
+
+    if (density == null) {
+      return null;
+    }
+
+    return (
+        <Card>
+          <div className="measures">
+            <div className="measures-chart">
+              <Donut data={donutData} size="47"/>
+            </div>
+            <div className="measure measure-big" data-metric="duplicated_lines_density">
+              <span className="measure-value">
+                <MeasureVariation value={density} type="PERCENT"/>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.duplications')}</span>
+            </div>
+          </div>
+          <ul className="list-inline big-spacer-top measures-chart-indent">
+            <li>
+              <span><MeasureVariation value={lines} type="SHORT_INT"/></span>&nbsp;
+              <span>{window.t('overview.metric.duplicated_lines')}</span>
+            </li>
+          </ul>
+        </Card>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-issues.js b/server/sonar-web/src/main/js/apps/overview/general/leak-issues.js
new file mode 100644 (file)
index 0000000..fc33b7c
--- /dev/null
@@ -0,0 +1,69 @@
+import moment from 'moment';
+import React from 'react';
+import Card from './card';
+import Measure from './../helpers/measure';
+import MeasureVariation from './../helpers/measure-variation';
+import IssuesLink from './../helpers/issues-link';
+import SeverityIcon from '../../../components/shared/severity-icon';
+import StatusIcon from '../../../components/shared/status-icon';
+import {getPeriodDate} from './../helpers/period-label';
+
+export default React.createClass({
+  render() {
+    let
+        newDebt = this.props.leak.newDebt,
+        issues = this.props.leak.newIssues,
+        blockerIssues = this.props.leak.newBlockerIssues,
+        criticalIssues = this.props.leak.newCriticalIssues,
+        issuesToReview = this.props.leak.newOpenIssues + this.props.leak.newReopenedIssues,
+        periodDate = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+    return (
+        <Card>
+          <div className="measures">
+            <div className="measure measure-big" data-metric="sqale_index">
+              <span className="measure-value">
+                <IssuesLink component={this.props.component.key}
+                            params={{ resolved: 'false', createdAfter: periodDate, facetMode: 'debt' }}>
+                  <Measure value={newDebt} type="SHORT_WORK_DUR"/>
+                </IssuesLink>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.new_debt')}</span>
+            </div>
+            <div className="measure measure-big" data-metric="violations">
+              <span className="measure-value">
+                <IssuesLink component={this.props.component.key}
+                            params={{ resolved: 'false', createdAfter: periodDate }}>
+                  <Measure value={issues} type="SHORT_INT"/>
+                </IssuesLink>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.new_issues')}</span>
+            </div>
+          </div>
+          <ul className="list-inline big-spacer-top">
+            <li>
+              <span><SeverityIcon severity="BLOCKER"/></span>&nbsp;
+              <IssuesLink component={this.props.component.key}
+                          params={{ resolved: 'false', createdAfter: periodDate, severities: 'BLOCKER' }}>
+                <MeasureVariation value={blockerIssues} type="SHORT_INT"/>
+              </IssuesLink>
+            </li>
+            <li>
+              <span><SeverityIcon severity="CRITICAL"/></span>&nbsp;
+              <IssuesLink component={this.props.component.key}
+                          params={{ resolved: 'false', createdAfter: periodDate, severities: 'CRITICAL' }}>
+                <MeasureVariation value={criticalIssues} type="SHORT_INT"/>
+              </IssuesLink>
+            </li>
+            <li>
+              <span><StatusIcon status="OPEN"/></span>&nbsp;
+              <IssuesLink component={this.props.component.key}
+                          params={{ resolved: 'false', createdAfter: periodDate, statuses: 'OPEN,REOPENED' }}>
+                <MeasureVariation value={issuesToReview} type="SHORT_INT"/>
+              </IssuesLink>
+            </li>
+          </ul>
+        </Card>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-size.js b/server/sonar-web/src/main/js/apps/overview/general/leak-size.js
new file mode 100644 (file)
index 0000000..7716237
--- /dev/null
@@ -0,0 +1,30 @@
+import React from 'react';
+import Card from './card';
+import MeasureVariation from './../helpers/measure-variation';
+
+export default React.createClass({
+  render() {
+    let
+        lines = this.props.leak.lines,
+        files = this.props.leak.files;
+
+    return (
+        <Card>
+          <div className="measures">
+            <div className="measure measure-big" data-metric="lines">
+              <span className="measure-value">
+                <MeasureVariation value={lines} type="SHORT_INT"/>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.lines')}</span>
+            </div>
+            <div className="measure measure-big" data-metric="files">
+              <span className="measure-value">
+                <MeasureVariation value={files} type="SHORT_INT"/>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.files')}</span>
+            </div>
+          </div>
+        </Card>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak.js b/server/sonar-web/src/main/js/apps/overview/general/leak.js
new file mode 100644 (file)
index 0000000..20db93f
--- /dev/null
@@ -0,0 +1,33 @@
+import _ from 'underscore';
+import React from 'react';
+import Cards from './cards';
+import LeakIssues from './leak-issues';
+import LeakCoverage from './leak-coverage';
+import LeakSize from './leak-size';
+import LeakDups from './leak-dups';
+import {periodLabel} from './../helpers/period-label';
+
+export default React.createClass({
+  render() {
+    if (_.size(this.props.component.periods) < 3) {
+      return null;
+    }
+
+    let period = periodLabel(this.props.component.periods, '3');
+
+    return (
+        <div className="overview-leak">
+          <h2 className="overview-title">
+            {window.t('overview.water_leak')}
+            <span className="overview-leak-period">{period}</span>
+          </h2>
+          <Cards>
+            <LeakIssues component={this.props.component} leak={this.props.leak} measures={this.props.measures}/>
+            <LeakCoverage component={this.props.component} leak={this.props.leak}/>
+            <LeakDups component={this.props.component} leak={this.props.leak}/>
+            <LeakSize component={this.props.component} leak={this.props.leak}/>
+          </Cards>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/main.js b/server/sonar-web/src/main/js/apps/overview/general/main.js
new file mode 100644 (file)
index 0000000..a55eacf
--- /dev/null
@@ -0,0 +1,97 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+import Gate from './gate';
+import Leak from './leak';
+import Nutshell from './nutshell';
+import {getPeriodDate} from './../helpers/period-label';
+
+export default React.createClass({
+  getInitialState() {
+    return { leak: this.props.leak, measures: this.props.measures };
+  },
+
+  componentDidMount() {
+    if (this._hasWaterLeak()) {
+      this.requestLeakIssues();
+      this.requestLeakDebt();
+    }
+    this.requestNutshellIssues();
+    this.requestNutshellDebt();
+  },
+
+  _hasWaterLeak() {
+    return !!_.findWhere(this.props.component.periods, { index: '3' });
+  },
+
+  _requestIssues(data) {
+    let url = `${baseUrl}/api/issues/search`;
+    data.ps = 1;
+    data.componentUuids = this.props.component.id;
+    return $.get(url, data);
+  },
+
+  requestLeakIssues() {
+    let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ');
+    this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities,statuses' }).done(r => {
+      let
+          severitiesFacet = _.findWhere(r.facets, { property: 'severities' }).values,
+          statusesFacet = _.findWhere(r.facets, { property: 'statuses' }).values;
+
+      this.setState({
+        leak: _.extend({}, this.state.leak, {
+          newIssues: r.total,
+          newBlockerIssues: _.findWhere(severitiesFacet, { val: 'BLOCKER' }).count,
+          newCriticalIssues: _.findWhere(severitiesFacet, { val: 'CRITICAL' }).count,
+          newOpenIssues: _.findWhere(statusesFacet, { val: 'OPEN' }).count,
+          newReopenedIssues: _.findWhere(statusesFacet, { val: 'REOPENED' }).count
+        })
+      });
+    });
+  },
+
+  requestNutshellIssues() {
+    this._requestIssues({ resolved: 'false', facets: 'severities,statuses' }).done(r => {
+      let
+          severitiesFacet = _.findWhere(r.facets, { property: 'severities' }).values,
+          statusesFacet = _.findWhere(r.facets, { property: 'statuses' }).values;
+
+      this.setState({
+        measures: _.extend({}, this.state.measures, {
+          issues: r.total,
+          blockerIssues: _.findWhere(severitiesFacet, { val: 'BLOCKER' }).count,
+          criticalIssues: _.findWhere(severitiesFacet, { val: 'CRITICAL' }).count,
+          openIssues: _.findWhere(statusesFacet, { val: 'OPEN' }).count,
+          reopenedIssues: _.findWhere(statusesFacet, { val: 'REOPENED' }).count
+        })
+      });
+    });
+  },
+
+  requestLeakDebt() {
+    let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ');
+    this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities', facetMode: 'debt' }).done(r => {
+      this.setState({
+        leak: _.extend({}, this.state.leak, { newDebt: r.debtTotal })
+      });
+    });
+  },
+
+  requestNutshellDebt() {
+    this._requestIssues({ resolved: 'false', facets: 'severities', facetMode: 'debt' }).done(r => {
+      this.setState({
+        measures: _.extend({}, this.state.measures, { debt: r.debtTotal })
+      });
+    });
+  },
+
+  render() {
+    return <div>
+      <Gate component={this.props.component} gate={this.props.gate}/>
+      <Leak component={this.props.component} leak={this.state.leak} measures={this.state.measures}/>
+      <Nutshell component={this.props.component} measures={this.state.measures} section={this.props.section}
+                onRoute={this.props.onRoute}/>
+    </div>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js
new file mode 100644 (file)
index 0000000..9de1e53
--- /dev/null
@@ -0,0 +1,47 @@
+import React from 'react';
+import Card from './card';
+import Measure from './../helpers/measure';
+import DrilldownLink from './../helpers/drilldown-link';
+import Donut from './../helpers/donut';
+
+export default React.createClass({
+  render() {
+    let
+        coverage = this.props.measures.coverage,
+        tests = this.props.measures.tests,
+        donutData = [
+          { value: coverage, fill: '#85bb43' },
+          { value: 100 - coverage, fill: '#d4333f' }
+        ];
+
+    if (coverage == null) {
+      return null;
+    }
+
+    return (
+        <Card>
+          <div className="measures">
+            <div className="measures-chart">
+              <Donut data={donutData} size="47"/>
+            </div>
+            <div className="measure measure-big">
+              <span className="measure-value">
+                <DrilldownLink component={this.props.component.key} metric="overall_coverage">
+                  <Measure value={coverage} type="PERCENT"/>
+                </DrilldownLink>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.coverage')}</span>
+            </div>
+          </div>
+          <ul className="list-inline big-spacer-top measures-chart-indent">
+            <li>
+              <DrilldownLink component={this.props.component.key} metric="tests">
+                <Measure value={tests} type="SHORT_INT"/>
+              </DrilldownLink>&nbsp;
+              <span>{window.t('overview.metric.tests')}</span>
+            </li>
+          </ul>
+        </Card>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js
new file mode 100644 (file)
index 0000000..71a4055
--- /dev/null
@@ -0,0 +1,47 @@
+import React from 'react';
+import Card from './card';
+import Measure from './../helpers/measure';
+import DrilldownLink from './../helpers/drilldown-link';
+import Donut from './../helpers/donut';
+
+export default React.createClass({
+  render() {
+    let
+        density = this.props.measures.duplications,
+        lines = this.props.measures.duplicatedLines,
+        donutData = [
+          { value: density, fill: '#f3ca8e' },
+          { value: 100 - density, fill: '#e6e6e6' }
+        ];
+
+    if (density == null) {
+      return null;
+    }
+
+    return (
+        <Card>
+          <div className="measures">
+            <div className="measures-chart">
+              <Donut data={donutData} size="47"/>
+            </div>
+            <div className="measure measure-big">
+              <span className="measure-value">
+                <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density">
+                  <Measure value={density} type="PERCENT"/>
+                </DrilldownLink>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.duplications')}</span>
+            </div>
+          </div>
+          <ul className="list-inline big-spacer-top measures-chart-indent">
+            <li>
+              <DrilldownLink component={this.props.component.key} metric="duplicated_lines">
+                <Measure value={lines} type="SHORT_INT"/>
+              </DrilldownLink>&nbsp;
+              <span>{window.t('overview.metric.duplicated_lines')}</span>
+            </li>
+          </ul>
+        </Card>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js
new file mode 100644 (file)
index 0000000..8856bac
--- /dev/null
@@ -0,0 +1,70 @@
+import React from 'react';
+import Card from './card';
+import Measure from './../helpers/measure';
+import Rating from './../helpers/rating';
+import IssuesLink from './../helpers/issues-link';
+import DrilldownLink from './../helpers/drilldown-link';
+import SeverityIcon from '../../../components/shared/severity-icon';
+import StatusIcon from '../../../components/shared/status-icon';
+
+export default React.createClass({
+  render() {
+    let
+        debt = this.props.measures.debt,
+        rating = this.props.measures.sqaleRating,
+        issues = this.props.measures.issues,
+        blockerIssues = this.props.measures.blockerIssues,
+        criticalIssues = this.props.measures.criticalIssues,
+        issuesToReview = this.props.measures.openIssues + this.props.measures.reopenedIssues;
+
+    let active = this.props.section === 'issues';
+
+    return (
+        <Card linkTo="issues" active={active} onRoute={this.props.onRoute}>
+          <div className="measures">
+            <div className="measure measure-big" data-metric="sqale_rating">
+              <DrilldownLink component={this.props.component.key} metric="sqale_rating">
+                <Rating value={rating}/>
+              </DrilldownLink>
+            </div>
+            <div className="measure measure-big" data-metric="sqale_index">
+              <span className="measure-value">
+                <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}>
+                  <Measure value={debt} type="SHORT_WORK_DUR"/>
+                </IssuesLink>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.debt')}</span>
+            </div>
+            <div className="measure measure-big" data-metric="violations">
+              <span className="measure-value">
+                <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}>
+                  <Measure value={issues} type="SHORT_INT"/>
+                </IssuesLink>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.issues')}</span>
+            </div>
+          </div>
+          <ul className="list-inline big-spacer-top">
+            <li>
+              <span><SeverityIcon severity="BLOCKER"/></span>&nbsp;
+              <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'BLOCKER' }}>
+                <Measure value={blockerIssues} type="SHORT_INT"/>
+              </IssuesLink>
+            </li>
+            <li>
+              <span><SeverityIcon severity="CRITICAL"/></span>&nbsp;
+              <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'CRITICAL' }}>
+                <Measure value={criticalIssues} type="SHORT_INT"/>
+              </IssuesLink>
+            </li>
+            <li>
+              <span><StatusIcon status="OPEN"/></span>&nbsp;
+              <IssuesLink component={this.props.component.key} params={{ resolved: 'false', statuses: 'OPEN,REOPENED' }}>
+                <Measure value={issuesToReview} type="SHORT_INT"/>
+              </IssuesLink>
+            </li>
+          </ul>
+        </Card>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js
new file mode 100644 (file)
index 0000000..9488219
--- /dev/null
@@ -0,0 +1,35 @@
+import React from 'react';
+import Card from './card';
+import Measure from './../helpers/measure';
+import DrilldownLink from './../helpers/drilldown-link';
+
+export default React.createClass({
+  render() {
+    let
+        lines = this.props.measures['lines'],
+        files = this.props.measures['files'];
+
+    return (
+        <Card>
+          <div className="measures">
+            <div className="measure measure-big" data-metric="lines">
+              <span className="measure-value">
+                <DrilldownLink component={this.props.component.key} metric="lines">
+                  <Measure value={lines} type="SHORT_INT"/>
+                </DrilldownLink>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.lines')}</span>
+            </div>
+            <div className="measure measure-big" data-metric="files">
+              <span className="measure-value">
+                <DrilldownLink component={this.props.component.key} metric="files">
+                  <Measure value={files} type="SHORT_INT"/>
+                </DrilldownLink>
+              </span>
+              <span className="measure-name">{window.t('overview.metric.files')}</span>
+            </div>
+          </div>
+        </Card>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell.js
new file mode 100644 (file)
index 0000000..1c9c586
--- /dev/null
@@ -0,0 +1,28 @@
+import React from 'react';
+import Cards from './cards';
+import NutshellIssues from './nutshell-issues';
+import NutshellCoverage from './nutshell-coverage';
+import NutshellSize from './nutshell-size';
+import NutshellDups from './nutshell-dups';
+
+export default React.createClass({
+  render() {
+    let props = {
+      measures: this.props.measures,
+      component: this.props.component,
+      section: this.props.section,
+      onRoute: this.props.onRoute
+    };
+    return (
+        <div className="overview-nutshell">
+          <h2 className="overview-title">{window.t('overview.project_in_a_nutshell')}</h2>
+          <Cards>
+            <NutshellIssues {...props}/>
+            <NutshellCoverage {...props}/>
+            <NutshellDups {...props}/>
+            <NutshellSize {...props}/>
+          </Cards>
+        </div>
+    );
+  }
+});
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
new file mode 100644 (file)
index 0000000..af445cc
--- /dev/null
@@ -0,0 +1,28 @@
+import React from 'react';
+import Assignee from '../../../components/shared/assignee-helper';
+import { DomainHeader } from '../domain/header';
+import { formatMeasure } from '../formatting';
+import { componentIssuesUrl } from '../../../helpers/Url';
+
+export default class extends React.Component {
+  render () {
+    let rows = this.props.assignees.map(s => {
+      let href = componentIssuesUrl(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, 'violations')}</a>
+        </td>
+      </tr>;
+    });
+
+    return <div className="overview-domain-section">
+      <DomainHeader title="Issues to Review"/>
+      <table className="data zebra">
+        <tbody>{rows}</tbody>
+      </table>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/bubble-chart.js b/server/sonar-web/src/main/js/apps/overview/issues/bubble-chart.js
new file mode 100644 (file)
index 0000000..b37c420
--- /dev/null
@@ -0,0 +1,105 @@
+import _ from 'underscore';
+import React from 'react';
+import { BubbleChart } from '../../../components/charts/bubble-chart';
+import { getProjectUrl } from '../../../helpers/Url';
+import { getFiles } from '../../../api/components';
+
+
+const X_METRIC = 'violations';
+const Y_METRIC = 'sqale_index';
+const SIZE_METRIC_1 = 'blocker_violations';
+const SIZE_METRIC_2 = 'critical_violations';
+const COMPONENTS_METRICS = [X_METRIC, Y_METRIC, SIZE_METRIC_1, SIZE_METRIC_2];
+const HEIGHT = 360;
+
+
+function formatIssues (d) {
+  return window.formatMeasure(d, 'SHORT_INT');
+}
+
+function formatDebt (d) {
+  return window.formatMeasure(d, 'SHORT_WORK_DUR');
+}
+
+function getMeasure (component, metric) {
+  return component.measures[metric] || 0;
+}
+
+
+export class IssuesBubbleChart extends React.Component {
+  constructor () {
+    super();
+    this.state = { loading: true, files: [] };
+  }
+
+  componentDidMount () {
+    this.requestFiles();
+  }
+
+  requestFiles () {
+    return getFiles(this.props.component.key, COMPONENTS_METRICS).then(r => {
+      let files = r.map(file => {
+        let measures = {};
+        (file.msr || []).forEach(measure => {
+          measures[measure.key] = measure.val;
+        });
+        return _.extend(file, { measures });
+      });
+      this.setState({ loading: false, files });
+    });
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderBubbleChart () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    let items = this.state.files.map(component => {
+      return {
+        x: getMeasure(component, X_METRIC),
+        y: getMeasure(component, Y_METRIC),
+        size: getMeasure(component, SIZE_METRIC_1) + getMeasure(component, SIZE_METRIC_2),
+        link: getProjectUrl(component.key)
+      };
+    });
+    let xGrid = this.state.files.map(component => component.measures[X_METRIC]);
+    let tooltips = this.state.files.map(component => {
+      let inner = [
+        component.name,
+        `Issues: ${formatIssues(getMeasure(component, X_METRIC))}`,
+        `Technical Debt: ${formatDebt(getMeasure(component, Y_METRIC))}`,
+        `Blocker & Critical Issues: ${formatIssues(getMeasure(component, SIZE_METRIC_1) + getMeasure(component, SIZE_METRIC_2))}`
+      ].join('<br>');
+      return `<div class="text-left">${inner}</div>`;
+    });
+    return <BubbleChart items={items}
+                        xGrid={xGrid}
+                        tooltips={tooltips}
+                        height={HEIGHT}
+                        padding={[25, 30, 50, 60]}
+                        formatXTick={formatIssues}
+                        formatYTick={formatDebt}/>;
+  }
+
+  render () {
+    return <div className="overview-bubble-chart overview-domain-dark">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Project Files</h2>
+        <ul className="list-inline small">
+          <li>X: Issues</li>
+          <li>Y: Technical Debt</li>
+          <li>Size: Blocker & Critical Issues</li>
+        </ul>
+      </div>
+      <div>
+        {this.renderBubbleChart()}
+      </div>
+    </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
new file mode 100644 (file)
index 0000000..e55a2f0
--- /dev/null
@@ -0,0 +1,66 @@
+import React from 'react';
+
+import IssuesSeverities from './severities';
+import IssuesAssignees from './assignees';
+import IssuesTags from './tags';
+import { IssuesBubbleChart } from './bubble-chart';
+import { IssuesTimeline } from './timeline';
+import { IssuesTreemap } from './treemap';
+
+import { getSeverities, getTags, getAssignees } from '../../../api/issues';
+
+
+export default class OverviewDomain extends React.Component {
+  constructor () {
+    super();
+    this.state = { severities: [], tags: [], assignees: [] };
+  }
+
+  componentDidMount () {
+    Promise.all([
+      this.requestSeverities(),
+      this.requestTags(),
+      this.requestAssignees()
+    ]).then(responses => {
+      this.setState({
+        severities: responses[0],
+        tags: responses[1],
+        assignees: responses[2]
+      });
+    });
+  }
+
+  requestSeverities () {
+    return getSeverities({ resolved: 'false', componentUuids: this.props.component.id });
+  }
+
+  requestTags () {
+    return getTags({ resolved: 'false', componentUuids: this.props.component.id });
+  }
+
+  requestAssignees () {
+    return getAssignees({ statuses: 'OPEN,REOPENED', componentUuids: this.props.component.id });
+  }
+
+  render () {
+    return <div className="overview-domain">
+
+      <IssuesTimeline {...this.props}/>
+
+      <div className="flex-columns">
+        <div className="flex-column flex-column-third">
+          <IssuesSeverities {...this.props} severities={this.state.severities}/>
+        </div>
+        <div className="flex-column flex-column-third">
+          <IssuesTags {...this.props} tags={this.state.tags}/>
+        </div>
+        <div className="flex-column flex-column-third">
+          <IssuesAssignees {...this.props} assignees={this.state.assignees}/>
+        </div>
+      </div>
+
+      <IssuesBubbleChart {...this.props}/>
+      <IssuesTreemap {...this.props}/>
+    </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
new file mode 100644 (file)
index 0000000..be2911b
--- /dev/null
@@ -0,0 +1,35 @@
+import _ from 'underscore';
+import React from 'react';
+import SeverityHelper from '../../../components/shared/severity-helper';
+import { DomainHeader } from '../domain/header';
+import { formatMeasure } from '../formatting';
+import { componentIssuesUrl } from '../../../helpers/Url';
+
+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 = componentIssuesUrl(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, 'violations')}
+          </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
new file mode 100644 (file)
index 0000000..8c29d00
--- /dev/null
@@ -0,0 +1,24 @@
+import React from 'react';
+import { DomainHeader } from '../domain/header';
+import { WordCloud } from '../../../components/charts/word-cloud';
+import { componentIssuesUrl } from '../../../helpers/Url';
+
+export default class extends React.Component {
+  renderWordCloud () {
+    let tags = this.props.tags.map(tag => {
+      let link = componentIssuesUrl(this.props.component.key, { resolved: 'false', tags: tag.val });
+      let tooltip = `Issues: ${window.formatMeasure(tag.count, 'SHORT_INT')}`;
+      return { text: tag.val, size: tag.count, link, tooltip };
+    });
+    return <WordCloud items={tags}/>;
+  }
+
+  render () {
+    return <div className="overview-domain-section">
+      <DomainHeader title="Issues By Tag"/>
+      <div>
+        {this.renderWordCloud()}
+      </div>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/timeline.js b/server/sonar-web/src/main/js/apps/overview/issues/timeline.js
new file mode 100644 (file)
index 0000000..5266dad
--- /dev/null
@@ -0,0 +1,147 @@
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import { LineChart } from '../../../components/charts/line-chart';
+import { formatMeasure } from '../formatting';
+import { getTimeMachineData } from '../../../api/time-machine';
+import { getEvents } from '../../../api/events';
+
+
+const ISSUES_METRICS = [
+  'violations',
+  'blocker_violations',
+  'critical_violations',
+  'major_violations',
+  'minor_violations',
+  'info_violations',
+  'confirmed_issues',
+  'false_positive_issues',
+  'open_issues',
+  'reopened_issues'
+];
+
+const DEBT_METRICS = [
+  'sqale_index',
+  'sqale_debt_ratio'
+];
+
+const HEIGHT = 280;
+
+
+export class IssuesTimeline extends React.Component {
+  constructor () {
+    super();
+    this.state = { loading: true, currentMetric: ISSUES_METRICS[0] };
+  }
+
+  componentDidMount () {
+    Promise.all([
+      this.requestTimeMachineData(),
+      this.requestEvents()
+    ]).then(() => this.setState({ loading: false }));
+  }
+
+  requestTimeMachineData () {
+    return getTimeMachineData(this.props.component.key, this.state.currentMetric).then(r => {
+      let snapshots = r[0].cells.map(cell => {
+        return { date: moment(cell.d).toDate(), value: cell.v[0] };
+      });
+      this.setState({ snapshots });
+    });
+  }
+
+  requestEvents () {
+    return getEvents(this.props.component.key, 'Version').then(r => {
+      let events = r.map(event => {
+        return { version: event.n, date: moment(event.dt).toDate() };
+      });
+      events = _.sortBy(events, 'date');
+      this.setState({ events });
+    });
+  }
+
+  prepareEvents () {
+    let events = this.state.events;
+    let snapshots = this.state.snapshots;
+    return events
+        .map(event => {
+          let snapshot = snapshots.find(s => s.date.getTime() === event.date.getTime());
+          event.value = snapshot && snapshot.value;
+          return event;
+        })
+        .filter(event => event.value != null);
+  }
+
+  handleMetricChange () {
+    let metric = React.findDOMNode(this.refs.metricSelect).value;
+    this.setState({ currentMetric: metric }, this.requestTimeMachineData);
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderLineChart () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    let events = this.prepareEvents();
+
+    let data = events.map((event, index) => {
+      return { x: index, y: event.value };
+    });
+
+    let xTicks = events.map(event => event.version.substr(0, 6));
+
+    let xValues = events.map(event => formatMeasure(event.value, this.state.currentMetric));
+
+    // TODO use leak period
+    let backdropConstraints = [
+      this.state.events.length - 2,
+      this.state.events.length - 1
+    ];
+
+    return <LineChart data={data}
+                      xTicks={xTicks}
+                      xValues={xValues}
+                      backdropConstraints={backdropConstraints}
+                      height={HEIGHT}
+                      interpolate="linear"
+                      padding={[25, 30, 50, 30]}/>;
+  }
+
+  renderTimelineMetricSelect () {
+    if (this.state.loading) {
+      return null;
+    }
+
+    let issueOptions = ISSUES_METRICS
+        .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
+    let debtOptions = DEBT_METRICS
+        .map(metric => <option key={metric} value={metric}>{window.t('metric', metric, 'name')}</option>);
+
+    return <select ref="metricSelect"
+                   className="overview-timeline-select"
+                   onChange={this.handleMetricChange.bind(this)}
+                   value={this.state.currentMetric}>
+      <optgroup label="Issues">{issueOptions}</optgroup>
+      <optgroup label="Technical Debt">{debtOptions}</optgroup>
+    </select>;
+  }
+
+  render () {
+    return <div className="overview-timeline overview-domain-dark">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Project History</h2>
+        {this.renderTimelineMetricSelect()}
+      </div>
+      <div>
+        {this.renderLineChart()}
+      </div>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/issues/treemap.js b/server/sonar-web/src/main/js/apps/overview/issues/treemap.js
new file mode 100644 (file)
index 0000000..665406c
--- /dev/null
@@ -0,0 +1,99 @@
+import _ from 'underscore';
+import React from 'react';
+
+import { Treemap } from '../../../components/charts/treemap';
+import { formatMeasure } from '../formatting';
+import { getChildren } from '../../../api/components';
+
+const COMPONENTS_METRICS = [
+  'lines',
+  'sqale_rating'
+];
+
+const HEIGHT = 360;
+
+export class IssuesTreemap extends React.Component {
+  constructor () {
+    super();
+    this.state = { loading: true, components: [] };
+  }
+
+  componentDidMount () {
+    this.requestComponents();
+  }
+
+  requestComponents () {
+    return getChildren(this.props.component.key, COMPONENTS_METRICS).then(r => {
+      let components = r.map(component => {
+        let measures = {};
+        component.msr.forEach(measure => {
+          measures[measure.key] = measure.val;
+        });
+        return _.extend(component, { measures });
+      });
+      this.setState({ loading: false, components });
+    });
+  }
+
+  // TODO use css
+  getRatingColor (rating) {
+    switch (rating) {
+      case 1:
+        return '#00AA00';
+      case 2:
+        return '#80CC00';
+      case 3:
+        return '#FFEE00';
+      case 4:
+        return '#F77700';
+      case 5:
+        return '#EE0000';
+      default:
+        return '#777';
+    }
+  }
+
+  renderLoading () {
+    return <div className="overview-chart-placeholder" style={{ height: HEIGHT }}>
+      <i className="spinner"/>
+    </div>;
+  }
+
+  renderTreemap () {
+    if (this.state.loading) {
+      return this.renderLoading();
+    }
+
+    let items = this.state.components.map(component => {
+      return {
+        size: component.measures['lines'],
+        color: this.getRatingColor(component.measures['sqale_rating'])
+      };
+    });
+    let labels = this.state.components.map(component => component.name);
+    let tooltips = this.state.components.map(component => {
+      let inner = [
+        component.name,
+        `Lines: ${formatMeasure(component.measures['lines'], 'lines')}`,
+        `SQALE Rating: ${formatMeasure(component.measures['sqale_rating'], 'sqale_rating')}`
+      ].join('<br>');
+      return `<div class="text-left">${inner}</div>`;
+    });
+    return <Treemap items={items} labels={labels} tooltips={tooltips} height={HEIGHT}/>;
+  }
+
+  render () {
+    return <div className="overview-domain-section overview-treemap">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Project Components</h2>
+        <ul className="list-inline small">
+          <li>Size: Lines</li>
+          <li>Color: SQALE Rating</li>
+        </ul>
+      </div>
+      <div>
+        {this.renderTreemap()}
+      </div>
+    </div>;
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/leak-coverage.js b/server/sonar-web/src/main/js/apps/overview/leak-coverage.js
deleted file mode 100644 (file)
index 0d50243..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react';
-import Card from './card';
-import Measure from './helpers/measure';
-import MeasureVariation from './helpers/measure-variation';
-import DrilldownLink from './helpers/drilldown-link';
-import Donut from './helpers/donut';
-
-export default React.createClass({
-  render() {
-    let
-        newCoverage = parseInt(this.props.leak.newCoverage, 10),
-        tests = this.props.leak.tests,
-        donutData = [
-          { value: newCoverage, fill: '#85bb43' },
-          { value: 100 - newCoverage, fill: '#d4333f' }
-        ];
-
-    if (newCoverage == null || isNaN(newCoverage)) {
-      return null;
-    }
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measures-chart">
-              <Donut data={donutData} size="47"/>
-            </div>
-            <div className="measure measure-big" data-metric="new_coverage">
-              <span className="measure-value">
-                <DrilldownLink component={this.props.component.key} metric="new_coverage" period="3">
-                  <Measure value={newCoverage} type="PERCENT"/>
-                </DrilldownLink>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.new_coverage')}</span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top measures-chart-indent">
-            <li>
-              <span><MeasureVariation value={tests} type="SHORT_INT"/></span>&nbsp;
-              <span>{window.t('overview.metric.tests')}</span>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/leak-dups.js b/server/sonar-web/src/main/js/apps/overview/leak-dups.js
deleted file mode 100644 (file)
index a34555f..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import Card from './card';
-import MeasureVariation from './helpers/measure-variation';
-import Donut from './helpers/donut';
-
-export default React.createClass({
-  render() {
-    let
-        density = this.props.leak.duplications,
-        lines = this.props.leak.duplicatedLines,
-        donutData = [
-          { value: density, fill: '#f3ca8e' },
-          { value: 100 - density, fill: '#e6e6e6' }
-        ];
-
-    if (density == null) {
-      return null;
-    }
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measures-chart">
-              <Donut data={donutData} size="47"/>
-            </div>
-            <div className="measure measure-big" data-metric="duplicated_lines_density">
-              <span className="measure-value">
-                <MeasureVariation value={density} type="PERCENT"/>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.duplications')}</span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top measures-chart-indent">
-            <li>
-              <span><MeasureVariation value={lines} type="SHORT_INT"/></span>&nbsp;
-              <span>{window.t('overview.metric.duplicated_lines')}</span>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/leak-issues.js b/server/sonar-web/src/main/js/apps/overview/leak-issues.js
deleted file mode 100644 (file)
index 61443d6..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-import moment from 'moment';
-import React from 'react';
-import Card from './card';
-import Measure from './helpers/measure';
-import MeasureVariation from './helpers/measure-variation';
-import IssuesLink from './helpers/issues-link';
-import SeverityIcon from '../../components/shared/severity-icon';
-import StatusIcon from '../../components/shared/status-icon';
-import {getPeriodDate} from './helpers/period-label';
-
-export default React.createClass({
-  render() {
-    let
-        newDebt = this.props.leak.newDebt,
-        issues = this.props.leak.newIssues,
-        blockerIssues = this.props.leak.newBlockerIssues,
-        criticalIssues = this.props.leak.newCriticalIssues,
-        issuesToReview = this.props.leak.newOpenIssues + this.props.leak.newReopenedIssues,
-        periodDate = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ');
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measure measure-big" data-metric="sqale_index">
-              <span className="measure-value">
-                <IssuesLink component={this.props.component.key}
-                            params={{ resolved: 'false', createdAfter: periodDate, facetMode: 'debt' }}>
-                  <Measure value={newDebt} type="SHORT_WORK_DUR"/>
-                </IssuesLink>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.new_debt')}</span>
-            </div>
-            <div className="measure measure-big" data-metric="violations">
-              <span className="measure-value">
-                <IssuesLink component={this.props.component.key}
-                            params={{ resolved: 'false', createdAfter: periodDate }}>
-                  <Measure value={issues} type="SHORT_INT"/>
-                </IssuesLink>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.new_issues')}</span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top">
-            <li>
-              <span><SeverityIcon severity="BLOCKER"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key}
-                          params={{ resolved: 'false', createdAfter: periodDate, severities: 'BLOCKER' }}>
-                <MeasureVariation value={blockerIssues} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-            <li>
-              <span><SeverityIcon severity="CRITICAL"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key}
-                          params={{ resolved: 'false', createdAfter: periodDate, severities: 'CRITICAL' }}>
-                <MeasureVariation value={criticalIssues} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-            <li>
-              <span><StatusIcon status="OPEN"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key}
-                          params={{ resolved: 'false', createdAfter: periodDate, statuses: 'OPEN,REOPENED' }}>
-                <MeasureVariation value={issuesToReview} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/leak-size.js b/server/sonar-web/src/main/js/apps/overview/leak-size.js
deleted file mode 100644 (file)
index d7df01c..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import Card from './card';
-import MeasureVariation from './helpers/measure-variation';
-
-export default React.createClass({
-  render() {
-    let
-        lines = this.props.leak.lines,
-        files = this.props.leak.files;
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measure measure-big" data-metric="lines">
-              <span className="measure-value">
-                <MeasureVariation value={lines} type="SHORT_INT"/>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.lines')}</span>
-            </div>
-            <div className="measure measure-big" data-metric="files">
-              <span className="measure-value">
-                <MeasureVariation value={files} type="SHORT_INT"/>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.files')}</span>
-            </div>
-          </div>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/leak.js b/server/sonar-web/src/main/js/apps/overview/leak.js
deleted file mode 100644 (file)
index f629153..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-import _ from 'underscore';
-import React from 'react';
-import Cards from './cards';
-import LeakIssues from './leak-issues';
-import LeakCoverage from './leak-coverage';
-import LeakSize from './leak-size';
-import LeakDups from './leak-dups';
-import {periodLabel} from './helpers/period-label';
-
-export default React.createClass({
-  render() {
-    if (_.size(this.props.component.periods) < 3) {
-      return null;
-    }
-
-    let period = periodLabel(this.props.component.periods, '3');
-
-    return (
-        <div className="overview-leak">
-          <h2 className="overview-title">
-            {window.t('overview.water_leak')}
-            <span className="overview-leak-period">{period}</span>
-          </h2>
-          <Cards>
-            <LeakIssues component={this.props.component} leak={this.props.leak} measures={this.props.measures}/>
-            <LeakCoverage component={this.props.component} leak={this.props.leak}/>
-            <LeakDups component={this.props.component} leak={this.props.leak}/>
-            <LeakSize component={this.props.component} leak={this.props.leak}/>
-          </Cards>
-        </div>
-    );
-  }
-});
index dc1025a89110f16b749a34417ac877fb65f9e0b0..81924a313cc2938cfb432559f50ab307a1eca032 100644 (file)
-import $ from 'jquery';
-import _ from 'underscore';
-import moment from 'moment';
 import React from 'react';
-import Gate from './gate';
-import Leak from './leak';
-import Nutshell from './nutshell';
+import offset from 'document-offset';
+import GeneralMain from './general/main';
+import IssuesMain from './issues/main';
 import Meta from './meta';
-import {getPeriodDate} from './helpers/period-label';
 
-export default React.createClass({
-  getInitialState() {
-    return { leak: this.props.leak, measures: this.props.measures };
-  },
-
-  componentDidMount() {
-    if (this._hasWaterLeak()) {
-      this.requestLeakIssues();
-      this.requestLeakDebt();
-    }
-    this.requestNutshellIssues();
-    this.requestNutshellDebt();
-  },
-
-  _hasWaterLeak() {
-    return !!_.findWhere(this.props.component.periods, { index: '3' });
-  },
-
-  _requestIssues(data) {
-    let url = `${baseUrl}/api/issues/search`;
-    data.ps = 1;
-    data.componentUuids = this.props.component.id;
-    return $.get(url, data);
-  },
-
-  requestLeakIssues() {
-    let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ');
-    this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities,statuses' }).done(r => {
-      let
-          severitiesFacet = _.findWhere(r.facets, { property: 'severities' }).values,
-          statusesFacet = _.findWhere(r.facets, { property: 'statuses' }).values;
-
-      this.setState({
-        leak: _.extend({}, this.state.leak, {
-          newIssues: r.total,
-          newBlockerIssues: _.findWhere(severitiesFacet, { val: 'BLOCKER' }).count,
-          newCriticalIssues: _.findWhere(severitiesFacet, { val: 'CRITICAL' }).count,
-          newOpenIssues: _.findWhere(statusesFacet, { val: 'OPEN' }).count,
-          newReopenedIssues: _.findWhere(statusesFacet, { val: 'REOPENED' }).count
-        })
-      });
-    });
-  },
-
-  requestNutshellIssues() {
-    this._requestIssues({ resolved: 'false', facets: 'severities,statuses' }).done(r => {
-      let
-          severitiesFacet = _.findWhere(r.facets, { property: 'severities' }).values,
-          statusesFacet = _.findWhere(r.facets, { property: 'statuses' }).values;
+export default class Overview extends React.Component {
+  constructor () {
+    super();
+    let hash = window.location.hash;
+    this.state = { section: hash.length ? hash.substr(1) : null };
+  }
 
-      this.setState({
-        measures: _.extend({}, this.state.measures, {
-          issues: r.total,
-          blockerIssues: _.findWhere(severitiesFacet, { val: 'BLOCKER' }).count,
-          criticalIssues: _.findWhere(severitiesFacet, { val: 'CRITICAL' }).count,
-          openIssues: _.findWhere(statusesFacet, { val: 'OPEN' }).count,
-          reopenedIssues: _.findWhere(statusesFacet, { val: 'REOPENED' }).count
-        })
-      });
-    });
-  },
+  handleRoute (section, el) {
+    this.setState({ section }, () => this.scrollToEl(el));
+    window.location.href = '#' + section;
+  }
 
-  requestLeakDebt() {
-    let createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ');
-    this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities', facetMode: 'debt' }).done(r => {
-      this.setState({
-        leak: _.extend({}, this.state.leak, { newDebt: r.debtTotal })
-      });
-    });
-  },
+  scrollToEl (el) {
+    let top = offset(el).top - el.getBoundingClientRect().height;
+    window.scrollTo(0, top);
+  }
 
-  requestNutshellDebt() {
-    this._requestIssues({ resolved: 'false', facets: 'severities', facetMode: 'debt' }).done(r => {
-      this.setState({
-        measures: _.extend({}, this.state.measures, { debt: r.debtTotal })
-      });
-    });
-  },
+  render () {
+    let child;
+    switch (this.state.section) {
+      case 'issues':
+        child = <IssuesMain {...this.props}/>;
+        break;
+      default:
+        child = null;
+    }
 
-  render() {
-    return (
-        <div className="overview">
-          <div className="overview-main">
-            <Gate component={this.props.component} gate={this.props.gate}/>
-            <Leak component={this.props.component} leak={this.state.leak} measures={this.state.measures}/>
-            <Nutshell component={this.props.component} measures={this.state.measures}/>
-          </div>
-          <Meta component={this.props.component}/>
-        </div>
-    );
+    return <div className="overview">
+      <div className="overview-main">
+        <GeneralMain {...this.props} section={this.state.section} onRoute={this.handleRoute.bind(this)}/>
+        {child}
+      </div>
+      <Meta component={this.props.component}/>
+    </div>;
   }
-});
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js b/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js
deleted file mode 100644 (file)
index e478a71..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import Card from './card';
-import Measure from './helpers/measure';
-import DrilldownLink from './helpers/drilldown-link';
-import Donut from './helpers/donut';
-
-export default React.createClass({
-  render() {
-    let
-        coverage = this.props.measures.coverage,
-        tests = this.props.measures.tests,
-        donutData = [
-          { value: coverage, fill: '#85bb43' },
-          { value: 100 - coverage, fill: '#d4333f' }
-        ];
-
-    if (coverage == null) {
-      return null;
-    }
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measures-chart">
-              <Donut data={donutData} size="47"/>
-            </div>
-            <div className="measure measure-big">
-              <span className="measure-value">
-                <DrilldownLink component={this.props.component.key} metric="overall_coverage">
-                  <Measure value={coverage} type="PERCENT"/>
-                </DrilldownLink>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.coverage')}</span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top measures-chart-indent">
-            <li>
-              <DrilldownLink component={this.props.component.key} metric="tests">
-                <Measure value={tests} type="SHORT_INT"/>
-              </DrilldownLink>&nbsp;
-              <span>{window.t('overview.metric.tests')}</span>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-dups.js b/server/sonar-web/src/main/js/apps/overview/nutshell-dups.js
deleted file mode 100644 (file)
index fd93f14..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import Card from './card';
-import Measure from './helpers/measure';
-import DrilldownLink from './helpers/drilldown-link';
-import Donut from './helpers/donut';
-
-export default React.createClass({
-  render() {
-    let
-        density = this.props.measures.duplications,
-        lines = this.props.measures.duplicatedLines,
-        donutData = [
-          { value: density, fill: '#f3ca8e' },
-          { value: 100 - density, fill: '#e6e6e6' }
-        ];
-
-    if (density == null) {
-      return null;
-    }
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measures-chart">
-              <Donut data={donutData} size="47"/>
-            </div>
-            <div className="measure measure-big">
-              <span className="measure-value">
-                <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density">
-                  <Measure value={density} type="PERCENT"/>
-                </DrilldownLink>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.duplications')}</span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top measures-chart-indent">
-            <li>
-              <DrilldownLink component={this.props.component.key} metric="duplicated_lines">
-                <Measure value={lines} type="SHORT_INT"/>
-              </DrilldownLink>&nbsp;
-              <span>{window.t('overview.metric.duplicated_lines')}</span>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-issues.js b/server/sonar-web/src/main/js/apps/overview/nutshell-issues.js
deleted file mode 100644 (file)
index f9ae334..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react';
-import Card from './card';
-import Measure from './helpers/measure';
-import Rating from './helpers/rating';
-import IssuesLink from './helpers/issues-link';
-import DrilldownLink from './helpers/drilldown-link';
-import SeverityIcon from '../../components/shared/severity-icon';
-import StatusIcon from '../../components/shared/status-icon';
-
-export default React.createClass({
-  render() {
-    let
-        debt = this.props.measures.debt,
-        rating = this.props.measures.sqaleRating,
-        issues = this.props.measures.issues,
-        blockerIssues = this.props.measures.blockerIssues,
-        criticalIssues = this.props.measures.criticalIssues,
-        issuesToReview = this.props.measures.openIssues + this.props.measures.reopenedIssues;
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measure measure-big" data-metric="sqale_rating">
-              <DrilldownLink component={this.props.component.key} metric="sqale_rating">
-                <Rating value={rating}/>
-              </DrilldownLink>
-            </div>
-            <div className="measure measure-big" data-metric="sqale_index">
-              <span className="measure-value">
-                <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}>
-                  <Measure value={debt} type="SHORT_WORK_DUR"/>
-                </IssuesLink>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.debt')}</span>
-            </div>
-            <div className="measure measure-big" data-metric="violations">
-              <span className="measure-value">
-                <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}>
-                  <Measure value={issues} type="SHORT_INT"/>
-                </IssuesLink>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.issues')}</span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top">
-            <li>
-              <span><SeverityIcon severity="BLOCKER"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'BLOCKER' }}>
-                <Measure value={blockerIssues} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-            <li>
-              <span><SeverityIcon severity="CRITICAL"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'CRITICAL' }}>
-                <Measure value={criticalIssues} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-            <li>
-              <span><StatusIcon status="OPEN"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key} params={{ resolved: 'false', statuses: 'OPEN,REOPENED' }}>
-                <Measure value={issuesToReview} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-size.js b/server/sonar-web/src/main/js/apps/overview/nutshell-size.js
deleted file mode 100644 (file)
index 5d41e29..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-import Card from './card';
-import Measure from './helpers/measure';
-import DrilldownLink from './helpers/drilldown-link';
-
-export default React.createClass({
-  render() {
-    let
-        lines = this.props.measures['lines'],
-        files = this.props.measures['files'];
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measure measure-big" data-metric="lines">
-              <span className="measure-value">
-                <DrilldownLink component={this.props.component.key} metric="lines">
-                  <Measure value={lines} type="SHORT_INT"/>
-                </DrilldownLink>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.lines')}</span>
-            </div>
-            <div className="measure measure-big" data-metric="files">
-              <span className="measure-value">
-                <DrilldownLink component={this.props.component.key} metric="files">
-                  <Measure value={files} type="SHORT_INT"/>
-                </DrilldownLink>
-              </span>
-              <span className="measure-name">{window.t('overview.metric.files')}</span>
-            </div>
-          </div>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell.js b/server/sonar-web/src/main/js/apps/overview/nutshell.js
deleted file mode 100644 (file)
index f8c7dc3..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-import Cards from './cards';
-import NutshellIssues from './nutshell-issues';
-import NutshellCoverage from './nutshell-coverage';
-import NutshellSize from './nutshell-size';
-import NutshellDups from './nutshell-dups';
-
-export default React.createClass({
-  render() {
-    let props = { measures: this.props.measures, component: this.props.component };
-    return (
-        <div className="overview-nutshell">
-          <h2 className="overview-title">{window.t('overview.project_in_a_nutshell')}</h2>
-          <Cards>
-            <NutshellIssues {...props}/>
-            <NutshellCoverage {...props}/>
-            <NutshellDups {...props}/>
-            <NutshellSize {...props}/>
-          </Cards>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/components/charts/bubble-chart.js b/server/sonar-web/src/main/js/components/charts/bubble-chart.js
new file mode 100644 (file)
index 0000000..e2c5d20
--- /dev/null
@@ -0,0 +1,213 @@
+import $ from 'jquery';
+import d3 from 'd3';
+import React from 'react';
+
+export class Bubble extends React.Component {
+  handleClick () {
+    if (this.props.link) {
+      window.location = this.props.link;
+    }
+  }
+
+  render () {
+    let tooltipAttrs = {};
+    if (this.props.tooltip) {
+      tooltipAttrs = {
+        'data-toggle': 'tooltip',
+        'title': this.props.tooltip
+      };
+    }
+    return <circle onClick={this.handleClick.bind(this)} className="bubble-chart-bubble"
+                   r={this.props.r} {...tooltipAttrs}
+                   transform={`translate(${this.props.x}, ${this.props.y})`}/>;
+  }
+}
+
+
+export class BubbleChart extends React.Component {
+  constructor (props) {
+    super();
+    this.state = { width: props.width, height: props.height };
+  }
+
+  componentDidMount () {
+    if (!this.props.width || !this.props.height) {
+      this.handleResize();
+      window.addEventListener('resize', this.handleResize.bind(this));
+    }
+    this.initTooltips();
+  }
+
+  componentDidUpdate () {
+    this.initTooltips();
+  }
+
+  componentWillUnmount () {
+    if (!this.props.width || !this.props.height) {
+      window.removeEventListener('resize', this.handleResize.bind(this));
+    }
+  }
+
+  handleResize () {
+    let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+    let newWidth = this.props.width || boundingClientRect.width;
+    let newHeight = this.props.height || boundingClientRect.height;
+    this.setState({ width: newWidth, height: newHeight });
+  }
+
+  initTooltips () {
+    $('[data-toggle="tooltip"]', React.findDOMNode(this))
+        .tooltip({ container: 'body', placement: 'bottom', html: true });
+  }
+
+  getXRange (xScale, sizeScale, availableWidth) {
+    var minX = d3.min(this.props.items, d => xScale(d.x) - sizeScale(d.size)),
+        maxX = d3.max(this.props.items, d => xScale(d.x) + sizeScale(d.size)),
+        dMinX = minX < 0 ? xScale.range()[0] - minX : xScale.range()[0],
+        dMaxX = maxX > xScale.range()[1] ? maxX - xScale.range()[1] : 0;
+    return [dMinX, availableWidth - dMaxX];
+  }
+
+  getYRange (yScale, sizeScale, availableHeight) {
+    var minY = d3.min(this.props.items, d => yScale(d.y) - sizeScale(d.size)),
+        maxY = d3.max(this.props.items, d => yScale(d.y) + sizeScale(d.size)),
+        dMinY = minY < 0 ? yScale.range()[1] - minY : yScale.range()[1],
+        dMaxY = maxY > yScale.range()[0] ? maxY - yScale.range()[0] : 0;
+    return [availableHeight - dMaxY, dMinY];
+  }
+
+  renderXGrid (xScale, yScale) {
+    if (!this.props.displayXGrid) {
+      return null;
+    }
+
+    let lines = xScale.ticks().map((tick, index) => {
+      let x = xScale(tick);
+      let y1 = yScale.range()[0];
+      let y2 = yScale.range()[1];
+
+      // TODO extract styling
+      return <line key={index} x1={x} x2={x} y1={y1} y2={y2}
+                   shapeRendering="crispEdges" strokeWidth="0.3" stroke="#ccc"/>;
+    });
+
+    return <g ref="xGrid">{lines}</g>;
+  }
+
+  renderYGrid (xScale, yScale) {
+    if (!this.props.displayYGrid) {
+      return null;
+    }
+
+    let lines = yScale.ticks(5).map((tick, index) => {
+      let y = yScale(tick);
+      let x1 = xScale.range()[0];
+      let x2 = xScale.range()[1];
+
+      // TODO extract styling
+      return <line key={index} x1={x1} x2={x2} y1={y} y2={y}
+                   shapeRendering="crispEdges" strokeWidth="0.3" stroke="#ccc"/>;
+    });
+
+    return <g ref="yGrid">{lines}</g>;
+  }
+
+  renderXTicks (xScale, yScale) {
+    if (!this.props.displayXTicks) {
+      return null;
+    }
+
+    let ticks = xScale.ticks().map((tick, index) => {
+      let x = xScale(tick);
+      let y = yScale.range()[0];
+      let text = this.props.formatXTick(tick);
+
+      // TODO extract styling
+      return <text key={index} className="bubble-chart-tick" x={x} y={y} dy="1.5em">{text}</text>;
+    });
+
+    return <g>{ticks}</g>;
+  }
+
+  renderYTicks (xScale, yScale) {
+    if (!this.props.displayYTicks) {
+      return null;
+    }
+
+    let ticks = yScale.ticks(5).map((tick, index) => {
+      let x = xScale.range()[0];
+      let y = yScale(tick);
+      let text = this.props.formatYTick(tick);
+
+      // TODO extract styling
+      return <text key={index} className="bubble-chart-tick bubble-chart-tick-y"
+                   x={x} y={y} dx="-0.5em" dy="0.3em">{text}</text>;
+    });
+
+    return <g>{ticks}</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.scale.linear()
+        .domain([0, d3.max(this.props.items, d => d.x)])
+        .range([0, availableWidth])
+        .nice();
+    let yScale = d3.scale.linear()
+        .domain([0, d3.max(this.props.items, d => d.y)])
+        .range([availableHeight, 0])
+        .nice();
+    let sizeScale = d3.scale.linear()
+        .domain([0, d3.max(this.props.items, d => d.size)])
+        .range(this.props.sizeRange);
+
+    xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
+    yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
+
+    let bubbles = this.props.items
+        .map((item, index) => {
+          let tooltip = index < this.props.tooltips.length ? this.props.tooltips[index] : null;
+          return <Bubble key={index}
+                         tooltip={tooltip}
+                         link={item.link}
+                         x={xScale(item.x)} y={yScale(item.y)} r={sizeScale(item.size)}/>;
+        });
+
+    return <svg className="bubble-chart" width={this.state.width} height={this.state.height}>
+      <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+        {this.renderXGrid(xScale, yScale)}
+        {this.renderXTicks(xScale, yScale)}
+        {this.renderYGrid(xScale, yScale)}
+        {this.renderYTicks(xScale, yScale)}
+        {bubbles}
+      </g>
+    </svg>;
+  }
+}
+
+BubbleChart.defaultProps = {
+  sizeRange: [5, 45],
+  displayXGrid: true,
+  displayYGrid: true,
+  displayXTicks: true,
+  displayYTicks: true,
+  tooltips: [],
+  padding: [10, 10, 10, 10],
+  formatXTick: d => d,
+  formatYTick: d => d
+};
+
+BubbleChart.propTypes = {
+  sizeRange: React.PropTypes.arrayOf(React.PropTypes.number),
+  displayXGrid: React.PropTypes.bool,
+  displayYGrid: React.PropTypes.bool,
+  padding: React.PropTypes.arrayOf(React.PropTypes.number),
+  formatXTick: React.PropTypes.func,
+  formatYTick: React.PropTypes.func
+};
diff --git a/server/sonar-web/src/main/js/components/charts/line-chart.js b/server/sonar-web/src/main/js/components/charts/line-chart.js
new file mode 100644 (file)
index 0000000..bbb14f3
--- /dev/null
@@ -0,0 +1,156 @@
+import d3 from 'd3';
+import React from 'react';
+
+export class LineChart extends React.Component {
+  constructor (props) {
+    super();
+    this.state = { width: props.width, height: props.height };
+  }
+
+  componentDidMount () {
+    if (!this.props.width || !this.props.height) {
+      this.handleResize();
+      window.addEventListener('resize', this.handleResize.bind(this));
+    }
+  }
+
+  componentWillUnmount () {
+    if (!this.props.width || !this.props.height) {
+      window.removeEventListener('resize', this.handleResize.bind(this));
+    }
+  }
+
+  handleResize () {
+    let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+    let newWidth = this.props.width || boundingClientRect.width;
+    let newHeight = this.props.height || boundingClientRect.height;
+    this.setState({ width: newWidth, height: newHeight });
+  }
+
+  renderBackdrop (xScale, yScale) {
+    if (!this.props.displayBackdrop) {
+      return null;
+    }
+
+    let area = d3.svg.area()
+        .x(d => xScale(d.x))
+        .y0(yScale.range()[0])
+        .y1(d => yScale(d.y))
+        .interpolate(this.props.interpolate);
+
+    let data = this.props.data;
+    if (this.props.backdropConstraints) {
+      let c = this.props.backdropConstraints;
+      data = data.filter(d => c[0] <= d.x && d.x <= c[1]);
+    }
+
+    // TODO extract styling
+    return <path d={area(data)} fill="#4b9fd5" fillOpacity="0.2"/>;
+  }
+
+  renderPoints (xScale, yScale) {
+    if (!this.props.displayPoints) {
+      return null;
+    }
+    let points = this.props.data.map((point, index) => {
+      let x = xScale(point.x);
+      let y = yScale(point.y);
+      return <circle key={index} className="line-chart-point" r="3" cx={x} cy={y}/>;
+    });
+    return <g>{points}</g>;
+  }
+
+  renderVerticalGrid (xScale, yScale) {
+    if (!this.props.displayVerticalGrid) {
+      return null;
+    }
+    let lines = this.props.data.map((point, index) => {
+      let x = xScale(point.x);
+      let y1 = yScale.range()[0];
+      let y2 = yScale(point.y);
+      return <line key={index} className="line-chart-grid" x1={x} x2={x} y1={y1} y2={y2}/>;
+    });
+    return <g>{lines}</g>;
+  }
+
+  renderXTicks (xScale, yScale) {
+    if (!this.props.xTicks.length) {
+      return null;
+    }
+    let ticks = this.props.xTicks.map((tick, index) => {
+      let point = this.props.data[index];
+      let x = xScale(point.x);
+      let y = yScale.range()[0];
+      return <text key={index} className="line-chart-tick" x={x} y={y} dy="1.5em">{tick}</text>;
+    });
+    return <g>{ticks}</g>;
+  }
+
+  renderXValues (xScale, yScale) {
+    if (!this.props.xValues.length) {
+      return null;
+    }
+    let ticks = this.props.xValues.map((value, index) => {
+      let point = this.props.data[index];
+      let x = xScale(point.x);
+      let y = yScale(point.y);
+      return <text key={index} className="line-chart-tick" x={x} y={y} dy="-1em">{value}</text>;
+    });
+    return <g>{ticks}</g>;
+  }
+
+  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)}/>;
+  }
+
+  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 maxY = d3.max(this.props.data, d => d.y);
+    let xScale = d3.scale.linear()
+        .domain(d3.extent(this.props.data, d => d.x))
+        .range([0, availableWidth]);
+    let yScale = d3.scale.linear()
+        .domain([0, maxY])
+        .range([availableHeight, 0]);
+
+    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.renderVerticalGrid(xScale, yScale, maxY)}
+        {this.renderBackdrop(xScale, yScale)}
+        {this.renderLine(xScale, yScale)}
+        {this.renderPoints(xScale, yScale)}
+        {this.renderXTicks(xScale, yScale)}
+        {this.renderXValues(xScale, yScale)}
+      </g>
+    </svg>;
+  }
+}
+
+LineChart.defaultProps = {
+  displayBackdrop: true,
+  displayPoints: true,
+  displayVerticalGrid: true,
+  xTicks: [],
+  xValues: [],
+  padding: [10, 10, 10, 10],
+  interpolate: 'basis'
+};
+
+LineChart.propTypes = {
+  data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+  xTicks: React.PropTypes.arrayOf(React.PropTypes.any),
+  xValues: React.PropTypes.arrayOf(React.PropTypes.any),
+  padding: React.PropTypes.arrayOf(React.PropTypes.number),
+  backdropConstraints: React.PropTypes.arrayOf(React.PropTypes.number)
+};
diff --git a/server/sonar-web/src/main/js/components/charts/timeline.js b/server/sonar-web/src/main/js/components/charts/timeline.js
new file mode 100644 (file)
index 0000000..d200ff1
--- /dev/null
@@ -0,0 +1,85 @@
+import d3 from 'd3';
+import React from 'react';
+
+export class Timeline extends React.Component {
+  constructor (props) {
+    super();
+    this.state = { width: props.width, height: props.height };
+  }
+
+  componentDidMount () {
+    if (!this.props.width || !this.props.height) {
+      this.handleResize();
+      window.addEventListener('resize', this.handleResize.bind(this));
+    }
+  }
+
+  componentWillUnmount () {
+    if (!this.props.width || !this.props.height) {
+      window.removeEventListener('resize', this.handleResize.bind(this));
+    }
+  }
+
+  handleResize () {
+    let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+    let newWidth = this.props.width || boundingClientRect.width;
+    let newHeight = this.props.height || boundingClientRect.height;
+    this.setState({ width: newWidth, height: newHeight });
+  }
+
+  renderBackdrop (xScale, yScale, maxY) {
+    if (!this.props.displayBackdrop) {
+      return null;
+    }
+
+    let area = d3.svg.area()
+        .x(d => xScale(d.date))
+        .y0(maxY)
+        .y1(d => yScale(d.value))
+        .interpolate(this.props.interpolate);
+
+    // TODO extract styling
+    return <path d={area(this.props.snapshots)} fill="#4b9fd5" fillOpacity="0.2"/>;
+  }
+
+  renderLine (xScale, yScale) {
+    let path = d3.svg.line()
+        .x(d => xScale(d.date))
+        .y(d => yScale(d.value))
+        .interpolate(this.props.interpolate);
+
+    // TODO extract styling
+    return <path d={path(this.props.snapshots)} stroke="#4b9fd5" strokeWidth={this.props.lineWidth} fill="none"/>;
+  }
+
+  render () {
+    if (!this.state.width || !this.state.height) {
+      return <div/>;
+    }
+
+    let maxY = d3.max(this.props.snapshots, d => d.value);
+    let xScale = d3.time.scale()
+        .domain(d3.extent(this.props.snapshots, d => d.date))
+        .range([0, this.state.width - this.props.lineWidth]);
+    let yScale = d3.scale.linear()
+        .domain([0, maxY])
+        .range([this.state.height, 0]);
+
+    return <svg width={this.state.width} height={this.state.height}>
+      <g transform={`translate(${this.props.lineWidth / 2}, ${this.props.lineWidth / 2})`}>
+        {this.renderBackdrop(xScale, yScale, maxY)}
+        {this.renderLine(xScale, yScale)}
+      </g>
+    </svg>;
+  }
+}
+
+Timeline.defaultProps = {
+  lineWidth: 2,
+  displayBackdrop: true,
+  interpolate: 'basis'
+};
+
+Timeline.propTypes = {
+  snapshots: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
+};
diff --git a/server/sonar-web/src/main/js/components/charts/treemap.js b/server/sonar-web/src/main/js/components/charts/treemap.js
new file mode 100644 (file)
index 0000000..db6ceb8
--- /dev/null
@@ -0,0 +1,143 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import d3 from 'd3';
+import React from 'react';
+
+
+const SIZE_SCALE = d3.scale.linear()
+    .domain([3, 15])
+    .range([11, 18])
+    .clamp(true);
+
+
+function mostCommitPrefix (strings) {
+  var sortedStrings = strings.slice(0).sort(),
+      firstString = sortedStrings[0],
+      firstStringLength = firstString.length,
+      lastString = sortedStrings[sortedStrings.length - 1],
+      i = 0;
+  while (i < firstStringLength && firstString.charAt(i) === lastString.charAt(i)) {
+    i++;
+  }
+  var prefix = firstString.substr(0, i),
+      lastPrefixPart = _.last(prefix.split(/[\s\\\/]/));
+  return prefix.substr(0, prefix.length - lastPrefixPart.length);
+}
+
+
+export class TreemapRect extends React.Component {
+  render () {
+    let tooltipAttrs = {};
+    if (this.props.tooltip) {
+      tooltipAttrs = {
+        'data-toggle': 'tooltip',
+        'title': this.props.tooltip
+      };
+    }
+    let cellStyles = {
+      left: this.props.x,
+      top: this.props.y,
+      width: this.props.width,
+      height: this.props.height,
+      backgroundColor: this.props.fill,
+      fontSize: SIZE_SCALE(this.props.width / this.props.label.length),
+      lineHeight: `${this.props.height}px`
+    };
+    return <div className="treemap-cell" {...tooltipAttrs} style={cellStyles}>
+      <div className="treemap-inner" dangerouslySetInnerHTML={{ __html: this.props.label }}
+           style={{ maxWidth: this.props.width }}/>
+    </div>;
+  }
+}
+
+TreemapRect.propTypes = {
+  x: React.PropTypes.number.isRequired,
+  y: React.PropTypes.number.isRequired,
+  width: React.PropTypes.number.isRequired,
+  height: React.PropTypes.number.isRequired,
+  fill: React.PropTypes.string.isRequired
+};
+
+
+export class Treemap extends React.Component {
+  constructor (props) {
+    super();
+    this.state = { width: props.width, height: props.height };
+  }
+
+  componentDidMount () {
+    if (!this.props.width || !this.props.height) {
+      this.handleResize();
+      window.addEventListener('resize', this.handleResize.bind(this));
+    }
+    this.initTooltips();
+  }
+
+  componentDidUpdate () {
+    this.initTooltips();
+  }
+
+  componentWillUnmount () {
+    if (!this.props.width || !this.props.height) {
+      window.removeEventListener('resize', this.handleResize.bind(this));
+    }
+  }
+
+  initTooltips () {
+    $('[data-toggle="tooltip"]', React.findDOMNode(this))
+        .tooltip({ container: 'body', placement: 'top', html: true });
+  }
+
+  handleResize () {
+    let boundingClientRect = React.findDOMNode(this).parentNode.getBoundingClientRect();
+    let newWidth = this.props.width || boundingClientRect.width;
+    let newHeight = this.props.height || boundingClientRect.height;
+    this.setState({ width: newWidth, height: newHeight });
+  }
+
+  render () {
+    if (!this.state.width || !this.state.height || !this.props.items.length) {
+      return <div>&nbsp;</div>;
+    }
+
+    let sizeScale = d3.scale.linear()
+        .domain([0, d3.max(this.props.items, d => d.size)])
+        .range([5, 45]);
+    let treemap = d3.layout.treemap()
+        .round(true)
+        .value(d => sizeScale(d.size))
+        .size([this.state.width, 360]);
+    let nodes = treemap
+        .nodes({ children: this.props.items })
+        .filter(d => !d.children);
+
+    let prefix = mostCommitPrefix(this.props.labels),
+        prefixLength = prefix.length;
+
+    let rectangles = nodes.map((node, index) => {
+      let label = prefixLength ? `${prefix}<br>${this.props.labels[index].substr(prefixLength)}` :
+          this.props.labels[index];
+      let tooltip = index < this.props.tooltips.length ? this.props.tooltips[index] : null;
+      return <TreemapRect key={index}
+                          x={node.x}
+                          y={node.y}
+                          width={node.dx}
+                          height={node.dy}
+                          fill={node.color}
+                          label={label}
+                          prefix={prefix}
+                          tooltip={tooltip}/>;
+    });
+
+    return <div className="sonar-d3">
+      <div className="treemap-container" style={{ width: this.state.width, height: this.state.height }}>
+        {rectangles}
+      </div>
+    </div>;
+  }
+}
+
+Treemap.propTypes = {
+  labels: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
+  tooltips: React.PropTypes.arrayOf(React.PropTypes.string)
+};
diff --git a/server/sonar-web/src/main/js/components/charts/word-cloud.js b/server/sonar-web/src/main/js/components/charts/word-cloud.js
new file mode 100644 (file)
index 0000000..e2382d9
--- /dev/null
@@ -0,0 +1,59 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import React from 'react';
+
+export class Word extends React.Component {
+  render () {
+    let tooltipAttrs = {};
+    if (this.props.tooltip) {
+      tooltipAttrs = {
+        'data-toggle': 'tooltip',
+        'title': this.props.tooltip
+      };
+    }
+    return <a {...tooltipAttrs} style={{ fontSize: this.props.size }} href={this.props.link}>{this.props.text}</a>;
+  }
+}
+
+
+export class WordCloud extends React.Component {
+  componentDidMount () {
+    this.initTooltips();
+  }
+
+  componentDidUpdate () {
+    this.initTooltips();
+  }
+
+  initTooltips () {
+    $('[data-toggle="tooltip"]', React.findDOMNode(this))
+        .tooltip({ container: 'body', placement: 'bottom', html: true });
+  }
+
+  render () {
+    let len = this.props.items.length;
+    let sortedItems = _.sortBy(this.props.items, (item, idx) => {
+      let index = len - idx;
+      return (index % 2) * (len - index) + index / 2;
+    });
+
+    let sizeScale = d3.scale.linear()
+        .domain([0, d3.max(this.props.items, d => d.size)])
+        .range(this.props.sizeRange);
+    let words = sortedItems
+        .map((item, index) => <Word key={index}
+                                    text={item.text}
+                                    size={sizeScale(item.size)}
+                                    link={item.link}
+                                    tooltip={item.tooltip}/>);
+    return <div className="word-cloud">{words}</div>;
+  }
+}
+
+WordCloud.defaultProps = {
+  sizeRange: [10, 24]
+};
+
+WordCloud.propTypes = {
+  sizeRange: React.PropTypes.arrayOf(React.PropTypes.number)
+};
diff --git a/server/sonar-web/src/main/js/components/shared/assignee-helper.js b/server/sonar-web/src/main/js/components/shared/assignee-helper.js
new file mode 100644 (file)
index 0000000..6f3aedd
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+import Avatar from './avatar';
+
+export default class Assignee extends React.Component {
+  render () {
+    let avatar = this.props.user ?
+        <span className="spacer-right"><Avatar email={this.props.user.email} size={16}/></span> : null;
+    let name = this.props.user ? this.props.user.name : window.t('unassigned');
+    return <span>{avatar}{name}</span>;
+  }
+}
index a0e931aca2fb35dd507663885ae6700f03a36f7a..5aeec6e3b684c7d51100dd8eb5a2e432a9e9ff8f 100644 (file)
@@ -6,12 +6,11 @@ export default React.createClass({
     if (!this.props.severity) {
       return null;
     }
-    return (
-        <span>
-            <SeverityIcon severity={this.props.severity}/>
-          &nbsp;
-          {window.t('severity', this.props.severity)}
-          </span>
-    );
+    return <span>
+      <span className="spacer-right">
+        <SeverityIcon severity={this.props.severity}/>
+      </span>
+      {window.t('severity', this.props.severity)}
+    </span>;
   }
 });
index 6a57b3537f73d9ac695872f877778147001ca0ce..f369bb79b6753fd791899a6118ad135de7fb2609 100644 (file)
@@ -1,6 +1,13 @@
-export function getProjectUrl(project) {
+export function getProjectUrl (project) {
   if (typeof project !== 'string') {
     throw new TypeError('Project ID or KEY should be passed');
   }
-  return `${window.baseUrl}/overview?id=${encodeURIComponent(project)}`;
+  return `${window.baseUrl}/dashboard?id=${encodeURIComponent(project)}`;
+}
+
+export function componentIssuesUrl (componentKey, query) {
+  let serializedQuery = Object.keys(query).map(criterion => {
+    return `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}`;
+  }).join('|');
+  return window.baseUrl + '/component_issues?id=' + encodeURIComponent(componentKey) + '#' + serializedQuery;
 }
index 106c3a5c8715d5214456120ffba4818ea792a012..1e59a6f13ad9e2034578aec0864783ea63c7acaf 100644 (file)
   padding: 0 10px;
   .box-sizing(border-box);
 }
+
+
+.flex-columns {
+  display: flex;
+}
+
+.flex-column + .flex-column {
+  margin-left: 20px;
+}
+
+.flex-column-half {
+  width: 50%;
+}
+
+.flex-column-third {
+  width: ~"calc(100% / 3)";
+}
index 3226307b9aec3009225ae7412e5b567e9e4b7969..6a2fb4ae9e8bb8e013625f0580c52394eda16ec1 100644 (file)
@@ -1,5 +1,6 @@
 @import (reference) "../variables";
 @import (reference) "../mixins";
+@import (reference) "../init/links";
 
 .sonar-d3 {
 
@@ -233,3 +234,15 @@ text.max-results-reached-message {
 
   & > .icon-chevron-right { margin-right: 10px; }
 }
+
+.word-cloud {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-around;
+  align-items: center;
+
+  a {
+    padding: 5px;
+    .link-no-underline;
+  }
+}
index bf9cbb705487fa025dab740c12b4162be52eef0a..5cda531615546c41e06665091f86c9d6d66c9c96 100644 (file)
@@ -45,6 +45,7 @@
   text-decoration: none;
   background-color: @background;
   border-radius: 4px;
+  letter-spacing: 0.04em;
 }
 
 .tooltip-arrow {
index da3055a44858cc3019b182369524489644af06c1..4e0edc3fcbaeed86f7d362478f79c079c1c48d8a 100644 (file)
@@ -1,6 +1,7 @@
 @import (reference) "../variables";
 @import (reference) "../mixins";
 @import (reference) "../init/type";
+@import (reference) "../init/links";
 
 .overview {
   display: flex;
   font-weight: 300;
 }
 
-.overview-gate-box-error { background-color: @red; }
+.overview-gate-box-error {
+  background-color: @red;
+}
 
-.overview-gate-box-warn { background-color: @orange; }
+.overview-gate-box-warn {
+  background-color: @orange;
+}
 
-.overview-gate-box-ok { background-color: @green; }
+.overview-gate-box-ok {
+  background-color: @green;
+}
 
 .overview-gate-conditions {
   line-height: 70px;
@@ -88,6 +95,7 @@
 }
 
 .overview-title {
+  margin-bottom: 20px;
   font-size: 18px;
   font-weight: 400;
 
   }
 }
 
-.overview-title + .overview-cards:not(:empty) {
-  margin-top: 20px;
-}
-
 .overview-leak-period {
   margin-left: 10px;
   font-size: 14px;
 }
 
 .overview-nutshell {
-  padding: 50px 30px;
+  padding: 50px 30px 0;
+
+  .overview-card {
+    padding-bottom: 25px;
+  }
 }
 
 .overview-cards {
   }
 }
 
+.overview-card-section {
+  cursor: pointer;
+
+  &:hover, &.active {
+    border-bottom: 4px solid #2c3946;
+  }
+}
+
 .overview-measure {
   font-size: 28px;
 }
     .text-ellipsis;
   }
 }
+
+.overview-domain-dark {
+  background-color: #2c3946;
+  color: mix(#fff, #2c3946, 75%);
+
+  a {
+    color: @blue;
+    border-bottom-color: @darkBlue;
+
+    &:hover, &:focus {
+      border-bottom-color: @blue;
+    }
+  }
+
+  .overview-title {
+    color: mix(#fff, #2c3946, 75%);
+  }
+
+  table.data.zebra > tbody > tr:nth-child(odd) {
+    background-color: mix(#fff, #2c3946, 5%);;
+  }
+}
+
+.overview-domain-section {
+  padding: 50px 30px;
+}
+
+.overview-domain-header {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 20px;;
+  padding: 50px 30px 0;
+
+  .overview-title {
+    flex: 1;
+    margin: 0;
+    padding: 0;
+  }
+}
+
+.overview-timeline {
+  position: relative;
+
+  .line-chart {
+
+  }
+
+  .line-chart-grid {
+    shape-rendering: crispedges;
+    stroke: #384653;
+  }
+
+  .line-chart-path {
+    fill: none;
+    stroke-width: 2;
+    stroke: @blue;
+  }
+
+  .line-chart-point {
+    fill: @blue;
+    stroke: none;
+  }
+
+  .line-chart-tick {
+    fill: mix(#fff, #2c3946);
+    font-size: 11px;
+    text-anchor: middle;
+  }
+}
+
+.overview-timeline-select {
+  height: @formControlHeight;
+  border: 1px solid mix(#fff, #2c3946);
+  background-color: transparent;
+  color: mix(#fff, #2c3946);
+}
+
+.overview-bubble-chart {
+  .bubble-chart-tick {
+    fill: mix(#fff, #2c3946);
+    font-size: 11px;
+    text-anchor: middle;
+  }
+
+  .bubble-chart-tick-y {
+    text-anchor: end;
+  }
+
+  .bubble-chart-bubble {
+    stroke: @blue;
+    fill: @blue;
+    fill-opacity: 0.2;
+    transition: fill-opacity 0.2s ease;
+
+    &:hover {
+      fill-opacity: 0.5;
+    }
+  }
+}
+
+.overview-treemap {
+  .overview-domain-header {
+    padding-left: 0;
+    padding-right: 0;
+  }
+}
+
+.overview-chart-placeholder {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  align-content: center;
+}