]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6331 add project overview page
authorStas Vilchik <vilchiks@gmail.com>
Mon, 19 Oct 2015 14:40:37 +0000 (16:40 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Tue, 20 Oct 2015 09:03:41 +0000 (11:03 +0200)
42 files changed:
server/sonar-web/package.json
server/sonar-web/src/main/js/apps/overview/app.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/card.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/cards.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/empty.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate-condition.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate-conditions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate-empty.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/donut.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/measure.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/period-label.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/rating.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak-coverage.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak-dups.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak-issues.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak-size.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/leak.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/main.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/nutshell-dups.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/nutshell-issues.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/nutshell-size.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/nutshell.js [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/Url.js
server/sonar-web/src/main/js/libs/application.js
server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
server/sonar-web/src/main/less/components/measures.less
server/sonar-web/src/main/less/components/navbar.less
server/sonar-web/src/main/less/components/ui.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/less/variables.less
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 0b9f0251293a67606aece0bef557995050a4f748..2d21537cd6d80d2b3874ac5f6c8e3633bdbdcbee 100644 (file)
@@ -52,7 +52,8 @@
   },
   "browserify-shim": {
     "jquery": "global:jQuery",
-    "underscore": "global:_"
+    "underscore": "global:_",
+    "d3": "global:d3"
   },
   "browserify": {
     "transform": [
diff --git a/server/sonar-web/src/main/js/apps/overview/app.js b/server/sonar-web/src/main/js/apps/overview/app.js
new file mode 100644 (file)
index 0000000..80bc990
--- /dev/null
@@ -0,0 +1,26 @@
+import $ from 'jquery';
+import _ from 'underscore';
+import React from 'react';
+import Main from './main';
+import Empty from './empty';
+
+class App {
+  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);
+    });
+  }
+}
+
+window.sonarqube.appStarted.then(options => new App().start(options));
diff --git a/server/sonar-web/src/main/js/apps/overview/card.js b/server/sonar-web/src/main/js/apps/overview/card.js
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.js b/server/sonar-web/src/main/js/apps/overview/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/empty.js b/server/sonar-web/src/main/js/apps/overview/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/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/gate-condition.js
new file mode 100644 (file)
index 0000000..2cc4282
--- /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/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/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/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/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/gate.js b/server/sonar-web/src/main/js/apps/overview/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/helpers/donut.js b/server/sonar-web/src/main/js/apps/overview/helpers/donut.js
new file mode 100644 (file)
index 0000000..368a522
--- /dev/null
@@ -0,0 +1,34 @@
+import d3 from 'd3';
+import React from 'react';
+
+let Sector = React.createClass({
+  render() {
+    let arc = d3.svg.arc()
+        .outerRadius(this.props.radius)
+        .innerRadius(this.props.radius - this.props.thickness);
+    return <path d={arc(this.props.data)} style={{ fill: this.props.fill }}/>;
+  }
+});
+
+export default React.createClass({
+  getDefaultProps() {
+    return {
+      size: 30,
+      thickness: 6
+    };
+  },
+
+  render() {
+    let radius = this.props.size / 2;
+    let pie = d3.layout.pie()
+        .sort(null)
+        .value(d => d.value);
+    let data = this.props.data;
+    let sectors = pie(data).map((d, i) => {
+      return <Sector key={i} data={d} fill={data[i].fill} radius={radius} thickness={this.props.thickness}/>;
+    });
+    return <svg width={this.props.size} height={this.props.size}>
+      <g transform={`translate(${radius}, ${radius})`}>{sectors}</g>
+    </svg>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/drilldown-link.js
new file mode 100644 (file)
index 0000000..338a34b
--- /dev/null
@@ -0,0 +1,95 @@
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+import IssuesLink from './issues-link';
+
+export default React.createClass({
+  isIssueMeasure() {
+    const ISSUE_MEASURES = [
+      'violations',
+      'blocker_violations',
+      'critical_violations',
+      'major_violations',
+      'minor_violations',
+      'info_violations',
+      'new_blocker_violations',
+      'new_critical_violations',
+      'new_major_violations',
+      'new_minor_violations',
+      'new_info_violations',
+      'open_issues',
+      'reopened_issues',
+      'confirmed_issues',
+      'false_positive_issues'
+    ];
+    return ISSUE_MEASURES.indexOf(this.props.metric) !== -1;
+  },
+
+  propsToIssueParams() {
+    let params = {};
+    if (this.props.periodDate) {
+      params.createdAfter = moment(this.props.periodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+    }
+    switch (this.props.metric) {
+      case 'blocker_violations':
+      case 'new_blocker_violations':
+        _.extend(params, { resolved: 'false', severities: 'BLOCKER' });
+        break;
+      case 'critical_violations':
+      case 'new_critical_violations':
+        _.extend(params, { resolved: 'false', severities: 'CRITICAL' });
+        break;
+      case 'major_violations':
+      case 'new_major_violations':
+        _.extend(params, { resolved: 'false', severities: 'MAJOR' });
+        break;
+      case 'minor_violations':
+      case 'new_minor_violations':
+        _.extend(params, { resolved: 'false', severities: 'MINOR' });
+        break;
+      case 'info_violations':
+      case 'new_info_violations':
+        _.extend(params, { resolved: 'false', severities: 'INFO' });
+        break;
+      case 'open_issues':
+        _.extend(params, { resolved: 'false', statuses: 'OPEN' });
+        break;
+      case 'reopened_issues':
+        _.extend(params, { resolved: 'false', statuses: 'REOPENED' });
+        break;
+      case 'confirmed_issues':
+        _.extend(params, { resolved: 'false', statuses: 'CONFIRMED' });
+        break;
+      case 'false_positive_issues':
+        _.extend(params, { resolutions: 'FALSE-POSITIVE' });
+        break;
+      default:
+        _.extend(params, { resolved: 'false' });
+    }
+    return params;
+  },
+
+  renderIssuesLink() {
+    return <IssuesLink component={this.props.component} params={this.propsToIssueParams()}>
+      {this.props.children}
+    </IssuesLink>;
+  },
+
+  render() {
+    if (this.isIssueMeasure()) {
+      return this.renderIssuesLink();
+    }
+
+    let params = { id: this.props.component, metric: this.props.metric };
+    if (this.props.period) {
+      params.period = this.props.period;
+    }
+
+    let query = Object.keys(params).map(key => {
+          return `${key}=${encodeURIComponent(params[key])}`;
+        }).join('&'),
+        url = `${baseUrl}/drilldown/measures?${query}`;
+
+    return <a href={url}>{this.props.children}</a>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/gate-link.js
new file mode 100644 (file)
index 0000000..79878ac
--- /dev/null
@@ -0,0 +1,8 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    let url = `${baseUrl}/quality_gates/show/${this.props.gate}`;
+    return <a href={url}>{this.props.children}</a>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js b/server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js
new file mode 100644 (file)
index 0000000..d311857
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    let params = Object.keys(this.props.params).map((key) => {
+          return `${key}=${encodeURIComponent(this.props.params[key])}`;
+        }).join('|'),
+        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.js b/server/sonar-web/src/main/js/apps/overview/helpers/measure-variation.js
new file mode 100644 (file)
index 0000000..0e2e1fa
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    if (this.props.value == null || isNaN(this.props.value)) {
+      return null;
+    }
+    let 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.js b/server/sonar-web/src/main/js/apps/overview/helpers/measure.js
new file mode 100644 (file)
index 0000000..dafc98a
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    if (this.props.value == null || isNaN(this.props.value)) {
+      return null;
+    }
+    let 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.js b/server/sonar-web/src/main/js/apps/overview/helpers/period-label.js
new file mode 100644 (file)
index 0000000..109a9df
--- /dev/null
@@ -0,0 +1,18 @@
+import _ from 'underscore';
+import moment from 'moment';
+
+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.js b/server/sonar-web/src/main/js/apps/overview/helpers/profile-link.js
new file mode 100644 (file)
index 0000000..22065ab
--- /dev/null
@@ -0,0 +1,8 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    let url = `${baseUrl}/profiles/show?key=${encodeURIComponent(this.props.profile)}`;
+    return <a href={url}>{this.props.children}</a>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/helpers/rating.js b/server/sonar-web/src/main/js/apps/overview/helpers/rating.js
new file mode 100644 (file)
index 0000000..e160019
--- /dev/null
@@ -0,0 +1,12 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    if (this.props.value == null || isNaN(this.props.value)) {
+      return null;
+    }
+    let formatted = window.formatMeasure(this.props.value, 'RATING');
+    let className = 'rating rating-' + formatted;
+    return <span className={className}>{formatted}</span>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/leak-coverage.js b/server/sonar-web/src/main/js/apps/overview/leak-coverage.js
new file mode 100644 (file)
index 0000000..0d50243
--- /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/leak-dups.js b/server/sonar-web/src/main/js/apps/overview/leak-dups.js
new file mode 100644 (file)
index 0000000..a34555f
--- /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/leak-issues.js b/server/sonar-web/src/main/js/apps/overview/leak-issues.js
new file mode 100644 (file)
index 0000000..61443d6
--- /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/leak-size.js b/server/sonar-web/src/main/js/apps/overview/leak-size.js
new file mode 100644 (file)
index 0000000..d7df01c
--- /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/leak.js b/server/sonar-web/src/main/js/apps/overview/leak.js
new file mode 100644 (file)
index 0000000..f629153
--- /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/main.js b/server/sonar-web/src/main/js/apps/overview/main.js
new file mode 100644 (file)
index 0000000..dc1025a
--- /dev/null
@@ -0,0 +1,102 @@
+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 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;
+
+      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 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.js b/server/sonar-web/src/main/js/apps/overview/meta.js
new file mode 100644 (file)
index 0000000..310aadd
--- /dev/null
@@ -0,0 +1,68 @@
+import _ from 'underscore';
+import React from 'react';
+import ProfileLink from './helpers/profile-link';
+import GateLink from './helpers/gate-link';
+
+export default React.createClass({
+  render() {
+    let
+        profiles = (this.props.component.profiles || []).map(profile => {
+          return (
+              <li key={profile.key}>
+                <span className="note spacer-right">({profile.language})</span>
+                <ProfileLink profile={profile.key}>{profile.name}</ProfileLink>
+              </li>
+          );
+        }),
+        links = (this.props.component.links || []).map(link => {
+          let iconClassName = `spacer-right icon-color-link icon-${link.type}`;
+          return (
+              <li key={link.type}>
+                <i className={iconClassName}/>
+                <a href={link.href} target="_blank">{link.name}</a>
+              </li>
+          );
+        });
+
+    let 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 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.js b/server/sonar-web/src/main/js/apps/overview/nutshell-coverage.js
new file mode 100644 (file)
index 0000000..e478a71
--- /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/nutshell-dups.js b/server/sonar-web/src/main/js/apps/overview/nutshell-dups.js
new file mode 100644 (file)
index 0000000..fd93f14
--- /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/nutshell-issues.js b/server/sonar-web/src/main/js/apps/overview/nutshell-issues.js
new file mode 100644 (file)
index 0000000..f9ae334
--- /dev/null
@@ -0,0 +1,68 @@
+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
new file mode 100644 (file)
index 0000000..5d41e29
--- /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/nutshell.js b/server/sonar-web/src/main/js/apps/overview/nutshell.js
new file mode 100644 (file)
index 0000000..f8c7dc3
--- /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() {
+    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>
+    );
+  }
+});
index 80e24f0fc28c8f69d7467aa3afca489d1fe2b8f1..6a57b3537f73d9ac695872f877778147001ca0ce 100644 (file)
@@ -2,5 +2,5 @@ export function getProjectUrl(project) {
   if (typeof project !== 'string') {
     throw new TypeError('Project ID or KEY should be passed');
   }
-  return `${window.baseUrl}/dashboard?id=${encodeURIComponent(project)}`;
+  return `${window.baseUrl}/overview?id=${encodeURIComponent(project)}`;
 }
index 255af4f496c389b19767ec0298f4c94099548150..5338ae64886b4e344d1400bec8300c15f7c5882a 100644 (file)
@@ -362,7 +362,7 @@ function closeModalWindow () {
    */
   function shouldDisplayAbout (days, hours, minutes) {
     var hasDays = days > 0,
-        fewDays = days < 1000,
+        fewDays = days < 5,
         hasHours = hours > 0,
         hasMinutes = minutes > 0;
     return (hasDays && fewDays && hasHours) || (!hasDays && hasHours && hasMinutes);
index b7db14235b9198dd1faab88094d2a1a61da86b5b..be59409f008e1fe1be883c93ec26c0d0456fc0e8 100644 (file)
@@ -17,7 +17,7 @@ export default React.createClass({
     return params.period ? `&period=${params.period}` : '';
   },
 
-  renderOverviewLink() {
+  renderMainDashboardLink() {
     if (_.size(this.props.component.dashboards) === 0) {
       return null;
     }
@@ -34,6 +34,11 @@ export default React.createClass({
     });
   },
 
+  renderOverviewLink() {
+    let url = `/overview?id=${encodeURIComponent(this.props.component.key)}`;
+    return this.renderLink(url, window.t('overview.page'), '/overview');
+  },
+
   renderComponentsLink() {
     const url = `/components/index?id=${encodeURIComponent(this.props.component.key)}`;
     return this.renderLink(url, window.t('components.page'), '/components');
@@ -239,6 +244,7 @@ export default React.createClass({
   render() {
     return (
         <ul className="nav navbar-nav nav-tabs">
+          {this.renderMainDashboardLink()}
           {this.renderOverviewLink()}
           {this.renderComponentsLink()}
           {this.renderComponentIssuesLink()}
index a112b884fc41e587485cc26ce079c233c509ed30..17292f6eabc30ba52c4a6aad7fa1723a22506500 100644 (file)
@@ -58,7 +58,7 @@
 .measure-value {
   color: @darkBlue;
   font-size: @bigFontSize;
-  font-weight: 300;
+  font-weight: 400;
 }
 
 .measure-bar {
@@ -95,7 +95,7 @@
   .measure-name {
     margin-top: 2px;
     font-size: 15px;
-    font-weight: 300;
+    font-weight: 400;
   }
 
   .measure-value {
index 141c2b08f31bf232cc33ed0f07a7041c7d30be08..f6e613f933e7cb13a561ab982c570e270c2113bb 100644 (file)
   top: @navbarGlobalHeight;
   z-index: @navbar-context-z-index;
   height: @navbarContextHeight;
+  padding-top: 5px;
   background-color: @navbarContextBackground;
 
   .nav-tabs {
index 6e8ebe364ef51218d8900565d014da5306e7dffa..1af0fed2673232f4a743af7c6f354dc3186270ba 100644 (file)
 
   > li:first-child {
     font-size: 18px;
-    font-weight: 300;
+    font-weight: 400;
 
     > a {
       color: @baseFontColor;
index 4eee2d9201580c0596db7d5c981ceeb7754e9f13..0dce505a7a86124467d7c33e1f691683ff45dd75 100644 (file)
@@ -9,3 +9,4 @@
 @import "pages/maintenance";
 @import "pages/login";
 @import "pages/api-documentation";
+@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..da3055a
--- /dev/null
@@ -0,0 +1,195 @@
+@import (reference) "../variables";
+@import (reference) "../mixins";
+@import (reference) "../init/type";
+
+.overview {
+  display: flex;
+  width: 100%;
+  min-height: ~"calc(100vh - @{navbarGlobalHeight} - @{navbarContextHeight} - @{pageFooterHeight})";
+}
+
+.overview-main {
+  flex: 1;
+  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-size: 15px;
+  font-weight: 400;
+  //letter-spacing: 0.03em;
+}
+
+.overview-gate-condition-value {
+  margin-top: 8px;
+  font-weight: 300;
+  font-size: 22px;
+
+  i {
+    position: relative;
+    top: -1px;
+  }
+}
+
+.overview-gate-condition-itself {
+  padding-left: 4px;
+  color: mix(@baseFontColor, @barBackgroundColor, 70%);
+  font-size: 13px;
+  font-weight: 400;
+}
+
+.overview-gate-condition-level {
+  margin-top: 8px;
+}
+
+.overview-leak {
+  padding: 50px 30px;
+  border-top: 1px solid @barBorderColor;
+  border-bottom: 1px solid @barBorderColor;
+}
+
+.overview-title {
+  font-size: 18px;
+  font-weight: 400;
+
+  & > .badge {
+    position: relative;
+    top: -2px;
+    margin-left: 15px;
+    padding: 8px 15px;
+    font-size: 16px;
+    letter-spacing: 0.04em;
+  }
+}
+
+.overview-title + .overview-cards:not(:empty) {
+  margin-top: 20px;
+}
+
+.overview-leak-period {
+  margin-left: 10px;
+  font-size: 14px;
+}
+
+.overview-nutshell {
+  padding: 50px 30px;
+}
+
+.overview-cards {
+  display: flex;
+}
+
+.overview-card {
+  flex: 1 0 25%;
+  box-sizing: border-box;
+
+  .overview-gate & {
+    flex-grow: 0;
+  }
+
+  .overview-main & {
+    font-size: 14px;
+  }
+
+  .measures-chart {
+    width: auto;
+    text-align: left;
+  }
+
+  .measures-chart-indent {
+    padding-left: 67px;
+  }
+
+  .measure-big + .measure-big {
+    margin-left: 30px;
+  }
+
+  .list-inline {
+    margin-left: -10px;
+    margin-right: -10px;
+
+    & > li {
+      padding-left: 10px;
+      padding-right: 10px;
+    }
+  }
+}
+
+.overview-measure {
+  font-size: 28px;
+}
+
+.overview-measure-label {
+  font-size: 16px;
+}
+
+.overview-meta {
+  width: 240px;
+  padding: 50px 30px;
+  border-left: 1px solid @barBorderColor;
+  box-sizing: border-box;
+  background-color: @barBackgroundColor;
+
+  .panel {
+    border: none !important;
+  }
+}
+
+.overview-meta .overview-card {
+  width: auto;
+  margin-bottom: 30px;
+}
+
+.overview-meta-description {
+  line-height: 1.5;
+}
+
+.overview-meta-header {
+  color: #797979;
+}
+
+.overview-meta-list {
+  & > li {
+    padding-bottom: 4px;
+    .text-ellipsis;
+  }
+}
index a92cdd372f270ab4b3430fb2477a683f36501c4e..b21f2353e3cca8005f0e0397ed660d198f1d4f0b 100644 (file)
  */
 
 @navbarGlobalHeight: 30px;
-@navbarContextHeight: 60px;
+@navbarContextHeight: 65px;
 @pageFooterHeight: 60px;
 
 
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..41fe871
--- /dev/null
@@ -0,0 +1,181 @@
+<%
+   links_size = @resource.project_links.size
+
+   profiles = []
+   if @snapshot
+     qprofiles_measure = @snapshot.measure(Metric::QUALITY_PROFILES)
+     if qprofiles_measure && !qprofiles_measure.data.blank?
+       profiles = JSON.parse qprofiles_measure.data
+     end
+   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
+%>
+
+<%
+   if @snapshot
+     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
+   end
+%>
+
+<% content_for :extra_script do %>
+  <script>
+    (function () {
+      var component = {
+        id: '<%= escape_javascript @resource.uuid %>',
+        key: '<%= escape_javascript @resource.key %>',
+        description: '<%= escape_javascript @resource.description %>',
+        hasSnapshot: <%= @snapshot ? true : false %>,
+        periods: [
+          <%
+            if @snapshot && @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 = {
+        <% if @snapshot %>
+
+        // issues
+        <% if @snapshot.measure('sqale_rating') %>
+        sqaleRating: '<%= @snapshot.measure('sqale_rating').value -%>',
+        <% else %>
+        sqaleRating: 'A',
+        <% end %>
+
+        // 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 -%>'
+        <% end %>
+      };
+
+      var leak = {
+        <% if @snapshot %>
+        // 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) -%>'
+        <% end %>
+      };
+
+      window.sonarqube.overview = {
+        component: component,
+        gate: gate,
+        measures: measures,
+        leak: leak
+      };
+    })();
+  </script>
+  <script src="<%= ApplicationController.root_context -%>/js/bundles/overview.js?v=<%= sonar_version -%>"></script>
+<% end %>
index ebec9a8157b2a7b17fbc0e8d9b7b3a4c309b6d72..7760e5320be69422179c1f2091912529e2c03c7a 100644 (file)
@@ -3092,3 +3092,35 @@ background_tasks.in_progress_duration=Duration of the current task in progress.
 #
 #------------------------------------------------------------------------------
 system.log_level.warning=Current level has performance impacts, please make sure to get back to INFO level once your investigation is done. Please note that when you restart the server, the level will automatically be reset to INFO.
+
+
+
+#------------------------------------------------------------------------------
+#
+# 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