]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6331 add project overview
authorStas Vilchik <vilchiks@gmail.com>
Tue, 4 Aug 2015 15:53:08 +0000 (17:53 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 10 Aug 2015 15:11:54 +0000 (17:11 +0200)
51 files changed:
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js
server/sonar-web/src/main/js/apps/issues/component-viewer/main.js
server/sonar-web/src/main/js/apps/issues/workspace-home-view.js
server/sonar-web/src/main/js/apps/nav/context-navbar-view.js
server/sonar-web/src/main/js/apps/nav/templates/nav-context-navbar.hbs
server/sonar-web/src/main/js/apps/overview/app.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/card.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/cards.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate-condition.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate-conditions.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/donut.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/gate-link.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/issues-link.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/measure.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/period-label.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/profile-link.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/rating.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak-coverage.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak-dups.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak-issues.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak-size.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/main.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/nutshell-coverage.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/nutshell-dups.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/nutshell-issues.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/nutshell-size.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/nutshell.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/handlebars-extensions.js
server/sonar-web/src/main/js/components/shared/severity-helper.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/severity-icon.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/status-helper.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/status-icon.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/source-viewer/main.js
server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js
server/sonar-web/src/main/js/libs/application.js
server/sonar-web/src/main/js/widgets/issue-filter/widget.js
server/sonar-web/src/main/less/components/badges.less
server/sonar-web/src/main/less/components/measures.less
server/sonar-web/src/main/less/init/icons.less
server/sonar-web/src/main/less/pages.less
server/sonar-web/src/main/less/pages/overview.less [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index d324e5b63fcd11c0e05b22147eb33fec0bc05431..a74b6f861714f9a8be5cef238ef7752e5f4ee7f6 100644 (file)
@@ -38,6 +38,8 @@ module.exports = (grunt) ->
 
     babel:
       build:
+        options:
+          modules: 'amd'
         files: [
           expand: true
           cwd: '<%= SOURCE_PATH %>/js'
@@ -142,6 +144,7 @@ module.exports = (grunt) ->
           'build-app:measures'
           'build-app:metrics'
           'build-app:nav'
+          'build-app:overview'
           'build-app:provisioning'
           'build-app:quality-gates'
           'build-app:quality-profiles'
index e25e8be81de9ece8bee256e501e5f89edce6aaf4..182e3b988d6a5052c92dff45282660583434407c 100644 (file)
@@ -30,12 +30,12 @@ define([
       var that = this;
       this.total = null;
       this.projects = [];
-      this.requestIssues().done(function () {
+      this.requestNutshellIssues().done(function () {
         that.render();
       });
     },
 
-    requestIssues: function () {
+    requestNutshellIssues: function () {
       var that = this,
           url = baseUrl + '/api/issues/search',
           options = {
index 1e79d92e45dd580fdccc9e74da635275f9512a39..5eabc8b76fd5942fce43df8b7ef6a2a5d3e9f991 100644 (file)
@@ -158,7 +158,7 @@ define([
       });
     },
 
-    requestIssues: function () {
+    requestNutshellIssues: function () {
       var that = this;
       var r;
       if (this.options.app.list.last().get('component') === this.model.get('key')) {
index 942e86e4ba7779ebe64c14580250366daf1960e6..dac3eb0bc5fb32f3f20a46882a4c377f6423bec4 100644 (file)
@@ -32,7 +32,7 @@ define([
 
     initialize: function () {
       this.model = new Backbone.Model();
-      this.requestIssues();
+      this.requestNutshellIssues();
       this.requestMyIssues();
     },
 
@@ -75,7 +75,7 @@ define([
       }
     },
 
-    requestIssues: function () {
+    requestNutshellIssues: function () {
       var that = this;
       var url = baseUrl + '/api/issues/search',
           options = {
index 95351c828ba10ea4d5138e17dcd83770d3becb5d..960599e1d218a08cbf3dd557e646cfffa7bc8e3f 100644 (file)
@@ -23,7 +23,7 @@ define([
 
   var $ = jQuery,
       MORE_URLS = [
-          '/dashboards', '/plugins/resource'
+          '/dashboards', '/dashboard', '/plugins/resource'
       ],
       SETTINGS_URLS = [
         '/project/settings', '/project/profile', '/project/qualitygate', '/manual_measures/index',
@@ -72,13 +72,11 @@ define([
           }) || (href.indexOf('/dashboard') !== -1 && search.indexOf('did=') !== -1),
           isSettingsActive = _.some(SETTINGS_URLS, function (url) {
             return href.indexOf(url) !== -1;
-          }),
-          isOverviewActive = !isMoreActive && href.indexOf('/dashboard') !== -1 && search.indexOf('did=') === -1;
+          });
       return _.extend(Marionette.LayoutView.prototype.serializeData.apply(this, arguments), {
         canManageContextDashboards: !!window.SS.user,
         contextKeyEncoded: encodeURIComponent(this.model.get('componentKey')),
 
-        isOverviewActive: isOverviewActive,
         isSettingsActive: isSettingsActive,
         isMoreActive: isMoreActive
       });
index e6eeb5cf547fcf8948983a44d9e9eeb315dbf4c4..a3504814f6387c9bb8e7b97305eb5f6c7ca7e477 100644 (file)
@@ -21,8 +21,8 @@
   </div>
 
   <ul class="nav navbar-nav nav-tabs">
-    <li {{#if isOverviewActive}}class="active"{{/if}}>
-      <a href="{{componentPermalink component.key}}">{{t 'overview.page'}}</a>
+    <li {{#isActiveLink '/overview'}}class="active"{{/isActiveLink}}>
+      <a href="{{componentOverviewPermalink component.key}}">{{t 'overview.page'}}</a>
     </li>
     <li {{#isActiveLink '/components'}}class="active"{{/isActiveLink}}>
       <a href="{{componentBrowsePermalink component.key}}">{{t 'components.page'}}</a>
       <a class="dropdown-toggle" data-toggle="dropdown" href="#">{{t 'more'}}&nbsp;<i class="icon-dropdown"></i></a>
       <ul class="dropdown-menu">
         <li class="dropdown-header">{{t 'layout.dashboards'}}</li>
-        {{#withoutFirst component.dashboards}}
+        {{#each component.dashboards}}
           <li>
             <a href="{{componentDashboardPermalink ../component.key key}}">{{dashboardL10n name}}</a>
           </li>
-        {{/withoutFirst}}
+        {{/each}}
         {{#if canManageContextDashboards}}
           <li class="small-divider"></li>
           <li>
diff --git a/server/sonar-web/src/main/js/apps/overview/app.jsx b/server/sonar-web/src/main/js/apps/overview/app.jsx
new file mode 100644 (file)
index 0000000..eca47e5
--- /dev/null
@@ -0,0 +1,18 @@
+import React from 'react';
+import Main from './main';
+
+const $ = jQuery;
+
+export default {
+  start: function (options) {
+    $('html').addClass('dashboard-page');
+    window.requestMessages().done(() => {
+      var el = document.querySelector(options.el);
+      React.render(<Main
+          component={options.component}
+          gate={options.gate}
+          measures={options.measures}
+          leak={options.leak}/>, el);
+    });
+  }
+};
diff --git a/server/sonar-web/src/main/js/apps/overview/card.jsx b/server/sonar-web/src/main/js/apps/overview/card.jsx
new file mode 100644 (file)
index 0000000..a221462
--- /dev/null
@@ -0,0 +1,7 @@
+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.jsx b/server/sonar-web/src/main/js/apps/overview/cards.jsx
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/gate-condition.jsx b/server/sonar-web/src/main/js/apps/overview/gate-condition.jsx
new file mode 100644 (file)
index 0000000..cd2c4ec
--- /dev/null
@@ -0,0 +1,35 @@
+import React from 'react';
+import Card from './card';
+import Measure from './helpers/measure';
+import {periodLabel, getPeriodDate} from './helpers/period-label';
+import DrilldownLink from './helpers/drilldown-link';
+
+export default React.createClass({
+  render() {
+    const
+        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} {period}</h4>
+          <div className="overview-gate-condition-value">
+            <i className={iconClassName}></i>&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.jsx b/server/sonar-web/src/main/js/apps/overview/gate-conditions.jsx
new file mode 100644 (file)
index 0000000..dddac46
--- /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() {
+    const 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.jsx b/server/sonar-web/src/main/js/apps/overview/gate.jsx
new file mode 100644 (file)
index 0000000..af924bd
--- /dev/null
@@ -0,0 +1,24 @@
+import React from 'react';
+import GateConditions from './gate-conditions';
+
+export default React.createClass({
+  render: function () {
+    if (!this.props.gate || !this.props.gate.level) {
+      return null;
+    }
+
+    const
+        badgeClassName = 'badge badge-' + this.props.gate.level.toLowerCase(),
+        badgeText = window.t('overview.gate', this.props.gate.level);
+
+    return (
+        <div className="overview-gate">
+          <div className="overview-title">
+            {window.t('overview.quality_gate')}
+            <span className={badgeClassName}>{badgeText}</span>
+          </div>
+          <GateConditions gate={this.props.gate} component={this.props.component}/>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/donut.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/donut.jsx
new file mode 100644 (file)
index 0000000..58ba8a3
--- /dev/null
@@ -0,0 +1,41 @@
+import React from 'react';
+
+const Sector = React.createClass({
+  render() {
+    const arc = d3.svg.arc()
+        .outerRadius(this.props.radius)
+        .innerRadius(this.props.radius - this.props.thickness);
+    return <path d={arc(this.props.data)} style={{ fill: this.props.fill }}></path>;
+  }
+});
+
+export default React.createClass({
+  getDefaultProps() {
+    return {
+      size: 30,
+      thickness: 6
+    };
+  },
+
+  render() {
+    const radius = this.props.size / 2;
+    const pie = d3.layout.pie().sort(null)
+        .value(d => {
+          return d.value
+        });
+    const data = this.props.data;
+    const sectors = pie(data).map((d, i) => {
+      return <Sector
+          key={i}
+          data={d}
+          fill={data[i].fill}
+          radius={radius}
+          thickness={this.props.thickness}/>;
+    });
+    return (
+        <svg width={this.props.size} height={this.props.size}>
+          <g transform={`translate(${radius}, ${radius})`}>{sectors}</g>
+        </svg>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.jsx
new file mode 100644 (file)
index 0000000..d9e9f99
--- /dev/null
@@ -0,0 +1,96 @@
+import React from 'react';
+import IssuesLink from './issues-link';
+
+export default React.createClass({
+  render() {
+    if (this.isIssueMeasure()) {
+      return this.renderIssuesLink();
+    }
+
+    let params = { id: this.props.component, metric: this.props.metric };
+    if (this.props.period) {
+      params.period = this.props.period;
+    }
+
+    const
+        query = Object.keys(params).map(function (key) {
+          return `${key}=${encodeURIComponent(params[key])}`;
+        }).join('&'),
+        url = `${baseUrl}/drilldown/measures?${query}`;
+
+    return <a href={url}>{this.props.children}</a>;
+  },
+
+  isIssueMeasure() {
+    const ISSUE_MEASURES = [
+      'violations',
+      'blocker_violations',
+      'critical_violations',
+      'major_violations',
+      'minor_violations',
+      'info_violations',
+      'new_blocker_violations',
+      'new_critical_violations',
+      'new_major_violations',
+      'new_minor_violations',
+      'new_info_violations',
+      'open_issues',
+      'reopened_issues',
+      'confirmed_issues',
+      'false_positive_issues'
+    ];
+    return ISSUE_MEASURES.indexOf(this.props.metric) !== -1;
+  },
+
+  propsToIssueParams() {
+    let params = {};
+    if (this.props.periodDate) {
+      params.createdAfter = moment(this.props.periodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+    }
+    switch (this.props.metric) {
+      case 'blocker_violations':
+      case 'new_blocker_violations':
+        _.extend(params, { resolved: 'false', severities: 'BLOCKER' });
+        break;
+      case 'critical_violations':
+      case 'new_critical_violations':
+        _.extend(params, { resolved: 'false', severities: 'CRITICAL' });
+        break;
+      case 'major_violations':
+      case 'new_major_violations':
+        _.extend(params, { resolved: 'false', severities: 'MAJOR' });
+        break;
+      case 'minor_violations':
+      case 'new_minor_violations':
+        _.extend(params, { resolved: 'false', severities: 'MINOR' });
+        break;
+      case 'info_violations':
+      case 'new_info_violations':
+        _.extend(params, { resolved: 'false', severities: 'INFO' });
+        break;
+      case 'open_issues':
+        _.extend(params, { resolved: 'false', statuses: 'OPEN' });
+        break;
+      case 'reopened_issues':
+        _.extend(params, { resolved: 'false', statuses: 'REOPENED' });
+        break;
+      case 'confirmed_issues':
+        _.extend(params, { resolved: 'false', statuses: 'CONFIRMED' });
+        break;
+      case 'false_positive_issues':
+        _.extend(params, { resolutions: 'FALSE-POSITIVE' });
+        break;
+      default:
+        _.extend(params, { resolved: 'false' });
+    }
+    return params;
+  },
+
+  renderIssuesLink() {
+    return (
+        <IssuesLink component={this.props.component} params={this.propsToIssueParams()}>
+          {this.props.children}
+        </IssuesLink>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.jsx
new file mode 100644 (file)
index 0000000..82831d8
--- /dev/null
@@ -0,0 +1,8 @@
+import React from 'react';
+
+export default React.createClass({
+  render: function () {
+    const url = `${baseUrl}/quality_gates/show/${this.props.gate}`;
+    return <a href={url}>{this.props.children}</a>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.jsx
new file mode 100644 (file)
index 0000000..edfaede
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+  render: function () {
+    var params = Object.keys(this.props.params).map((key) => {
+      return `${key}=${encodeURIComponent(this.props.params[key])}`;
+    }).join('|');
+    var url = `${baseUrl}/component_issues/index?id=${encodeURIComponent(this.props.component)}#${params}`;
+    return <a href={url}>{this.props.children}</a>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.jsx
new file mode 100644 (file)
index 0000000..18ca927
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+  render: function () {
+    if (this.props.value == null || isNaN(this.props.value)) {
+      return null;
+    }
+    var formatted = window.formatMeasureVariation(this.props.value, this.props.type);
+    return <span>{formatted}</span>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/measure.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/measure.jsx
new file mode 100644 (file)
index 0000000..b2f398b
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+  render: function () {
+    if (this.props.value == null || isNaN(this.props.value)) {
+      return null;
+    }
+    const formatted = window.formatMeasure(this.props.value, this.props.type);
+    return <span>{formatted}</span>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/period-label.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.jsx
new file mode 100644 (file)
index 0000000..996ea01
--- /dev/null
@@ -0,0 +1,15 @@
+export let periodLabel = (periods, periodIndex) => {
+  let period = _.findWhere(periods, { index: periodIndex });
+  if (!period) {
+    return null;
+  }
+  return window.tp(`overview.period.${period.mode}`, period.modeParam);
+};
+
+export let getPeriodDate = (periods, periodIndex) => {
+  let period = _.findWhere(periods, { index: periodIndex });
+  if (!period) {
+    return null;
+  }
+  return moment(period.date).toDate();
+};
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.jsx
new file mode 100644 (file)
index 0000000..c4f12bf
--- /dev/null
@@ -0,0 +1,8 @@
+import React from 'react';
+
+export default React.createClass({
+  render: function () {
+    const url = `${baseUrl}/profiles/show?key=${encodeURIComponent(this.props.profile)}`;
+    return <a href={url}>{this.props.children}</a>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/rating.jsx b/server/sonar-web/src/main/js/apps/overview/helpers/rating.jsx
new file mode 100644 (file)
index 0000000..a5337ec
--- /dev/null
@@ -0,0 +1,12 @@
+import React from 'react';
+
+export default React.createClass({
+  render: function () {
+    if (this.props.value == null || isNaN(this.props.value)) {
+      return null;
+    }
+    const formatted = window.formatMeasure(this.props.value, 'RATING');
+    const className = 'rating rating-' + formatted;
+    return <span className={className}>{formatted}</span>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/leak-coverage.jsx b/server/sonar-web/src/main/js/apps/overview/leak-coverage.jsx
new file mode 100644 (file)
index 0000000..db5b992
--- /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: function () {
+    const
+        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.jsx b/server/sonar-web/src/main/js/apps/overview/leak-dups.jsx
new file mode 100644 (file)
index 0000000..20cf2be
--- /dev/null
@@ -0,0 +1,43 @@
+import React from 'react';
+import Card from './card';
+import MeasureVariation from './helpers/measure-variation';
+import DrilldownLink from './helpers/drilldown-link';
+import Donut from './helpers/donut';
+
+export default React.createClass({
+  render: function () {
+    const
+        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.jsx b/server/sonar-web/src/main/js/apps/overview/leak-issues.jsx
new file mode 100644 (file)
index 0000000..3264dd6
--- /dev/null
@@ -0,0 +1,70 @@
+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 DrilldownLink from './helpers/drilldown-link';
+import SeverityIcon from 'components/shared/severity-icon';
+import StatusIcon from 'components/shared/status-icon';
+import SeverityHelper from 'components/shared/severity-helper';
+import {getPeriodDate} from './helpers/period-label';
+
+export default React.createClass({
+  render: function () {
+    const
+        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.jsx b/server/sonar-web/src/main/js/apps/overview/leak-size.jsx
new file mode 100644 (file)
index 0000000..d97b0a3
--- /dev/null
@@ -0,0 +1,31 @@
+import React from 'react';
+import Card from './card';
+import MeasureVariation from './helpers/measure-variation';
+import DrilldownLink from './helpers/drilldown-link';
+
+export default React.createClass({
+  render: function () {
+    const
+        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.jsx b/server/sonar-web/src/main/js/apps/overview/leak.jsx
new file mode 100644 (file)
index 0000000..da44c8f
--- /dev/null
@@ -0,0 +1,32 @@
+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: function () {
+    if (_.size(this.props.component.periods) < 3) {
+      return null;
+    }
+
+    const period = periodLabel(this.props.component.periods, '3');
+
+    return (
+        <div className="overview-leak">
+          <div className="overview-title">
+            {window.t('overview.water_leak')}
+            <span className="overview-leak-period">{period}</span>
+          </div>
+          <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/main.jsx b/server/sonar-web/src/main/js/apps/overview/main.jsx
new file mode 100644 (file)
index 0000000..5a9408f
--- /dev/null
@@ -0,0 +1,101 @@
+import React from 'react';
+import Gate from './gate';
+import Leak from './leak';
+import Nutshell from './nutshell';
+import Meta from './meta';
+import {getPeriodDate} from './helpers/period-label';
+
+const $ = jQuery;
+
+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) {
+    const url = `${baseUrl}/api/issues/search`;
+    data.ps = 1;
+    data.componentUuids = this.props.component.id;
+    return $.get(url, data);
+  },
+
+  requestLeakIssues() {
+    const createdAfter = moment(getPeriodDate(this.props.component.periods, '3')).format('YYYY-MM-DDTHH:mm:ssZZ');
+    this._requestIssues({ resolved: 'false', createdAfter, facets: 'severities,statuses' }).done(r => {
+      const
+          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 => {
+      const
+          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() {
+    const 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 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>
+    );
+  }
+})
diff --git a/server/sonar-web/src/main/js/apps/overview/meta.jsx b/server/sonar-web/src/main/js/apps/overview/meta.jsx
new file mode 100644 (file)
index 0000000..7971125
--- /dev/null
@@ -0,0 +1,67 @@
+import React from 'react';
+import ProfileLink from './helpers/profile-link';
+import GateLink from './helpers/gate-link';
+
+export default React.createClass({
+  render: function () {
+    const
+        profiles = (this.props.component.profiles || []).map(function (profile) {
+          return (
+              <li key={profile.key}>
+                <span className="note little-spacer-right">({profile.language})</span>
+                <ProfileLink profile={profile.key}>{profile.name}</ProfileLink>
+              </li>
+          );
+        }),
+        links = (this.props.component.links || []).map(function (link) {
+          const iconClassName = `little-spacer-right icon-color-link icon-${link.type}`;
+          return (
+              <li key={link.type}>
+                <i className={iconClassName}></i>
+                <a href={link.href} target="_blank">{link.name}</a>
+              </li>
+          );
+        });
+
+    const descriptionCard = this.props.component.description ? (
+            <div className="overview-card">
+              <div className="overview-meta-description">{this.props.component.description}</div>
+            </div>
+        ) : null,
+
+        linksCard = _.size(this.props.component.links) > 0 ? (
+            <div className="overview-card">
+              <ul className="overview-meta-list">{links}</ul>
+            </div>
+        ) : null,
+
+        profilesCard = _.size(this.props.component.profiles) > 0 ? (
+            <div className="overview-card">
+              <h4 className="overview-meta-header">{window.t('overview.quality_profiles')}</h4>
+              <ul className="overview-meta-list">{profiles}</ul>
+            </div>
+        ) : null,
+
+        gateCard = this.props.component.gate ? (
+            <div className="overview-card">
+              <h4 className="overview-meta-header">{window.t('overview.quality_gate')}</h4>
+              <ul className="overview-meta-list">
+                <li>
+                  {this.props.component.gate.isDefault ?
+                      <span className="note little-spacer-right">(Default)</span> : null}
+                  <GateLink gate={this.props.component.gate.key}>{this.props.component.gate.name}</GateLink>
+                </li>
+              </ul>
+            </div>
+        ) : null;
+
+    return (
+        <div className="overview-meta">
+          {descriptionCard}
+          {linksCard}
+          {profilesCard}
+          {gateCard}
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.jsx b/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.jsx
new file mode 100644 (file)
index 0000000..a788004
--- /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: function () {
+    const
+        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.jsx b/server/sonar-web/src/main/js/apps/overview/nutshell-dups.jsx
new file mode 100644 (file)
index 0000000..b0c1df3
--- /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: function () {
+    const
+        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.jsx b/server/sonar-web/src/main/js/apps/overview/nutshell-issues.jsx
new file mode 100644 (file)
index 0000000..d1c242b
--- /dev/null
@@ -0,0 +1,69 @@
+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 SeverityHelper from 'components/shared/severity-helper';
+import StatusIcon from 'components/shared/status-icon';
+
+export default React.createClass({
+  render: function () {
+    const
+        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.jsx b/server/sonar-web/src/main/js/apps/overview/nutshell-size.jsx
new file mode 100644 (file)
index 0000000..6591119
--- /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: function () {
+    const
+        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.jsx b/server/sonar-web/src/main/js/apps/overview/nutshell.jsx
new file mode 100644 (file)
index 0000000..f15d3b8
--- /dev/null
@@ -0,0 +1,23 @@
+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: function () {
+    const props = { measures: this.props.measures, component: this.props.component };
+    return (
+        <div className="overview-nutshell">
+          <div className="overview-title">{window.t('overview.project_in_a_nutshell')}</div>
+          <Cards>
+            <NutshellIssues {...props}/>
+            <NutshellCoverage {...props}/>
+            <NutshellDups {...props}/>
+            <NutshellSize {...props}/>
+          </Cards>
+        </div>
+    );
+  }
+});
index e8e8c13f3e9e020edeb915632e4e9b35b291233c..c85329a0535ee8a29359ad3f4b96f9d02192e202 100644 (file)
     return baseUrl + '/dashboard/index?id=' + encodeURIComponent(componentKey);
   });
 
+  Handlebars.registerHelper('componentOverviewPermalink', function (componentKey) {
+    return baseUrl + '/overview/index?id=' + encodeURIComponent(componentKey);
+  });
+
   Handlebars.registerHelper('componentDashboardPermalink', function (componentKey, dashboardKey) {
     var params = [
       { key: 'id', value: componentKey },
diff --git a/server/sonar-web/src/main/js/components/shared/severity-helper.jsx b/server/sonar-web/src/main/js/components/shared/severity-helper.jsx
new file mode 100644 (file)
index 0000000..a0e931a
--- /dev/null
@@ -0,0 +1,17 @@
+import React from 'react';
+import SeverityIcon from './severity-icon';
+
+export default React.createClass({
+  render() {
+    if (!this.props.severity) {
+      return null;
+    }
+    return (
+        <span>
+            <SeverityIcon severity={this.props.severity}/>
+          &nbsp;
+          {window.t('severity', this.props.severity)}
+          </span>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/shared/severity-icon.jsx b/server/sonar-web/src/main/js/components/shared/severity-icon.jsx
new file mode 100644 (file)
index 0000000..87aec17
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    if (!this.props.severity) {
+      return null;
+    }
+    var className = 'icon-severity-' + this.props.severity.toLowerCase();
+    return <i className={className}></i>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/shared/status-helper.jsx b/server/sonar-web/src/main/js/components/shared/status-helper.jsx
new file mode 100644 (file)
index 0000000..033fd3f
--- /dev/null
@@ -0,0 +1,26 @@
+define([
+  'libs/third-party/react',
+  './status-icon'
+], function (React, StatusIcon) {
+
+  return React.createClass({
+    render: function () {
+      if (!this.props.status) {
+        return null;
+      }
+      var resolution;
+      if (this.props.resolution) {
+        resolution = ' (' + window.t('issue.resolution', this.props.resolution) + ')';
+      }
+      return (
+          <span>
+            <StatusIcon status={this.props.status}/>
+            &nbsp;
+            {window.t('issue.status', this.props.status)}
+            {resolution}
+          </span>
+      );
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/components/shared/status-icon.jsx b/server/sonar-web/src/main/js/components/shared/status-icon.jsx
new file mode 100644 (file)
index 0000000..5acd210
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    if (!this.props.status) {
+      return null;
+    }
+    var className = 'icon-status-' + this.props.status.toLowerCase();
+    return <i className={className}></i>;
+  }
+});
index 34df48218499c5d3020c9180f8903b3e9727dbb5..1a87855f0872ffd98c2e1ae6e6961faf46fc9910 100644 (file)
@@ -125,7 +125,7 @@ define([
           var that = this,
               opts = typeof options === 'object' ? options : {},
               finalize = function () {
-                that.requestIssues().done(function () {
+                that.requestNutshellIssues().done(function () {
                   that.render();
                   that.trigger('loaded');
                 });
@@ -269,7 +269,7 @@ define([
           });
         },
 
-        requestIssues: function () {
+        requestNutshellIssues: function () {
           var that = this,
               options = {
                 data: {
index 0341d42b88363932058e40816cccf66eb6c0d71b..54ec351b29b63f8649bd063d45715825fdd4e698 100644 (file)
@@ -31,7 +31,7 @@ define([
 
     initialize: function () {
       var that = this,
-          requests = [this.requestMeasures(), this.requestIssues()];
+          requests = [this.requestMeasures(), this.requestNutshellIssues()];
       if (this.model.get('isUnitTest')) {
         requests.push(this.requestTests());
       }
@@ -132,7 +132,7 @@ define([
       });
     },
 
-    requestIssues: function () {
+    requestNutshellIssues: function () {
       var that = this,
           url = baseUrl + '/api/issues/search',
           options = {
index e26addbfd1bf0d52c96e8ce4410f72ee94b2c804..7d141563f4964071d19599eef18d046986208e16 100644 (file)
@@ -465,6 +465,7 @@ function closeModalWindow () {
    * @returns {string}
    */
   var shortDurationFormatter = function (value) {
+    value = parseInt(value, 10);
     if (value === 0) {
       return '0';
     }
@@ -518,6 +519,9 @@ function closeModalWindow () {
           'PERCENT': function (value) {
             return numeral(+value / 100).format('0,0.0%');
           },
+          'SHORT_PERCENT': function (value) {
+            return numeral(+value / 100).format('0,0%');
+          },
           'WORK_DUR': durationFormatter,
           'SHORT_WORK_DUR': shortDurationFormatter,
           'RATING': ratingFormatter
index 97c72ca75a16b9fe033514d50f8ce18dc5a8c4d1..e46c637781570b9bf89397d4b30264ea54be85ab 100644 (file)
@@ -258,7 +258,7 @@ define(['./templates'], function () {
       this.listenTo(this.model, 'change', this.render);
       this.conf = byDistributionConf[this.options.distributionAxis];
       this.query = this.getParsedQuery();
-      this.requestIssues();
+      this.requestNutshellIssues();
     },
 
     getParsedQuery: function () {
@@ -317,7 +317,7 @@ define(['./templates'], function () {
       });
     },
 
-    requestIssues: function () {
+    requestNutshellIssues: function () {
       var that = this,
           facetMode = this.options.displayMode,
           url = baseUrl + '/api/issues/search',
index 756b3d8c0bf9004fc3d0c0ec4a2c9c9e18719a47..6ed82633d463e3d1b3123a704bf1181b4479e8b6 100644 (file)
   &:hover, &:focus, &:active { color: @blue; }
 }
 
-.badge-success {
+.badge-success,
+.badge-ok {
   background-color: @green;
 }
 
-.badge-warning {
+.badge-warning,
+.badge-warn {
   background-color: @orange;
 }
+
+.badge-danger,
+.badge-error {
+  background-color: @red
+}
index c7ef0a761a63bcb9ed6d541f328e6b50d46964af..eecb0d2e63f22ee95ededd43caa72b1753f9839e 100644 (file)
   }
 }
 
+.measures-chart-indent {
+  padding-left: 90px;
+}
+
 .measure {
   line-height: 1.3333333333333;
 }
   vertical-align: middle;
 
   .measure-name {
-    font-size: 16px;
+    margin-top: 2px;
+    font-size: 15px;
     font-weight: 300;
   }
 
 }
 
 .measure-one-line {
-  .justify;
+  .clearfix;
 
   .measure-name {
-    display: inline-block;
+    float: left;
   }
 
   .measure-value {
-    display: inline-block;
-    text-align: right;
+    float: right;
   }
 }
 
index 42a0c0e682d7d065fbbe35fa3b4692715f47feba..ca4a8b2b49fc004e1cdbcd4740bfc5053b27596d 100644 (file)
@@ -64,6 +64,7 @@ a[class^="icon-"], a[class*=" icon-"] {
 .icon-black { color: @baseFontColor; }
 .icon-red { color: @red; }
 .icon-green { color: @green; }
+.icon-color-link { color: @darkBlue; }
 
 
 /*
index bbd9f119eaf1ab9ca1957c0432a7c568873c7b1d..b0e1b6ce99a467ce6b4b4c3678fe8e173b98bc51 100644 (file)
@@ -1,22 +1,3 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * SonarQube is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
 @import "pages/analysis-reports";
 @import "pages/coding-rules";
 @import "pages/dashboard";
@@ -26,3 +7,4 @@
 @import "pages/quality-gates";
 @import "pages/maintenance";
 @import "pages/login";
+@import "pages/overview";
diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less
new file mode 100644 (file)
index 0000000..e4ec6a5
--- /dev/null
@@ -0,0 +1,190 @@
+@import (reference) "../variables";
+@import (reference) "../mixins";
+@import (reference) "../init/type";
+
+.overview {
+  display: table;
+  width: 100%;
+  min-height: ~"calc(100vh - @{navbarGlobalHeight} - @{navbarContextHeight} - @{pageFooterHeight})";
+}
+
+.overview-main {
+  display: table-cell;
+  vertical-align: top;
+  .box-sizing(border-box);
+  background-color: #fff;
+}
+
+.overview-gate {
+  .clearfix;
+  padding: 50px 30px;
+}
+
+.overview-gate-box {
+  float: left;
+  .size(120px, 70px);
+  padding: 10px;
+  .box-sizing(border-box);
+  line-height: 24px;
+  color: #fff;
+  font-size: 16px;
+  font-weight: 300;
+}
+
+.overview-gate-box-error { background-color: @red; }
+
+.overview-gate-box-warn { background-color: @orange; }
+
+.overview-gate-box-ok { background-color: @green; }
+
+.overview-gate-conditions {
+  line-height: 70px;
+  font-size: 0;
+  white-space: nowrap;
+  overflow: hidden;
+
+  & > li {
+    display: inline-block;
+    vertical-align: middle;
+    padding: 0 20px;
+    .box-sizing(border-box);
+    font-size: @baseFontSize;
+    line-height: 1;
+  }
+}
+
+.overview-gate-condition-metric {
+  //color: mix(@baseFontColor, @barBackgroundColor, 70%);
+  font-weight: 300;
+  font-size: 15px;
+  //letter-spacing: 0.03em;
+}
+
+.overview-gate-condition-value {
+  margin-top: 8px;
+  font-weight: 300;
+  font-size: 20px;
+
+  i {
+    position: relative;
+    top: -1px;
+  }
+}
+
+.overview-gate-condition-itself {
+  padding-left: 4px;
+  color: mix(@baseFontColor, @barBackgroundColor, 70%);
+  font-size: 13px;
+}
+
+.overview-gate-condition-level {
+  margin-top: 8px;
+}
+
+.overview-leak {
+  padding: 50px 30px;
+  border-top: 1px solid @barBorderColor;
+  border-bottom: 1px solid @barBorderColor;
+}
+
+.overview-title {
+  margin-bottom: 20px;
+  font-size: 24px;
+  font-weight: 300;
+
+  & > .badge {
+    position: relative;
+    top: -2px;
+    margin-left: 15px;
+    padding: 8px 15px;
+    font-size: 16px;
+  }
+}
+
+.overview-leak-period {
+  margin-left: 10px;
+  font-size: 16px;
+}
+
+.overview-nutshell {
+  padding: 50px 30px;
+}
+
+.overview-cards {
+}
+
+.overview-card {
+  display: inline-block;
+  vertical-align: top;
+  width: 200px;
+  margin-right: 30px;
+  .box-sizing(border-box);
+
+  &:last-child { margin-right: 0; }
+
+  .overview-main & {
+    font-weight: 300;
+    font-size: 14px;
+  }
+}
+
+.overview-measure {
+  font-size: 28px;
+}
+
+.overview-measure-label {
+  font-size: 16px;
+}
+
+.overview-meta {
+  display: table-cell;
+  vertical-align: top;
+  width: 180px;
+  padding: 30px;
+  border-left: 1px solid @barBorderColor;
+  background-color: @barBackgroundColor;
+
+  .panel {
+    border: none !important;
+  }
+}
+
+.overview-meta-description {
+  line-height: 1.5;
+}
+
+.overview-meta-header {
+  color: #797979;
+}
+
+.overview-meta-list {
+  & > li {
+    padding-bottom: 4px;
+    .text-ellipsis;
+  }
+}
+
+@media (max-width: 1200px) {
+  .overview {
+    display: block;
+  }
+
+  .overview-main {
+    display: block;
+  }
+
+  .overview-meta {
+    display: block;
+    width: auto;
+    border-top: 1px solid @barBorderColor;
+    border-left: none;
+  }
+}
+
+@media (min-width: 1201px) {
+  .overview-meta .overview-card {
+    width: 180px;
+    margin-right: 0;
+    margin-bottom: 30px;
+  }
+}
index 9ec1e477893fd07facb39f82ff0a7849592b9478..63b7a4c763671496818b26c0d11c1f2759e501d2 100644 (file)
@@ -17,6 +17,7 @@
 # along with this program; if not, write to the Free Software Foundation,
 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 #
+include ERB::Util
 
 class DashboardController < ApplicationController
 
@@ -26,6 +27,10 @@ class DashboardController < ApplicationController
 
   def index
     load_resource()
+    if @resource && !params[:did]
+      overview_url = url_for({:controller => 'overview', :action => :index}) + '?id=' + url_encode(@resource.key)
+      redirect_to overview_url
+    end
     if !@resource || @resource.display_dashboard?
       redirect_if_bad_component()
       load_dashboard()
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb
new file mode 100644 (file)
index 0000000..746bc65
--- /dev/null
@@ -0,0 +1,29 @@
+#
+# SonarQube, open source software quality management tool.
+# Copyright (C) 2008-2014 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# SonarQube is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# SonarQube is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+class OverviewController < ApplicationController
+  before_filter :init_resource_for_user_role
+
+  SECTION=Navigation::SECTION_RESOURCE
+
+  def index
+
+  end
+
+end
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb
new file mode 100644 (file)
index 0000000..b384804
--- /dev/null
@@ -0,0 +1,164 @@
+<%
+   links_size = @resource.project_links.size
+
+   profiles = []
+   qprofiles_measure = @snapshot.measure(Metric::QUALITY_PROFILES)
+   if qprofiles_measure && !qprofiles_measure.data.blank?
+     profiles = JSON.parse qprofiles_measure.data
+   end
+   profiles_size = profiles.size
+
+   is_gate_default = false
+   gate = nil
+   gate_id = Property.value('sonar.qualitygate', @resource && @resource.id, nil)
+   unless gate_id
+     gate_id=Property.value('sonar.qualitygate', nil, nil)
+     is_gate_default = false || gate_id
+   end
+   if gate_id
+     gate = Internal.quality_gates.get(gate_id.to_i)
+   end
+%>
+
+<% m = @snapshot.measure(Metric::QUALITY_GATE_DETAILS)
+   if m && !m.data.blank?
+     details = JSON.parse m.data
+     m.alert_status = details['level']
+     raw_conditions = details['conditions']
+     conditions = []
+     missing_metric = false
+     raw_conditions.each do |condition|
+       if metric(condition['metric']).nil?
+         missing_metric = true
+       else
+         conditions << condition
+       end
+     end
+     alert_metric = metric(Metric::ALERT_STATUS)
+   end
+%>
+
+<% content_for :extra_script do %>
+  <script>
+    (function () {
+      var component = {
+        id: '<%= @resource.uuid %>',
+        key: '<%= @resource.key %>',
+        description: '<%= @resource.description %>',
+        periods: [
+          <%
+            if @snapshot.project_snapshot.periods?
+              (1..5).each do |index|
+                if @snapshot.period_mode(index)
+          %>
+          {
+            index: '<%= index -%>',
+            mode: '<%= @snapshot.period_mode(index) -%>',
+            modeParam: '<%= @snapshot.period_param(index) -%>',
+            date: '<%= @snapshot.period_datetime(index).to_date.strftime('%FT%T%z') -%>'
+          },
+          <% end %>
+          <% end %>
+          <% end %>
+        ],
+        links: [
+          <% @resource.project_links.sort.each_with_index do |link, index| %>
+          {
+            name: '<%= escape_javascript link.name -%>',
+            type: '<%= escape_javascript link.link_type -%>',
+            href: '<%= escape_javascript link.href -%>'
+          }<% if index < links_size - 1 %>, <% end -%>
+          <% end %>
+        ],
+        profiles: [
+          <% profiles.each_with_index do |profile, index| %>
+          {
+            name: '<%= escape_javascript profile['name'] -%>',
+            key: '<%= escape_javascript profile['key']-%>',
+            language: '<%= escape_javascript Api::Utils.language_name(profile['language']) -%>'
+          }<% if index < profiles_size - 1 %>, <% end -%>
+          <% end %>
+        ],
+        <% if gate %>
+        gate: {
+          name: '<%= escape_javascript gate.getName() -%>',
+          key: <%= escape_javascript gate_id -%>,
+          isDefault: <%= is_gate_default -%>
+        }
+        <% end %>
+      };
+
+      <% if m %>
+      var gate = {
+        level: '<%= m.alert_status -%>',
+        conditions: [
+          <% conditions.sort_by {|condition| [ -condition['level'].length, metric(condition['metric']).short_name] }.each do |condition| %>
+          <% metric = metric(condition['metric']) %>
+          {
+            level: '<%= escape_javascript condition['level'] %>',
+            metric: {
+              name: '<%= escape_javascript metric.name %>',
+              type: '<%= escape_javascript metric.value_type %>'
+            },
+            op: '<%= escape_javascript condition['op'] %>',
+            period: '<%= condition['period'] %>',
+            warning: '<%= escape_javascript condition['warning'] %>',
+            error: '<%= escape_javascript condition['error'] %>',
+            actual: '<%= escape_javascript condition['actual'] %>',
+          },
+          <% end %>
+        ]
+      };
+      <% else %>
+      var gate = null;
+      <% end %>
+
+      var measures = {
+
+        // issues
+        sqaleRating: '<%= @snapshot.measure('sqale_rating').value -%>',
+
+        // coverage
+        <% if @snapshot.measure('overall_coverage') %>
+        coverage: '<%= @snapshot.measure('overall_coverage').value -%>',
+        <% end %>
+        <% if @snapshot.measure('tests') %>
+        tests: '<%= @snapshot.measure('tests').value -%>',
+        <% end %>
+
+        // duplications
+        duplications: '<%= @snapshot.measure('duplicated_lines_density').value -%>',
+        duplicatedLines: '<%= @snapshot.measure('duplicated_lines').value -%>',
+        duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').value -%>',
+
+        // size
+        lines: '<%= @snapshot.measure('lines').value -%>',
+        files: '<%= @snapshot.measure('files').value -%>'
+      };
+
+      var leak = {
+
+        // coverage
+        <% if @snapshot.measure('new_overall_coverage') %>
+        newCoverage: '<%= @snapshot.measure('new_overall_coverage').variation(3) -%>',
+        <% end %>
+        <% if @snapshot.measure('tests') %>
+        tests: '<%= @snapshot.measure('tests').variation(3) -%>',
+        <% end %>
+
+        // duplications
+        duplications: '<%= @snapshot.measure('duplicated_lines_density').variation(3) -%>',
+        duplicatedLines: '<%= @snapshot.measure('duplicated_lines').variation(3) -%>',
+        duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').variation(3) -%>',
+
+        // size
+        lines: '<%= @snapshot.measure('lines').variation(3) -%>',
+        files: '<%= @snapshot.measure('files').variation(3) -%>'
+      };
+
+      require(['apps/overview/app'], function (App) {
+        App.start({ el: '#content', component: component, gate: gate, measures: measures, leak: leak });
+      });
+    })();
+  </script>
+<% end %>
index e7bcba4207d959a1f90efef00bd5d618feb49d53..18dfdf16d5b9c35779f8901e39818474e680c2fd 100644 (file)
@@ -3020,3 +3020,35 @@ update_center.status.COMPATIBLE=Compatible
 update_center.status.INCOMPATIBLE=Incompatible
 update_center.status.REQUIRES_SYSTEM_UPGRADE=Requires system update
 update_center.status.DEPS_REQUIRE_SYSTEM_UPGRADE=Some of dependencies requires system update
+
+
+
+#------------------------------------------------------------------------------
+#
+# OVERVIEW
+#
+#------------------------------------------------------------------------------
+overview.quality_gate=Quality Gate
+overview.quality_profiles=Quality Profiles
+overview.water_leak=Water Leak
+overview.project_in_a_nutshell=Project In a Nutshell
+
+overview.metric.new_coverage=New Coverage
+overview.metric.tests=tests
+overview.metric.duplications=Duplications
+overview.metric.duplicated_lines=lines
+overview.metric.debt=Debt
+overview.metric.issues=Issues
+overview.metric.new_debt=New Debt
+overview.metric.new_issues=New Issues
+overview.metric.lines=Lines
+overview.metric.files=Files
+overview.metric.coverage=Coverage
+
+overview.period.previous_version=since {0}
+overview.period.previous_analysis=since previous analysis
+overview.period.days=last {0} days
+
+overview.gate.ERROR=Failed
+overview.gate.WARN=Warning
+overview.gate.OK=Passed