]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6331 improve UX of the project overview page
authorStas Vilchik <vilchiks@gmail.com>
Wed, 28 Oct 2015 09:25:13 +0000 (10:25 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Fri, 30 Oct 2015 09:46:02 +0000 (10:46 +0100)
61 files changed:
it/it-tests/src/test/java/it/projectOverview/ProjectOverviewTest.java [new file with mode: 0644]
it/it-tests/src/test/resources/projectOverview/ProjectOverviewTest/test_project_overview_after_first_analysis.html [new file with mode: 0644]
server/sonar-web/src/main/js/api/issues.js
server/sonar-web/src/main/js/api/measures.js
server/sonar-web/src/main/js/apps/overview/app.js
server/sonar-web/src/main/js/apps/overview/coverage/main.js
server/sonar-web/src/main/js/apps/overview/duplications/main.js
server/sonar-web/src/main/js/apps/overview/formatting.js [deleted file]
server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/gate/gate.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/card.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/cards.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/components.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/coverage.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/details-link.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/details.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/duplications.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/empty.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/gate-condition.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/gate-empty.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/gate.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/issues.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/leak-dups.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/leak-issues.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/leak-size.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/leak.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/main.js
server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/nutshell.js [deleted file]
server/sonar-web/src/main/js/apps/overview/general/size.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/general/timeline.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/helpers/issues-link.js
server/sonar-web/src/main/js/apps/overview/helpers/metrics.js
server/sonar-web/src/main/js/apps/overview/helpers/period-label.js
server/sonar-web/src/main/js/apps/overview/issues/assignees.js
server/sonar-web/src/main/js/apps/overview/issues/main.js
server/sonar-web/src/main/js/apps/overview/issues/severities.js
server/sonar-web/src/main/js/apps/overview/main.js [deleted file]
server/sonar-web/src/main/js/apps/overview/meta.js
server/sonar-web/src/main/js/apps/overview/overview.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/size/main.js
server/sonar-web/src/main/js/components/charts/bar-chart.js
server/sonar-web/src/main/js/components/charts/bubble-chart.js
server/sonar-web/src/main/js/components/charts/line-chart.js
server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js [deleted file]
server/sonar-web/src/main/js/components/charts/treemap.js
server/sonar-web/src/main/js/components/charts/word-cloud.js
server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/constants.js [new file with mode: 0644]
server/sonar-web/src/main/js/libs/application.js
server/sonar-web/src/main/less/pages/overview.less
server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb
server/sonar-web/tests/apps/overview-test.js [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/it/it-tests/src/test/java/it/projectOverview/ProjectOverviewTest.java b/it/it-tests/src/test/java/it/projectOverview/ProjectOverviewTest.java
new file mode 100644 (file)
index 0000000..145d926
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package it.projectOverview;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarRunner;
+import com.sonar.orchestrator.selenium.Selenese;
+import it.Category1Suite;
+import org.junit.ClassRule;
+import org.junit.Test;
+import util.selenium.SeleneseTest;
+
+import static util.ItUtils.projectDir;
+
+public class ProjectOverviewTest {
+
+  @ClassRule
+  public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+  @Test
+  public void test_project_overview_after_first_analysis() throws Exception {
+    executeBuild("project-for-overview", "Project For Overview");
+
+    Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_project_overview_after_first_analysis",
+      "/projectOverview/ProjectOverviewTest/test_project_overview_after_first_analysis.html"
+    ).build();
+    new SeleneseTest(selenese).runOn(orchestrator);
+  }
+
+  private void executeBuild(String projectKey, String projectName) {
+    orchestrator.executeBuild(
+      SonarRunner.create(projectDir("shared/xoo-sample"))
+        .setProjectKey(projectKey)
+        .setProjectName(projectName)
+    );
+  }
+
+}
diff --git a/it/it-tests/src/test/resources/projectOverview/ProjectOverviewTest/test_project_overview_after_first_analysis.html b/it/it-tests/src/test/resources/projectOverview/ProjectOverviewTest/test_project_overview_after_first_analysis.html
new file mode 100644 (file)
index 0000000..2b218e5
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<link rel="selenium.base" href="http://localhost:49506" />
+<title>test_project_overview_after_first_analysis</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">test_project_overview_after_first_analysis</td></tr>
+</thead><tbody>
+<tr>
+       <td>open</td>
+       <td>/sonar/overview?id=project-for-overview</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForTextPresent</td>
+       <td>*Quality Gate*Passed*</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForTextPresent</td>
+       <td>*A*0*Issues*0*Debt*</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForTextPresent</td>
+       <td>*Blocker*0*Critical*0*Major*0*Minor*0*Info*0*</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForTextPresent</td>
+       <td>*0.0%*Duplications*0*Duplicated Blocks*</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForTextPresent</td>
+       <td>*13*Lines of Code*1*Files*</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertTextNotPresent</td>
+       <td>*Coverage*</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForTextPresent</td>
+       <td>*Quality Gate*SonarQube way*</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForTextPresent</td>
+       <td>*Quality Profiles*Xoo*Basic*</td>
+       <td></td>
+</tr>
+
+</tbody></table>
+</body>
+</html>
index 5d2259e6855fc13a572c1b520f89e7b3d4adbe66..76a54463b7dca05770df9937506ce6c249c69dc4 100644 (file)
@@ -1,7 +1,9 @@
 import _ from 'underscore';
+
 import { getJSON } from '../helpers/request.js';
 
-function getFacet (query, facet) {
+
+export function getFacet (query, facet) {
   let url = baseUrl + '/api/issues/search';
   let data = _.extend({}, query, { facets: facet, ps: 1, additionalFields: '_all' });
   return getJSON(url, data).then(r => {
@@ -9,14 +11,17 @@ function getFacet (query, facet) {
   });
 }
 
+
 export function getSeverities (query) {
   return getFacet(query, 'severities').then(r => r.facet);
 }
 
+
 export function getTags (query) {
   return getFacet(query, 'tags').then(r => r.facet);
 }
 
+
 export function getAssignees (query) {
   return getFacet(query, 'assignees').then(r => {
     return r.facet.map(item => {
@@ -25,3 +30,12 @@ export function getAssignees (query) {
     });
   });
 }
+
+
+export function getIssuesCount (query) {
+  let url = baseUrl + '/api/issues/search';
+  let data = _.extend({}, query, { ps: 1, facetMode: 'debt' });
+  return getJSON(url, data).then(r => {
+    return { issues: r.total, debt: r.debtTotal };
+  });
+}
index e416e3a8882854f16957f1e6a8a7039c06206fb4..0916c4998c1f6cb44da7e360544618cddf5977f4 100644 (file)
@@ -1,5 +1,6 @@
 import { getJSON } from '../helpers/request.js';
 
+
 export function getMeasures (componentKey, metrics) {
   let url = baseUrl + '/api/resources/index';
   let data = { resource: componentKey, metrics: metrics.join(',') };
@@ -12,3 +13,22 @@ export function getMeasures (componentKey, metrics) {
     return measures;
   });
 }
+
+
+export function getMeasuresAndVariations (componentKey, metrics) {
+  let url = baseUrl + '/api/resources/index';
+  let data = { resource: componentKey, metrics: metrics.join(','), includetrends: 'true' };
+  return getJSON(url, data).then(r => {
+    let msr = r[0].msr || [];
+    let measures = {};
+    msr.forEach(measure => {
+      measures[measure.key] = {
+        value: measure.val != null ? measure.val : measure.data,
+        var1: measure.var1,
+        var2: measure.var2,
+        var3: measure.var3
+      };
+    });
+    return measures;
+  });
+}
index f2912244670fa70fa84b70d1bd0306f3d4e27b8e..418784e1405239a57a05f1d2cac78c7341bdcab2 100644 (file)
@@ -2,15 +2,26 @@ import $ from 'jquery';
 import _ from 'underscore';
 import React from 'react';
 import ReactDOM from 'react-dom';
-import { Overview } from './main';
+
+import { Overview, EmptyOverview } from './overview';
+
+
+const LEAK_PERIOD = '1';
+
 
 class App {
   start (options) {
     let opts = _.extend({}, options, window.sonarqube.overview);
     _.extend(opts.component, options.component);
+
     $('html').toggleClass('dashboard-page', opts.component.hasSnapshot);
     let el = document.querySelector(opts.el);
-    ReactDOM.render(<Overview {...opts}/>, el);
+
+    if (opts.component.hasSnapshot) {
+      ReactDOM.render(<Overview {...opts} leakPeriodIndex={LEAK_PERIOD}/>, el);
+    } else {
+      ReactDOM.render(<EmptyOverview/>, el);
+    }
   }
 }
 
index 084a7f94ea0736677ed8f24a8b1280cb31a4095c..a4e8f90fa3728fb14ffd15721ddba192f4aab317 100644 (file)
@@ -9,7 +9,14 @@ import { CoverageTreemap } from './treemap';
 
 export default class extends React.Component {
   render () {
-    return <div className="overview-domain">
+    return <div className="overview-detailed-page">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Coverage & Tests</h2>
+      </div>
+
+      <a className="overview-detailed-page-back" href="#">
+        <i className="icon-chevron-left"/>
+      </a>
 
       <CoverageTimeline {...this.props}/>
 
index a029f6ebb328b0c3be9f1c11bb8e5cd5e003ab27..02aa9eceecc350c81d3ab773dfa563a5be7ddbd1 100644 (file)
@@ -8,7 +8,15 @@ import { DuplicationsTreemap } from './treemap';
 
 export default class extends React.Component {
   render () {
-    return <div className="overview-domain">
+    return <div className="overview-detailed-page">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Duplications</h2>
+      </div>
+
+      <a className="overview-detailed-page-back" href="#">
+        <i className="icon-chevron-left"/>
+      </a>
+
       <DuplicationsTimeline {...this.props}/>
       <div className="flex-columns">
         <div className="flex-column flex-column-half">
diff --git a/server/sonar-web/src/main/js/apps/overview/formatting.js b/server/sonar-web/src/main/js/apps/overview/formatting.js
deleted file mode 100644 (file)
index 7f05531..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-const METRIC_TYPES = {
-  'violations': 'SHORT_INT',
-  'blocker_violations': 'SHORT_INT',
-  'critical_violations': 'SHORT_INT',
-  'major_violations': 'SHORT_INT',
-  'minor_violations': 'SHORT_INT',
-  'info_violations': 'SHORT_INT',
-  'confirmed_issues': 'SHORT_INT',
-  'false_positive_issues': 'SHORT_INT',
-  'open_issues': 'SHORT_INT',
-  'reopened_issues': 'SHORT_INT',
-  'sqale_index': 'SHORT_WORK_DUR',
-  'sqale_debt_ratio': 'PERCENT',
-  'sqale_rating': 'RATING',
-
-  'coverage': 'PERCENT',
-  'line_coverage': 'PERCENT',
-  'branch_coverage': 'PERCENT',
-  'lines_to_cover': 'SHORT_INT',
-  'conditions_to_cover': 'SHORT_INT',
-  'uncovered_lines': 'SHORT_INT',
-  'uncovered_conditions': 'SHORT_INT',
-
-  'it_coverage': 'PERCENT',
-  'it_line_coverage': 'PERCENT',
-  'it_branch_coverage': 'PERCENT',
-  'it_lines_to_cover': 'SHORT_INT',
-  'it_conditions_to_cover': 'SHORT_INT',
-  'it_uncovered_lines': 'SHORT_INT',
-  'it_uncovered_conditions': 'SHORT_INT',
-
-  'overall_coverage': 'PERCENT',
-  'overall_line_coverage': 'PERCENT',
-  'overall_branch_coverage': 'PERCENT',
-  'overall_lines_to_cover': 'SHORT_INT',
-  'overall_conditions_to_cover': 'SHORT_INT',
-  'overall_uncovered_lines': 'SHORT_INT',
-  'overall_uncovered_conditions': 'SHORT_INT',
-
-  'tests': 'SHORT_INT',
-  'skipped_tests': 'SHORT_INT',
-  'test_errors': 'SHORT_INT',
-  'test_failures': 'SHORT_INT',
-  'test_execution_time': 'MILLISEC',
-  'test_success_density': 'PERCENT',
-
-  'duplicated_blocks': 'INT',
-  'duplicated_files': 'INT',
-  'duplicated_lines': 'INT',
-  'duplicated_lines_density': 'PERCENT',
-
-  'ncloc': 'SHORT_INT',
-  'classes': 'SHORT_INT',
-  'lines': 'SHORT_INT',
-  'generated_ncloc': 'SHORT_INT',
-  'generated_lines': 'SHORT_INT',
-  'directories': 'SHORT_INT',
-  'files': 'SHORT_INT',
-  'functions': 'SHORT_INT',
-  'statements': 'SHORT_INT',
-  'public_api': 'SHORT_INT',
-
-  'complexity': 'SHORT_INT',
-  'class_complexity': 'SHORT_INT',
-  'file_complexity': 'SHORT_INT',
-  'function_complexity': 'SHORT_INT',
-
-  'comment_lines_density': 'PERCENT',
-  'comment_lines': 'SHORT_INT',
-  'commented_out_code_lines': 'SHORT_INT',
-  'public_documented_api_density': 'PERCENT',
-  'public_undocumented_api': 'SHORT_INT'
-};
-
-export function formatMeasure (value, metric) {
-  let type = METRIC_TYPES[metric];
-  return type ? window.formatMeasure(value, type) : value;
-}
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-condition.js
new file mode 100644 (file)
index 0000000..5699a79
--- /dev/null
@@ -0,0 +1,41 @@
+import React from 'react';
+
+import Measure from './../helpers/measure';
+import { getPeriodLabel, getPeriodDate } from './../helpers/period-label';
+import DrilldownLink from './../helpers/drilldown-link';
+
+
+export default React.createClass({
+  render() {
+    let metricName = window.t('metric', this.props.condition.metric.name, 'name'),
+        threshold = this.props.condition.level === 'ERROR' ?
+                    this.props.condition.error : this.props.condition.warning,
+        period = this.props.condition.period ?
+                 getPeriodLabel(this.props.component.periods, this.props.condition.period) : null,
+        periodDate = getPeriodDate(this.props.component.periods, this.props.condition.period);
+
+    let classes = 'alert_' + this.props.condition.level.toUpperCase();
+
+    return (
+        <li className="overview-gate-condition">
+          <div className="little-spacer-bottom">{period}</div>
+
+          <div style={{ display: 'flex', alignItems: 'center' }}>
+            <div className="overview-gate-condition-value">
+              <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name}
+                             period={this.props.condition.period} periodDate={periodDate}>
+              <span className={classes}>
+                <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/>
+              </span>
+              </DrilldownLink>&nbsp;
+            </div>
+
+            <div className="overview-gate-condition-metric">
+              <div>{metricName}</div>
+              <div>{window.t('quality_gates.operator', this.props.condition.op, 'short')} <Measure value={threshold} type={this.props.condition.metric.type}/></div>
+            </div>
+          </div>
+        </li>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-conditions.js
new file mode 100644 (file)
index 0000000..adefecb
--- /dev/null
@@ -0,0 +1,16 @@
+import React from 'react';
+import GateCondition from './gate-condition';
+
+export default React.createClass({
+  propTypes: {
+    gate: React.PropTypes.object.isRequired,
+    component: React.PropTypes.object.isRequired
+  },
+
+  render() {
+    let conditions = this.props.gate.conditions
+        .filter(c => c.level !== 'OK')
+        .map(c => <GateCondition key={c.metric.name} condition={c} component={this.props.component}/>);
+    return <ul className="overview-gate-conditions-list">{conditions}</ul>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/gate/gate-empty.js
new file mode 100644 (file)
index 0000000..6134718
--- /dev/null
@@ -0,0 +1,15 @@
+import React from 'react';
+
+export default React.createClass({
+  render() {
+    let qualityGatesUrl = window.baseUrl + '/quality_gates';
+
+    return (
+        <div className="overview-gate">
+          <h2 className="overview-title">{window.t('overview.quality_gate')}</h2>
+          <p className="overview-gate-warning">
+            You should <a href={qualityGatesUrl}>define</a> a quality gate on this project.</p>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/gate/gate.js b/server/sonar-web/src/main/js/apps/overview/gate/gate.js
new file mode 100644 (file)
index 0000000..076cbcd
--- /dev/null
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import GateConditions from './gate-conditions';
+import GateEmpty from './gate-empty';
+
+
+export default React.createClass({
+  render() {
+    if (!this.props.gate || !this.props.gate.level) {
+      return this.props.component.qualifier === 'TRK' ? <GateEmpty/> : null;
+    }
+
+    let level = this.props.gate.level.toLowerCase(),
+        badgeClassName = 'badge badge-' + level,
+        badgeText = window.t('overview.gate', this.props.gate.level);
+
+    return (
+        <div className="overview-gate">
+          <h2 className="overview-title">
+            {window.t('overview.quality_gate')}
+            <span className={badgeClassName}>{badgeText}</span>
+          </h2>
+          <GateConditions gate={this.props.gate} component={this.props.component}/>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/card.js b/server/sonar-web/src/main/js/apps/overview/general/card.js
deleted file mode 100644 (file)
index 4d5eb06..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import React from 'react';
-
-
-export default React.createClass({
-  render() {
-    return <li className="overview-card">{this.props.children}</li>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/cards.js b/server/sonar-web/src/main/js/apps/overview/general/cards.js
deleted file mode 100644 (file)
index 3d69cf8..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  render() {
-    return <ul className="overview-cards">{this.props.children}</ul>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/components.js b/server/sonar-web/src/main/js/apps/overview/general/components.js
new file mode 100644 (file)
index 0000000..441f6d1
--- /dev/null
@@ -0,0 +1,126 @@
+import moment from 'moment';
+import React from 'react';
+
+import { Timeline } from './timeline';
+
+
+export const Domain = React.createClass({
+  render () {
+    return <div className="overview-domain">{this.props.children}</div>;
+  }
+});
+
+
+export const DomainTitle = React.createClass({
+  render () {
+    return <div className="overview-title">{this.props.children}</div>;
+  }
+});
+
+
+export const DomainLeakTitle = React.createClass({
+  render() {
+    if (!this.props.label || !this.props.date) {
+      return null;
+    }
+    let momentDate = moment(this.props.date);
+    let fromNow = momentDate.fromNow();
+    let tooltip = 'Started ' + fromNow + ', ' + momentDate.format('LLL');
+    return <span title={tooltip} data-toggle="tooltip">Water Leak: {this.props.label}</span>;
+  }
+});
+
+
+export const DomainHeader = React.createClass({
+  render () {
+    return <div className="overview-domain-header">
+      <DomainTitle>{this.props.title}</DomainTitle>
+      <DomainLeakTitle label={this.props.leakPeriodLabel} date={this.props.leakPeriodDate}/>
+    </div>;
+  }
+});
+
+
+export const DomainPanel = React.createClass({
+  propTypes: {
+    domain: React.PropTypes.string
+  },
+
+  render () {
+    return <div className="overview-domain-panel">
+      {this.props.children}
+    </div>;
+  }
+});
+
+
+export const DomainNutshell = React.createClass({
+  render () {
+    return <div className="overview-domain-nutshell">{this.props.children}</div>;
+  }
+});
+
+export const DomainLeak = React.createClass({
+  render () {
+    return <div className="overview-domain-leak">{this.props.children}</div>;
+  }
+});
+
+
+export const MeasuresList = React.createClass({
+  render () {
+    return <div className="overview-domain-measures">{this.props.children}</div>;
+  }
+});
+
+
+export const Measure = React.createClass({
+  propTypes: {
+    label: React.PropTypes.string,
+    composite: React.PropTypes.bool
+  },
+
+  getDefaultProps() {
+    return { composite: false };
+  },
+
+  renderValue () {
+    if (this.props.composite) {
+      return this.props.children;
+    } else {
+      return <div className="overview-domain-measure-value">
+        {this.props.children}
+      </div>;
+    }
+  },
+
+  renderLabel() {
+    return this.props.label ?
+        <div className="overview-domain-measure-label">{this.props.label}</div> : null;
+  },
+
+  render () {
+    return <div className="overview-domain-measure">
+      {this.renderValue()}
+      {this.renderLabel()}
+    </div>;
+  }
+});
+
+
+export const DomainMixin = {
+  renderTimeline(range) {
+    if (!this.props.history) {
+      return null;
+    }
+    let props = { history: this.props.history };
+    props[range] = this.props.leakPeriodDate;
+    return <div className="overview-domain-timeline">
+      <Timeline {...props}/>
+    </div>;
+  },
+
+  hasLeakPeriod () {
+    return this.props.leakPeriodDate != null;
+  }
+};
diff --git a/server/sonar-web/src/main/js/apps/overview/general/coverage.js b/server/sonar-web/src/main/js/apps/overview/general/coverage.js
new file mode 100644 (file)
index 0000000..bec2f46
--- /dev/null
@@ -0,0 +1,64 @@
+import React from 'react';
+
+import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
+import DrilldownLink from '../helpers/drilldown-link';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { getMetricName } from '../helpers/metrics';
+
+
+export const GeneralCoverage = React.createClass({
+  mixins: [TooltipsMixin, DomainMixin],
+
+  propTypes: {
+    measures: React.PropTypes.object.isRequired,
+    leakPeriodLabel: React.PropTypes.string,
+    leakPeriodDate: React.PropTypes.object
+  },
+
+  renderLeak () {
+    if (!this.hasLeakPeriod()) {
+      return null;
+    }
+
+    return <DomainLeak>
+      <MeasuresList>
+        <Measure label={getMetricName('new_coverage')}>
+          <DrilldownLink component={this.props.component.key} metric="new_overall_coverage" period="1">
+            {window.formatMeasure(this.props.leak['new_overall_coverage'], 'PERCENT')}
+          </DrilldownLink>
+        </Measure>
+      </MeasuresList>
+      {this.renderTimeline('after')}
+    </DomainLeak>;
+  },
+
+  render () {
+    if (this.props.measures['overall_coverage'] == null) {
+      return null;
+    }
+
+    return <Domain>
+      <DomainHeader title="Tests"
+                    leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
+
+      <DomainPanel domain="coverage">
+        <DomainNutshell>
+          <MeasuresList>
+            <Measure label={getMetricName('coverage')}>
+              <DrilldownLink component={this.props.component.key} metric="overall_coverage">
+                {window.formatMeasure(this.props.measures['overall_coverage'], 'PERCENT')}
+              </DrilldownLink>
+            </Measure>
+            <Measure label={getMetricName('tests')}>
+              <DrilldownLink component={this.props.component.key} metric="tests">
+                {window.formatMeasure(this.props.measures['tests'], 'SHORT_INT')}
+              </DrilldownLink>
+            </Measure>
+          </MeasuresList>
+          {this.renderTimeline('before')}
+        </DomainNutshell>
+        {this.renderLeak()}
+      </DomainPanel>
+    </Domain>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/details-link.js b/server/sonar-web/src/main/js/apps/overview/general/details-link.js
deleted file mode 100644 (file)
index dca106d..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-import classNames from 'classnames';
-
-export default React.createClass({
-  handleClick(e) {
-    e.preventDefault();
-    this.props.onRoute(this.props.linkTo);
-  },
-
-  render() {
-    let classes = classNames('overview-card', 'overview-card-section', {
-      'active': this.props.active
-    });
-    return <li className={classes}>
-      <a onClick={this.handleClick}>{window.t('overview.domain', this.props.linkTo)}</a>
-    </li>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/details.js b/server/sonar-web/src/main/js/apps/overview/general/details.js
deleted file mode 100644 (file)
index f5ee24b..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import Cards from './cards';
-import DetailsLink from './details-link';
-
-
-function checkMeasureForDomain (domain, measures) {
-  if (domain === 'coverage' && measures.coverage == null) {
-    return false;
-  }
-  if (domain === 'duplications' && measures.duplications == null) {
-    return false;
-  }
-  return true;
-}
-
-
-export default React.createClass({
-  render() {
-    let domains = ['issues', 'coverage', 'duplications', 'size'].map(domain => {
-      if (!checkMeasureForDomain(domain, this.props.measures)) {
-        return null;
-      }
-      let active = domain === this.props.section;
-      return <DetailsLink key={domain} linkTo={domain} onRoute={this.props.onRoute} active={active}/>;
-    });
-
-    return (
-        <div className="overview-more">
-          <h2 className="overview-title">More Details</h2>
-          <Cards>{domains}</Cards>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/duplications.js b/server/sonar-web/src/main/js/apps/overview/general/duplications.js
new file mode 100644 (file)
index 0000000..d316eea
--- /dev/null
@@ -0,0 +1,56 @@
+import React from 'react';
+
+import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
+import DrilldownLink from '../helpers/drilldown-link';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { getMetricName } from '../helpers/metrics';
+
+
+export const GeneralDuplications = React.createClass({
+  mixins: [TooltipsMixin, DomainMixin],
+
+  propTypes: {
+    leakPeriodLabel: React.PropTypes.string,
+    leakPeriodDate: React.PropTypes.object
+  },
+
+  renderLeak () {
+    if (!this.hasLeakPeriod()) {
+      return null;
+    }
+    return <DomainLeak>
+      <MeasuresList>
+        <Measure label={getMetricName('duplications')}>
+          {window.formatMeasureVariation(this.props.leak['duplicated_lines_density'], 'PERCENT')}
+        </Measure>
+      </MeasuresList>
+      {this.renderTimeline('after')}
+    </DomainLeak>;
+  },
+
+  render () {
+    return <Domain>
+      <DomainHeader title="Duplications"
+                    leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
+
+      <DomainPanel domain="duplications">
+        <DomainNutshell>
+          <MeasuresList>
+            <Measure label={getMetricName('duplications')}>
+              <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density">
+                {window.formatMeasure(this.props.measures['duplicated_lines_density'], 'PERCENT')}
+              </DrilldownLink>
+            </Measure>
+            <Measure label={getMetricName('duplicated_blocks')}>
+              <DrilldownLink component={this.props.component.key} metric="duplicated_blocks">
+                {window.formatMeasure(this.props.measures['duplicated_blocks'], 'SHORT_INT')}
+              </DrilldownLink>
+            </Measure>
+          </MeasuresList>
+          {this.renderTimeline('before')}
+        </DomainNutshell>
+        {this.renderLeak()}
+      </DomainPanel>
+    </Domain>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/empty.js b/server/sonar-web/src/main/js/apps/overview/general/empty.js
deleted file mode 100644 (file)
index 78c5320..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  render() {
-    return (
-        <div className="panel">
-          <div className="alert alert-warning">
-            {window.t('provisioning.no_analysis')}
-          </div>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate-condition.js b/server/sonar-web/src/main/js/apps/overview/general/gate-condition.js
deleted file mode 100644 (file)
index 3272797..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react';
-
-import Measure from './../helpers/measure';
-import { periodLabel, getPeriodDate } from './../helpers/period-label';
-import DrilldownLink from './../helpers/drilldown-link';
-
-
-export default React.createClass({
-  render() {
-    let metricName = window.t('metric', this.props.condition.metric.name, 'name'),
-        threshold = this.props.condition.level === 'ERROR' ?
-                    this.props.condition.error : this.props.condition.warning,
-        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);
-
-    let classes = 'alert_' + this.props.condition.level.toUpperCase();
-
-    return (
-        <div>
-          <h4 className="overview-gate-condition-metric">{metricName}<br/><span className="nowrap">{period}</span></h4>
-          <div className="overview-gate-condition-value">
-            <DrilldownLink component={this.props.component.key} metric={this.props.condition.metric.name}
-                           period={this.props.condition.period} periodDate={periodDate}>
-              <span className={classes}>
-                <Measure value={this.props.condition.actual} type={this.props.condition.metric.type}/>
-              </span>
-            </DrilldownLink>&nbsp;
-            <span className="overview-gate-condition-itself">
-              {window.t('quality_gates.operator', this.props.condition.op, 'short')}&nbsp;
-              <Measure value={threshold} type={this.props.condition.metric.type}/>
-            </span>
-          </div>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js b/server/sonar-web/src/main/js/apps/overview/general/gate-conditions.js
deleted file mode 100644 (file)
index 65b92e4..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import Cards from './cards';
-import Card from './card';
-import GateCondition from './gate-condition';
-
-export default React.createClass({
-  render() {
-    let conditions = this.props.gate.conditions
-        .filter((c) => {
-          return c.level !== 'OK';
-        })
-        .map((c) => {
-          return (
-              <Card key={c.metric.name}>
-                <GateCondition condition={c} component={this.props.component}/>
-              </Card>
-          );
-        });
-    return <Cards>{conditions}</Cards>;
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate-empty.js b/server/sonar-web/src/main/js/apps/overview/general/gate-empty.js
deleted file mode 100644 (file)
index fb74bfa..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  render() {
-    let qualityGatesUrl = window.baseUrl + '/quality_gates';
-
-    return (
-        <div className="overview-gate">
-          <h2 className="overview-title">{window.t('overview.quality_gate')}</h2>
-          <p className="overview-paragraph big-spacer-top">
-            You should <a href={qualityGatesUrl}>define</a> a quality gate on this project.</p>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/gate.js b/server/sonar-web/src/main/js/apps/overview/general/gate.js
deleted file mode 100644 (file)
index 2436d2d..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react';
-import GateConditions from './gate-conditions';
-import GateEmpty from './gate-empty';
-
-export default React.createClass({
-  render() {
-    if (!this.props.gate || !this.props.gate.level) {
-      return this.props.component.qualifier === 'TRK' ? <GateEmpty/> : null;
-    }
-
-    let
-        badgeClassName = 'badge badge-' + this.props.gate.level.toLowerCase(),
-        badgeText = window.t('overview.gate', this.props.gate.level);
-
-    return (
-        <div className="overview-gate">
-          <h2 className="overview-title">
-            {window.t('overview.quality_gate')}
-            <span className={badgeClassName}>{badgeText}</span>
-          </h2>
-          <GateConditions gate={this.props.gate} component={this.props.component}/>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/issues.js b/server/sonar-web/src/main/js/apps/overview/general/issues.js
new file mode 100644 (file)
index 0000000..b97b0db
--- /dev/null
@@ -0,0 +1,129 @@
+import moment from 'moment';
+import React from 'react';
+
+import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
+import Rating from './../helpers/rating';
+import IssuesLink from '../helpers/issues-link';
+import DrilldownLink from '../helpers/drilldown-link';
+import SeverityHelper from '../../../components/shared/severity-helper';
+import SeverityIcon from '../../../components/shared/severity-icon';
+import StatusIcon from '../../../components/shared/status-icon';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { getMetricName } from '../helpers/metrics';
+import { SEVERITIES } from '../../../helpers/constants';
+
+
+export const GeneralIssues = React.createClass({
+  mixins: [TooltipsMixin, DomainMixin],
+
+  propTypes: {
+    leakPeriodLabel: React.PropTypes.string,
+    leakPeriodDate: React.PropTypes.object
+  },
+
+  renderSeverities() {
+    let severities = SEVERITIES.map((s, index) => {
+      let measure = this.props.measures.issuesSeverities[index];
+      return <tr key={s}>
+        <td>
+          <SeverityHelper severity={s}/>
+        </td>
+        <td className="thin nowrap text-right">
+          <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: s }}>
+            {window.formatMeasure(measure, 'SHORT_INT')}
+          </IssuesLink>
+        </td>
+      </tr>;
+    });
+
+    return <div style={{ width: 120 }}>
+      <table className="data">
+        <tbody>{severities}</tbody>
+      </table>
+    </div>;
+  },
+
+  renderLeak () {
+    if (!this.hasLeakPeriod()) {
+      return null;
+    }
+
+    let createdAfter = moment(this.props.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+    return <DomainLeak>
+      <MeasuresList>
+        <Measure label={getMetricName('new_issues')}>
+          <IssuesLink component={this.props.component.key}
+                      params={{ resolved: 'false', createdAfter: createdAfter }}>
+            {window.formatMeasureVariation(this.props.leak.issues, 'SHORT_INT')}
+          </IssuesLink>
+        </Measure>
+        <Measure label={getMetricName('new_debt')}>
+          <IssuesLink component={this.props.component.key}
+                      params={{ resolved: 'false', createdAfter: createdAfter, facetMode: 'debt' }}>
+            {window.formatMeasureVariation(this.props.leak.debt, 'SHORT_WORK_DUR')}
+          </IssuesLink>
+        </Measure>
+      </MeasuresList>
+      <MeasuresList>
+        <Measure label={getMetricName('new_blocker_issues')}>
+          <span className="spacer-right"><SeverityIcon severity="BLOCKER"/></span>
+          <IssuesLink component={this.props.component.key}
+                      params={{ resolved: 'false', severities: 'BLOCKER', createdAfter: createdAfter }}>
+            {window.formatMeasureVariation(this.props.leak.issuesSeverities[0], 'SHORT_INT')}
+          </IssuesLink>
+        </Measure>
+        <Measure label={getMetricName('new_critical_issues')}>
+          <span className="spacer-right"><SeverityIcon severity="CRITICAL"/></span>
+          <IssuesLink component={this.props.component.key}
+                      params={{ resolved: 'false', severities: 'CRITICAL', createdAfter: createdAfter }}>
+            {window.formatMeasureVariation(this.props.leak.issuesSeverities[1], 'SHORT_INT')}
+          </IssuesLink>
+        </Measure>
+        <Measure label={getMetricName('new_open_issues')}>
+          <span className="spacer-right"><StatusIcon status="OPEN"/></span>
+          <IssuesLink component={this.props.component.key}
+                      params={{ resolved: 'false', statuses: 'OPEN,REOPENED', createdAfter: createdAfter }}>
+            {window.formatMeasureVariation(this.props.leak.issuesStatuses[0] + this.props.leak.issuesStatuses[1],
+                'SHORT_INT')}
+          </IssuesLink>
+        </Measure>
+      </MeasuresList>
+      {this.renderTimeline('after')}
+    </DomainLeak>;
+  },
+
+  render () {
+    return <Domain>
+      <DomainHeader title="Technical Debt"
+                    leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
+
+      <DomainPanel domain="issues">
+        <DomainNutshell>
+          <MeasuresList>
+            <Measure>
+              <DrilldownLink component={this.props.component.key} metric="sqale_rating">
+                <Rating value={this.props.measures['sqale_rating']}/>
+              </DrilldownLink>
+            </Measure>
+            <Measure label={getMetricName('issues')}>
+              <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}>
+                {window.formatMeasure(this.props.measures.issues, 'SHORT_INT')}
+              </IssuesLink>
+            </Measure>
+            <Measure label={getMetricName('debt')}>
+              <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}>
+                {window.formatMeasure(this.props.measures.debt, 'SHORT_WORK_DUR')}
+              </IssuesLink>
+            </Measure>
+            <Measure composite={true}>
+              {this.renderSeverities()}
+            </Measure>
+          </MeasuresList>
+          {this.renderTimeline('before')}
+        </DomainNutshell>
+        {this.renderLeak()}
+      </DomainPanel>
+    </Domain>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js b/server/sonar-web/src/main/js/apps/overview/general/leak-coverage.js
deleted file mode 100644 (file)
index 23e3fab..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react';
-import Card from './card';
-import Measure from './../helpers/measure';
-import MeasureVariation from './../helpers/measure-variation';
-import DrilldownLink from './../helpers/drilldown-link';
-import Donut from './../helpers/donut';
-
-export default React.createClass({
-  render() {
-    let
-        newCoverage = parseInt(this.props.leak.newCoverage, 10),
-        tests = this.props.leak.tests,
-        donutData = [
-          { value: newCoverage, fill: '#85bb43' },
-          { value: 100 - newCoverage, fill: '#d4333f' }
-        ];
-
-    if (newCoverage == null || isNaN(newCoverage)) {
-      return null;
-    }
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measures-chart">
-              <Donut data={donutData} size="47"/>
-            </div>
-            <div className="measure measure-big" data-metric="new_coverage">
-              <span className="measure-name">{window.t('overview.metric.new_coverage')}</span>
-              <span className="measure-value">
-                <DrilldownLink component={this.props.component.key} metric="new_coverage" period="1">
-                  <Measure value={newCoverage} type="PERCENT"/>
-                </DrilldownLink>
-              </span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top measures-chart-indent">
-            <li>
-              <span><MeasureVariation value={tests} type="SHORT_INT"/></span>&nbsp;
-              <span>{window.t('overview.metric.tests')}</span>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-dups.js b/server/sonar-web/src/main/js/apps/overview/general/leak-dups.js
deleted file mode 100644 (file)
index f59e996..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import Card from './card';
-import MeasureVariation from './../helpers/measure-variation';
-import Donut from './../helpers/donut';
-
-export default React.createClass({
-  render() {
-    let
-        density = this.props.leak.duplications,
-        lines = this.props.leak.duplicatedLines,
-        donutData = [
-          { value: density, fill: '#f3ca8e' },
-          { value: 100 - density, fill: '#e6e6e6' }
-        ];
-
-    if (density == null) {
-      return null;
-    }
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measures-chart">
-              <Donut data={donutData} size="47"/>
-            </div>
-            <div className="measure measure-big" data-metric="duplicated_lines_density">
-              <span className="measure-name">{window.t('overview.metric.duplications')}</span>
-              <span className="measure-value">
-                <MeasureVariation value={density} type="PERCENT"/>
-              </span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top measures-chart-indent">
-            <li>
-              <span><MeasureVariation value={lines} type="SHORT_INT"/></span>&nbsp;
-              <span>{window.t('overview.metric.duplicated_lines')}</span>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-issues.js b/server/sonar-web/src/main/js/apps/overview/general/leak-issues.js
deleted file mode 100644 (file)
index 2957450..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-import moment from 'moment';
-import React from 'react';
-import Card from './card';
-import Measure from './../helpers/measure';
-import MeasureVariation from './../helpers/measure-variation';
-import IssuesLink from './../helpers/issues-link';
-import SeverityIcon from '../../../components/shared/severity-icon';
-import StatusIcon from '../../../components/shared/status-icon';
-import {getPeriodDate} from './../helpers/period-label';
-
-export default React.createClass({
-  render() {
-    let
-        newDebt = this.props.leak.newDebt,
-        issues = this.props.leak.newIssues,
-        blockerIssues = this.props.leak.newBlockerIssues,
-        criticalIssues = this.props.leak.newCriticalIssues,
-        issuesToReview = this.props.leak.newOpenIssues + this.props.leak.newReopenedIssues,
-        periodDate = moment(getPeriodDate(this.props.component.periods, '1')).format('YYYY-MM-DDTHH:mm:ssZZ');
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measure measure-big" data-metric="sqale_index">
-              <span className="measure-name">{window.t('overview.metric.new_debt')}</span>
-              <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>
-            </div>
-            <div className="measure measure-big" data-metric="violations">
-              <span className="measure-name">{window.t('overview.metric.new_issues')}</span>
-              <span className="measure-value">
-                <IssuesLink component={this.props.component.key}
-                            params={{ resolved: 'false', createdAfter: periodDate }}>
-                  <Measure value={issues} type="SHORT_INT"/>
-                </IssuesLink>
-              </span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top">
-            <li>
-              <span><SeverityIcon severity="BLOCKER"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key}
-                          params={{ resolved: 'false', createdAfter: periodDate, severities: 'BLOCKER' }}>
-                <MeasureVariation value={blockerIssues} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-            <li>
-              <span><SeverityIcon severity="CRITICAL"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key}
-                          params={{ resolved: 'false', createdAfter: periodDate, severities: 'CRITICAL' }}>
-                <MeasureVariation value={criticalIssues} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-            <li>
-              <span><StatusIcon status="OPEN"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key}
-                          params={{ resolved: 'false', createdAfter: periodDate, statuses: 'OPEN,REOPENED' }}>
-                <MeasureVariation value={issuesToReview} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak-size.js b/server/sonar-web/src/main/js/apps/overview/general/leak-size.js
deleted file mode 100644 (file)
index 5b09ddf..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import Card from './card';
-import MeasureVariation from './../helpers/measure-variation';
-
-export default React.createClass({
-  render() {
-    let
-        lines = this.props.leak.lines,
-        files = this.props.leak.files;
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measure measure-big" data-metric="lines">
-              <span className="measure-name">{window.t('overview.metric.lines')}</span>
-              <span className="measure-value">
-                <MeasureVariation value={lines} type="SHORT_INT"/>
-              </span>
-            </div>
-            <div className="measure measure-big" data-metric="files">
-              <span className="measure-name">{window.t('overview.metric.files')}</span>
-              <span className="measure-value">
-                <MeasureVariation value={files} type="SHORT_INT"/>
-              </span>
-            </div>
-          </div>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/leak.js b/server/sonar-web/src/main/js/apps/overview/general/leak.js
deleted file mode 100644 (file)
index 1cef613..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-import _ from 'underscore';
-import moment from 'moment';
-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, getPeriodDate } from './../helpers/period-label';
-
-
-export default React.createClass({
-  render() {
-    if (_.size(this.props.component.periods) < 1) {
-      return null;
-    }
-
-    let period = periodLabel(this.props.component.periods, '1');
-    let periodDate = getPeriodDate(this.props.component.periods, '1');
-
-    return (
-        <div className="overview-leak">
-          <h2 className="overview-title">
-            {window.t('overview.water_leak')}
-            <span className="overview-leak-period">{period} / {moment(periodDate).format('LL')}</span>
-          </h2>
-          <Cards>
-            <LeakIssues component={this.props.component} leak={this.props.leak} measures={this.props.measures}/>
-            <LeakCoverage component={this.props.component} leak={this.props.leak}/>
-            <LeakDups component={this.props.component} leak={this.props.leak}/>
-            <LeakSize component={this.props.component} leak={this.props.leak}/>
-          </Cards>
-        </div>
-    );
-  }
-});
index 233e111063e6cecfcac0efff6f25cef305394f71..dd0d748788ceab6b72b25fd85de1f66e63858d5b 100644 (file)
-import $ from 'jquery';
 import _ from 'underscore';
 import moment from 'moment';
 import React from 'react';
-import Gate from './gate';
-import Leak from './leak';
-import Nutshell from './nutshell';
-import MoreDetails from './details';
-import { getPeriodDate } from './../helpers/period-label';
+
+import { GeneralIssues } from './issues';
+import { GeneralCoverage } from './coverage';
+import { GeneralDuplications } from './duplications';
+import { GeneralSize } from './size';
+import { getPeriodLabel, getPeriodDate } from './../helpers/period-label';
+import { getMeasuresAndVariations } from '../../../api/measures';
+import { getFacet, getIssuesCount } from '../../../api/issues';
+import { getTimeMachineData } from '../../../api/time-machine';
+import { SEVERITIES, STATUSES } from '../../../helpers/constants';
+
+
+const METRICS_LIST = [
+  'sqale_rating',
+  'overall_coverage',
+  'new_overall_coverage',
+  'tests',
+  'duplicated_lines_density',
+  'duplicated_blocks',
+  'ncloc',
+  'files'
+];
+
+const HISTORY_METRICS_LIST = [
+  'violations',
+  'overall_coverage',
+  'duplicated_lines_density',
+  'ncloc'
+];
+
+
+function getFacetValue (facet, key) {
+  return _.findWhere(facet, { val: key }).count;
+}
+
 
 export default React.createClass({
+  propTypes: {
+    leakPeriodIndex: React.PropTypes.string.isRequired
+  },
+
   getInitialState() {
-    return { leak: this.props.leak, measures: this.props.measures };
+    return {
+      ready: false,
+      history: {},
+      leakPeriodLabel: getPeriodLabel(this.props.component.periods, this.props.leakPeriodIndex),
+      leakPeriodDate: getPeriodDate(this.props.component.periods, this.props.leakPeriodIndex)
+    };
   },
 
   componentDidMount() {
-    if (this._hasWaterLeak()) {
-      this.requestLeakIssues();
-      this.requestLeakDebt();
-    }
-    this.requestNutshellIssues();
-    this.requestNutshellDebt();
-  },
+    Promise.all([
+      this.requestMeasures(),
+      this.requestIssuesAndDebt(),
+      this.requestIssuesSeverities(),
+      this.requestLeakIssuesAndDebt(),
+      this.requestIssuesLeakSeverities(),
+      this.requestIssuesLeakStatuses()
+    ]).then(responses => {
+      let measures = this.getMeasuresValues(responses[0], 'value');
+      measures.issues = responses[1].issues;
+      measures.debt = responses[1].debt;
+      measures.issuesSeverities = SEVERITIES.map(s => getFacetValue(responses[2].facet, s));
 
-  _hasWaterLeak() {
-    return !!_.findWhere(this.props.component.periods, { index: '1' });
+      let leak;
+      if (this.state.leakPeriodLabel) {
+        leak = this.getMeasuresValues(responses[0], 'var' + this.props.leakPeriodIndex);
+        leak.issues = responses[3].issues;
+        leak.debt = responses[3].debt;
+        leak.issuesSeverities = SEVERITIES.map(s => getFacetValue(responses[4].facet, s));
+        leak.issuesStatuses = STATUSES.map(s => getFacetValue(responses[5].facet, s));
+      }
+
+      this.setState({
+        ready: true,
+        measures: measures,
+        leak: leak
+      }, this.requestHistory);
+    });
   },
 
-  _requestIssues(data) {
-    let url = `${baseUrl}/api/issues/search`;
-    data.ps = 1;
-    data.componentUuids = this.props.component.id;
-    return $.get(url, data);
+  requestMeasures () {
+    return getMeasuresAndVariations(this.props.component.key, METRICS_LIST);
   },
 
-  requestLeakIssues() {
-    let createdAfter = moment(getPeriodDate(this.props.component.periods, '1')).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;
+  getMeasuresValues (measures, fieldKey) {
+    let values = {};
+    Object.keys(measures).forEach(measureKey => {
+      values[measureKey] = measures[measureKey][fieldKey];
+    });
+    return 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
-        })
-      });
+  requestIssuesAndDebt() {
+    // FIXME requesting severities facet only to get debtTotal
+    return getIssuesCount({
+      componentUuids: this.props.component.id,
+      resolved: 'false',
+      facets: 'severities'
     });
   },
 
-  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;
+  requestLeakIssuesAndDebt() {
+    if (!this.state.leakPeriodLabel) {
+      return Promise.resolve();
+    }
 
-      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
-        })
-      });
+    let createdAfter = moment(this.state.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+    // FIXME requesting severities facet only to get debtTotal
+    return getIssuesCount({
+      componentUuids: this.props.component.id,
+      createdAfter: createdAfter,
+      resolved: 'false',
+      facets: 'severities'
     });
   },
 
-  requestLeakDebt() {
-    let createdAfter = moment(getPeriodDate(this.props.component.periods, '1')).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 })
-      });
-    });
+  requestIssuesSeverities() {
+    return getFacet({ componentUuids: this.props.component.id, resolved: 'false' }, 'severities');
   },
 
-  requestNutshellDebt() {
-    this._requestIssues({ resolved: 'false', facets: 'severities', facetMode: 'debt' }).done(r => {
-      this.setState({
-        measures: _.extend({}, this.state.measures, { debt: r.debtTotal })
+  requestIssuesLeakSeverities() {
+    if (!this.state.leakPeriodLabel) {
+      return Promise.resolve();
+    }
+
+    let createdAfter = moment(this.state.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+    return getFacet({
+      componentUuids: this.props.component.id,
+      createdAfter: createdAfter,
+      resolved: 'false'
+    }, 'severities');
+  },
+
+  requestIssuesLeakStatuses() {
+    if (!this.state.leakPeriodLabel) {
+      return Promise.resolve();
+    }
+
+    let createdAfter = moment(this.state.leakPeriodDate).format('YYYY-MM-DDTHH:mm:ssZZ');
+
+    return getFacet({
+      componentUuids: this.props.component.id,
+      createdAfter: createdAfter,
+      resolved: 'false'
+    }, 'statuses');
+  },
+
+  requestHistory () {
+    let metrics = HISTORY_METRICS_LIST.join(',');
+    return getTimeMachineData(this.props.component.key, metrics).then(r => {
+      let history = {};
+      r[0].cols.forEach((col, index) => {
+        history[col.metric] = r[0].cells.map(cell => {
+          let date = moment(cell.d).toDate();
+          let value = cell.v[index] || 0;
+          return { date, value };
+        });
       });
+      this.setState({ history });
     });
   },
 
+  renderLoading () {
+    return <div className="text-center">
+      <i className="spinner spinner-margin"/>
+    </div>;
+  },
+
   render() {
-    return <div>
-      <Gate component={this.props.component} gate={this.props.gate}/>
-      <Leak component={this.props.component} leak={this.state.leak} measures={this.state.measures}/>
-      <Nutshell component={this.props.component} measures={this.state.measures} section={this.props.section}/>
-      <MoreDetails component={this.props.component} measures={this.state.measures}
-                   section={this.props.section} onRoute={this.props.onRoute}/>
+    if (!this.state.ready) {
+      return this.renderLoading();
+    }
+
+    let props = _.extend({}, this.props, this.state);
+
+    return <div className="overview-domains">
+      <GeneralIssues {...props} history={this.state.history['violations']}/>
+      <GeneralCoverage {...props} history={this.state.history['overall_coverage']}/>
+      <GeneralDuplications {...props} history={this.state.history['duplicated_lines_density']}/>
+      <GeneralSize {...props} history={this.state.history['ncloc']}/>
     </div>;
   }
 });
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-coverage.js
deleted file mode 100644 (file)
index dabcb76..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import Card from './card';
-import Measure from './../helpers/measure';
-import DrilldownLink from './../helpers/drilldown-link';
-import Donut from './../helpers/donut';
-
-export default React.createClass({
-  render() {
-    let
-        coverage = this.props.measures.coverage,
-        tests = this.props.measures.tests,
-        donutData = [
-          { value: coverage, fill: '#85bb43' },
-          { value: 100 - coverage, fill: '#d4333f' }
-        ];
-
-    if (coverage == null) {
-      return null;
-    }
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measures-chart">
-              <Donut data={donutData} size="47"/>
-            </div>
-            <div className="measure measure-big">
-              <span className="measure-name">{window.t('overview.metric.coverage')}</span>
-              <span className="measure-value">
-                <DrilldownLink component={this.props.component.key} metric="overall_coverage">
-                  <Measure value={coverage} type="PERCENT"/>
-                </DrilldownLink>
-              </span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top measures-chart-indent">
-            <li>
-              <DrilldownLink component={this.props.component.key} metric="tests">
-                <Measure value={tests} type="SHORT_INT"/>
-              </DrilldownLink>&nbsp;
-              <span>{window.t('overview.metric.tests')}</span>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-dups.js
deleted file mode 100644 (file)
index 6c4bb0b..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import Card from './card';
-import Measure from './../helpers/measure';
-import DrilldownLink from './../helpers/drilldown-link';
-import Donut from './../helpers/donut';
-
-export default React.createClass({
-  render() {
-    let
-        density = this.props.measures.duplications,
-        lines = this.props.measures.duplicatedLines,
-        donutData = [
-          { value: density, fill: '#f3ca8e' },
-          { value: 100 - density, fill: '#e6e6e6' }
-        ];
-
-    if (density == null) {
-      return null;
-    }
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measures-chart">
-              <Donut data={donutData} size="47"/>
-            </div>
-            <div className="measure measure-big">
-              <span className="measure-name">{window.t('overview.metric.duplications')}</span>
-              <span className="measure-value">
-                <DrilldownLink component={this.props.component.key} metric="duplicated_lines_density">
-                  <Measure value={density} type="PERCENT"/>
-                </DrilldownLink>
-              </span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top measures-chart-indent">
-            <li>
-              <DrilldownLink component={this.props.component.key} metric="duplicated_lines">
-                <Measure value={lines} type="SHORT_INT"/>
-              </DrilldownLink>&nbsp;
-              <span>{window.t('overview.metric.duplicated_lines')}</span>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-issues.js
deleted file mode 100644 (file)
index 3cad21f..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react';
-import Card from './card';
-import Measure from './../helpers/measure';
-import Rating from './../helpers/rating';
-import IssuesLink from './../helpers/issues-link';
-import DrilldownLink from './../helpers/drilldown-link';
-import SeverityIcon from '../../../components/shared/severity-icon';
-import StatusIcon from '../../../components/shared/status-icon';
-
-export default React.createClass({
-  render() {
-    let
-        debt = this.props.measures.debt,
-        rating = this.props.measures.sqaleRating,
-        issues = this.props.measures.issues,
-        blockerIssues = this.props.measures.blockerIssues,
-        criticalIssues = this.props.measures.criticalIssues,
-        issuesToReview = this.props.measures.openIssues + this.props.measures.reopenedIssues;
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measure measure-big" data-metric="sqale_rating">
-              <DrilldownLink component={this.props.component.key} metric="sqale_rating">
-                <Rating value={rating}/>
-              </DrilldownLink>
-            </div>
-            <div className="measure measure-big" data-metric="sqale_index">
-              <span className="measure-name">{window.t('overview.metric.debt')}</span>
-              <span className="measure-value">
-                <IssuesLink component={this.props.component.key} params={{ resolved: 'false', facetMode: 'debt' }}>
-                  <Measure value={debt} type="SHORT_WORK_DUR"/>
-                </IssuesLink>
-              </span>
-            </div>
-            <div className="measure measure-big" data-metric="violations">
-              <span className="measure-name">{window.t('overview.metric.issues')}</span>
-              <span className="measure-value">
-                <IssuesLink component={this.props.component.key} params={{ resolved: 'false' }}>
-                  <Measure value={issues} type="SHORT_INT"/>
-                </IssuesLink>
-              </span>
-            </div>
-          </div>
-          <ul className="list-inline big-spacer-top">
-            <li>
-              <span><SeverityIcon severity="BLOCKER"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'BLOCKER' }}>
-                <Measure value={blockerIssues} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-            <li>
-              <span><SeverityIcon severity="CRITICAL"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key} params={{ resolved: 'false', severities: 'CRITICAL' }}>
-                <Measure value={criticalIssues} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-            <li>
-              <span><StatusIcon status="OPEN"/></span>&nbsp;
-              <IssuesLink component={this.props.component.key} params={{ resolved: 'false', statuses: 'OPEN,REOPENED' }}>
-                <Measure value={issuesToReview} type="SHORT_INT"/>
-              </IssuesLink>
-            </li>
-          </ul>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell-size.js
deleted file mode 100644 (file)
index 845088f..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-import Card from './card';
-import Measure from './../helpers/measure';
-import DrilldownLink from './../helpers/drilldown-link';
-
-export default React.createClass({
-  render() {
-    let
-        lines = this.props.measures['lines'],
-        files = this.props.measures['files'];
-
-    return (
-        <Card>
-          <div className="measures">
-            <div className="measure measure-big" data-metric="lines">
-              <span className="measure-name">{window.t('overview.metric.lines')}</span>
-              <span className="measure-value">
-                <DrilldownLink component={this.props.component.key} metric="lines">
-                  <Measure value={lines} type="SHORT_INT"/>
-                </DrilldownLink>
-              </span>
-            </div>
-            <div className="measure measure-big" data-metric="files">
-              <span className="measure-name">{window.t('overview.metric.files')}</span>
-              <span className="measure-value">
-                <DrilldownLink component={this.props.component.key} metric="files">
-                  <Measure value={files} type="SHORT_INT"/>
-                </DrilldownLink>
-              </span>
-            </div>
-          </div>
-        </Card>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/nutshell.js b/server/sonar-web/src/main/js/apps/overview/general/nutshell.js
deleted file mode 100644 (file)
index e538149..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import Cards from './cards';
-import NutshellIssues from './nutshell-issues';
-import NutshellCoverage from './nutshell-coverage';
-import NutshellSize from './nutshell-size';
-import NutshellDups from './nutshell-dups';
-
-export default React.createClass({
-  render() {
-    let props = {
-      measures: this.props.measures,
-      component: this.props.component
-    };
-    return (
-        <div className="overview-nutshell">
-          <h2 className="overview-title">{window.t('overview.project_in_a_nutshell')}</h2>
-          <Cards>
-            <NutshellIssues {...props}/>
-            <NutshellCoverage {...props}/>
-            <NutshellDups {...props}/>
-            <NutshellSize {...props}/>
-          </Cards>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/size.js b/server/sonar-web/src/main/js/apps/overview/general/size.js
new file mode 100644 (file)
index 0000000..ac43ab6
--- /dev/null
@@ -0,0 +1,60 @@
+import React from 'react';
+
+import { Domain, DomainHeader, DomainPanel, DomainNutshell, DomainLeak, MeasuresList, Measure, DomainMixin } from './components';
+import DrilldownLink from '../helpers/drilldown-link';
+import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { getMetricName } from '../helpers/metrics';
+
+
+export const GeneralSize = React.createClass({
+  mixins: [TooltipsMixin, DomainMixin],
+
+  propTypes: {
+    leakPeriodLabel: React.PropTypes.string,
+    leakPeriodDate: React.PropTypes.object
+  },
+
+  renderLeak () {
+    if (!this.hasLeakPeriod()) {
+      return null;
+    }
+
+    return <DomainLeak>
+      <MeasuresList>
+        <Measure label={getMetricName('ncloc')}>
+          {window.formatMeasureVariation(this.props.leak['ncloc'], 'SHORT_INT')}
+        </Measure>
+        <Measure label={getMetricName('files')}>
+          {window.formatMeasureVariation(this.props.leak['files'], 'SHORT_INT')}
+        </Measure>
+      </MeasuresList>
+      {this.renderTimeline('after')}
+    </DomainLeak>;
+  },
+
+  render () {
+    return <Domain>
+      <DomainHeader title="Size"
+                    leakPeriodLabel={this.props.leakPeriodLabel} leakPeriodDate={this.props.leakPeriodDate}/>
+
+      <DomainPanel domain="size">
+        <DomainNutshell>
+          <MeasuresList>
+            <Measure label={getMetricName('ncloc')}>
+              <DrilldownLink component={this.props.component.key} metric="ncloc">
+                {window.formatMeasure(this.props.measures['ncloc'], 'SHORT_INT')}
+              </DrilldownLink>
+            </Measure>
+            <Measure label={getMetricName('files')}>
+              <DrilldownLink component={this.props.component.key} metric="files">
+                {window.formatMeasure(this.props.measures['files'], 'SHORT_INT')}
+              </DrilldownLink>
+            </Measure>
+          </MeasuresList>
+          {this.renderTimeline('before')}
+        </DomainNutshell>
+        {this.renderLeak()}
+      </DomainPanel>
+    </Domain>;
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/general/timeline.js b/server/sonar-web/src/main/js/apps/overview/general/timeline.js
new file mode 100644 (file)
index 0000000..3938fff
--- /dev/null
@@ -0,0 +1,47 @@
+import d3 from 'd3';
+import React from 'react';
+
+import { LineChart } from '../../../components/charts/line-chart';
+
+
+const HEIGHT = 80;
+
+
+export class Timeline extends React.Component {
+  filterSnapshots () {
+    return this.props.history.filter(s => {
+      let matchBefore = !this.props.before || s.date <= this.props.before;
+      let matchAfter = !this.props.after || s.date >= this.props.after;
+      return matchBefore && matchAfter;
+    });
+  }
+
+  render () {
+    let snapshots = this.filterSnapshots();
+
+    if (snapshots.length < 2) {
+      return null;
+    }
+
+    let data = snapshots.map((snapshot, index) => {
+      return { x: index, y: snapshot.value };
+    });
+
+    let domain = [0, d3.max(this.props.history, d => d.value)];
+
+    return <LineChart data={data}
+                      domain={domain}
+                      interpolate="basis"
+                      displayBackdrop={true}
+                      displayPoints={false}
+                      displayVerticalGrid={false}
+                      height={HEIGHT}
+                      padding={[0, 0, 0, 0]}/>;
+  }
+}
+
+Timeline.propTypes = {
+  history: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+  before: React.PropTypes.object,
+  after: React.PropTypes.object
+};
index d3118579036d8860c7936f232f62c75b3726f808..f133139567125ebd0e8c32f0c87aaf51b048fe92 100644 (file)
@@ -5,7 +5,7 @@ export default React.createClass({
     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}`;
+        url = `${window.baseUrl}/component_issues/index?id=${encodeURIComponent(this.props.component)}#${params}`;
     return <a href={url}>{this.props.children}</a>;
   }
 });
index 1a1dcefcc4761c956d6a09375fc09b1c20193b82..f556841abe33be8700fb33c9bd6b81ba24a68cb3 100644 (file)
@@ -19,3 +19,8 @@ export function filterMetricsForDomains (metrics, domains) {
     return hasRightDomain(metric, domains) && isNotHidden(metric) && hasSimpleType(metric) && isNotDifferential(metric);
   });
 }
+
+
+export function getMetricName (metricKey) {
+  return window.t('overview.metric', metricKey);
+}
index 109a9df9a57ad28c8b2aee6bd5d4a5a8b6065685..07cfcd43b9ff28d737bcecf8f9de65b22dbbba92 100644 (file)
@@ -1,7 +1,7 @@
 import _ from 'underscore';
 import moment from 'moment';
 
-export let periodLabel = (periods, periodIndex) => {
+export let getPeriodLabel = (periods, periodIndex) => {
   let period = _.findWhere(periods, { index: periodIndex });
   if (!period) {
     return null;
index af445cc981b3fabb4b432de76356eed3bc708052..2d0905c8030ffe9e488c1b96affa00cd91eb669f 100644 (file)
@@ -1,7 +1,6 @@
 import React from 'react';
 import Assignee from '../../../components/shared/assignee-helper';
 import { DomainHeader } from '../domain/header';
-import { formatMeasure } from '../formatting';
 import { componentIssuesUrl } from '../../../helpers/Url';
 
 export default class extends React.Component {
@@ -13,7 +12,7 @@ export default class extends React.Component {
           <Assignee user={s.user}/>
         </td>
         <td className="thin text-right">
-          <a href={href}>{formatMeasure(s.count, 'violations')}</a>
+          <a href={href}>{window.formatMeasure(s.count, 'SHORT_INT')}</a>
         </td>
       </tr>;
     });
index e55a2f0cf2bbbc0bdfeb82b155739eb668bf3a2d..d615b8d44a3aec26ca0953cd48ac38e107a0045d 100644 (file)
@@ -18,7 +18,7 @@ export default class OverviewDomain extends React.Component {
 
   componentDidMount () {
     Promise.all([
-      this.requestSeverities(),
+      this.requestIssuesSeverities(),
       this.requestTags(),
       this.requestAssignees()
     ]).then(responses => {
@@ -43,7 +43,15 @@ export default class OverviewDomain extends React.Component {
   }
 
   render () {
-    return <div className="overview-domain">
+    return <div className="overview-detailed-page">
+
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Issues & Technical Debt</h2>
+      </div>
+
+      <a className="overview-detailed-page-back" href="#">
+        <i className="icon-chevron-left"/>
+      </a>
 
       <IssuesTimeline {...this.props}/>
 
index be2911b9889483b06612de0beef9b6c2a53c2946..5ccb3098b861a68be8c09819d4360818f6995a33 100644 (file)
@@ -2,7 +2,6 @@ import _ from 'underscore';
 import React from 'react';
 import SeverityHelper from '../../../components/shared/severity-helper';
 import { DomainHeader } from '../domain/header';
-import { formatMeasure } from '../formatting';
 import { componentIssuesUrl } from '../../../helpers/Url';
 
 export default class extends React.Component {
@@ -19,7 +18,7 @@ export default class extends React.Component {
         </td>
         <td className="thin text-right">
           <a className="cell-link" href={href}>
-            {formatMeasure(s.count, 'violations')}
+            {window.formatMeasure(s.count, 'SHORT_INT')}
           </a>
         </td>
       </tr>;
diff --git a/server/sonar-web/src/main/js/apps/overview/main.js b/server/sonar-web/src/main/js/apps/overview/main.js
deleted file mode 100644 (file)
index 5c19f42..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-import React from 'react';
-import offset from 'document-offset';
-
-import GeneralMain from './general/main';
-import IssuesMain from './issues/main';
-import CoverageMain from './coverage/main';
-import DuplicationsMain from './duplications/main';
-import SizeMain from './size/main';
-import Meta from './meta';
-import Empty from './general/empty';
-
-import { getMetrics } from '../../api/metrics';
-
-
-export const Overview = React.createClass({
-  getInitialState () {
-    let hash = window.location.hash;
-    return { section: hash.length ? hash.substr(1) : null };
-  },
-
-  componentWillMount () {
-    window.addEventListener('hashchange', this.handleHashChange);
-  },
-
-  componentDidMount () {
-    if (this.props.component.hasSnapshot) {
-      this.requestMetrics();
-    }
-  },
-
-  componentWillUnmount () {
-    window.removeEventListener('hashchange', this.handleHashChange);
-  },
-
-  requestMetrics () {
-    return getMetrics().then(metrics => this.setState({ metrics }));
-  },
-
-  handleRoute (section) {
-    if (section !== this.state.section) {
-      let el = document.querySelector('.overview-more');
-      this.setState({ section }, () => this.scrollToEl(el));
-      window.location.href = '#' + section;
-    } else {
-      this.setState({ section: null });
-      window.location.href = '#';
-    }
-  },
-
-  handleHashChange () {
-    let hash = window.location.hash;
-    this.setState({ section: hash.substr(1) });
-  },
-
-  scrollToEl (el) {
-    let top = offset(el).top;
-    window.scrollTo(0, top);
-  },
-
-  render () {
-    if (!this.props.component.hasSnapshot) {
-      return <div className="overview"><Empty/></div>;
-    }
-
-    if (!this.state.metrics) {
-      return null;
-    }
-
-    let child;
-    switch (this.state.section) {
-      case 'issues':
-        child = <IssuesMain {...this.props} {...this.state}/>;
-        break;
-      case 'coverage':
-        child = <CoverageMain {...this.props} {...this.state}/>;
-        break;
-      case 'duplications':
-        child = <DuplicationsMain {...this.props} {...this.state}/>;
-        break;
-      case 'size':
-        child = <SizeMain {...this.props} {...this.state}/>;
-        break;
-      default:
-        child = null;
-    }
-
-    return <div className="overview">
-      <div className="overview-main">
-        <GeneralMain {...this.props} section={this.state.section} onRoute={this.handleRoute}/>
-        {child}
-      </div>
-      <Meta component={this.props.component}/>
-    </div>;
-  }
-});
index 310aadd8ae6b1422f0282f0e9cae9fb0a9beb285..e3e632a30fa72c37795278d3b0b4c34febd535b7 100644 (file)
@@ -25,26 +25,26 @@ export default React.createClass({
         });
 
     let descriptionCard = this.props.component.description ? (
-            <div className="overview-card">
+            <div className="overview-meta-card">
               <div className="overview-meta-description">{this.props.component.description}</div>
             </div>
         ) : null,
 
         linksCard = _.size(this.props.component.links) > 0 ? (
-            <div className="overview-card">
+            <div className="overview-meta-card">
               <ul className="overview-meta-list">{links}</ul>
             </div>
         ) : null,
 
         profilesCard = _.size(this.props.component.profiles) > 0 ? (
-            <div className="overview-card">
+            <div className="overview-meta-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">
+            <div className="overview-meta-card">
               <h4 className="overview-meta-header">{window.t('overview.quality_gate')}</h4>
               <ul className="overview-meta-list">
                 <li>
@@ -60,8 +60,8 @@ export default React.createClass({
         <div className="overview-meta">
           {descriptionCard}
           {linksCard}
-          {profilesCard}
           {gateCard}
+          {profilesCard}
         </div>
     );
   }
diff --git a/server/sonar-web/src/main/js/apps/overview/overview.js b/server/sonar-web/src/main/js/apps/overview/overview.js
new file mode 100644 (file)
index 0000000..f03573a
--- /dev/null
@@ -0,0 +1,54 @@
+import React from 'react';
+
+import Gate from './gate/gate';
+import GeneralMain from './general/main';
+import Meta from './meta';
+import { getMetrics } from '../../api/metrics';
+
+
+export const Overview = React.createClass({
+  getInitialState () {
+    return { ready: false };
+  },
+
+  componentDidMount () {
+    this.requestMetrics();
+  },
+
+  requestMetrics () {
+    return getMetrics().then(metrics => this.setState({ ready: true, metrics }));
+  },
+
+  renderLoading () {
+    return <div className="text-center">
+      <i className="spinner spinner-margin"/>
+    </div>;
+  },
+
+  render () {
+    if (!this.state.ready) {
+      return this.renderLoading();
+    }
+
+    return <div className="overview">
+      <div className="overview-main">
+        <Gate component={this.props.component} gate={this.props.gate}/>
+        <GeneralMain {...this.props} {...this.state}/>
+      </div>
+      <Meta component={this.props.component}/>
+    </div>;
+  }
+});
+
+
+export const EmptyOverview = React.createClass({
+  render() {
+    return (
+        <div className="page">
+          <div className="alert alert-warning">
+            {window.t('provisioning.no_analysis')}
+          </div>
+        </div>
+    );
+  }
+});
index 72f892c9db6000330b7636a2164189069860a2f7..9e037e136c95f4cd71ecbf6592b28f84177ff1a0 100644 (file)
@@ -11,7 +11,15 @@ import { SizeTreemap } from './treemap';
 
 export default class extends React.Component {
   render () {
-    return <div className="overview-domain">
+    return <div className="overview-detailed-page">
+      <div className="overview-domain-header">
+        <h2 className="overview-title">Size</h2>
+      </div>
+
+      <a className="overview-detailed-page-back" href="#">
+        <i className="icon-chevron-left"/>
+      </a>
+
       <SizeTimeline {...this.props}/>
 
       <div className="flex-columns">
index c7444fedcb9a76f1e270e0bbe22c60f1509ccc2a..f82a9c7cda28c9a15d1ee64fa78362c2e40c8315 100644 (file)
@@ -2,7 +2,7 @@ import d3 from 'd3';
 import React from 'react';
 
 import { ResizeMixin } from './mixins/resize-mixin';
-import { TooltipsMixin } from './mixins/tooltips-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
 
 export const BarChart = React.createClass({
   mixins: [ResizeMixin, TooltipsMixin],
index e388a2baecb5aae021acd2e77cb41aaf9770b69a..9e3facaa74d7afe7edc9e25cffa20c5fa55ad32a 100644 (file)
@@ -2,7 +2,7 @@ import d3 from 'd3';
 import React from 'react';
 
 import { ResizeMixin } from './mixins/resize-mixin';
-import { TooltipsMixin } from './mixins/tooltips-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
 
 
 export const Bubble = React.createClass({
index f7c560bd3e1e5d717dba034b901e856e466d12f5..eeaed5e88a9584f34bf5132672d213795c5f505c 100644 (file)
@@ -2,7 +2,7 @@ import d3 from 'd3';
 import React from 'react';
 
 import { ResizeMixin } from './mixins/resize-mixin';
-import { TooltipsMixin } from './mixins/tooltips-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
 
 
 export const LineChart = React.createClass({
@@ -124,14 +124,21 @@ export const LineChart = React.createClass({
     let availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3];
     let availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2];
 
-    let maxY = d3.max(this.props.data, d => d.y);
+    let maxY;
     let xScale = d3.scale.linear()
                    .domain(d3.extent(this.props.data, d => d.x))
                    .range([0, availableWidth]);
     let yScale = d3.scale.linear()
-                   .domain([0, maxY])
                    .range([availableHeight, 0]);
 
+    if (this.props.domain) {
+      maxY = this.props.domain[1];
+      yScale.domain(this.props.domain);
+    } else {
+      maxY = d3.max(this.props.data, d => d.y);
+      yScale.domain([0, maxY]);
+    }
+
     return <svg className="line-chart" width={this.state.width} height={this.state.height}>
       <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
         {this.renderVerticalGrid(xScale, yScale, maxY)}
diff --git a/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js b/server/sonar-web/src/main/js/components/charts/mixins/tooltips-mixin.js
deleted file mode 100644 (file)
index 240edee..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-import $ from 'jquery';
-import React from 'react';
-import ReactDOM from 'react-dom';
-
-export const TooltipsMixin = {
-  componentDidMount () {
-    this.initTooltips();
-  },
-
-  componentDidUpdate () {
-    this.initTooltips();
-  },
-
-  initTooltips () {
-    if ($.fn.tooltip) {
-      $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this))
-          .tooltip({ container: 'body', placement: 'bottom', html: true });
-    }
-  }
-};
index be054fdc055fbe8a45a2f8670c04dddbfde800d6..24bcff49ce2f899810deaa2a7666e51274d77068 100644 (file)
@@ -3,7 +3,7 @@ import d3 from 'd3';
 import React from 'react';
 
 import { ResizeMixin } from './mixins/resize-mixin';
-import { TooltipsMixin } from './mixins/tooltips-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
 
 
 const SIZE_SCALE = d3.scale.linear()
index ed59b040fbf38857c61c0f9e0bcab41dc0ba152e..1edbc2274ecc2cb08fcc863076c21ef2e235ecb3 100644 (file)
@@ -2,7 +2,7 @@ import _ from 'underscore';
 import d3 from 'd3';
 import React from 'react';
 
-import { TooltipsMixin } from './mixins/tooltips-mixin';
+import { TooltipsMixin } from './../mixins/tooltips-mixin';
 
 export const Word = React.createClass({
   propTypes: {
diff --git a/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
new file mode 100644 (file)
index 0000000..240edee
--- /dev/null
@@ -0,0 +1,20 @@
+import $ from 'jquery';
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+export const TooltipsMixin = {
+  componentDidMount () {
+    this.initTooltips();
+  },
+
+  componentDidUpdate () {
+    this.initTooltips();
+  },
+
+  initTooltips () {
+    if ($.fn.tooltip) {
+      $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this))
+          .tooltip({ container: 'body', placement: 'bottom', html: true });
+    }
+  }
+};
diff --git a/server/sonar-web/src/main/js/helpers/constants.js b/server/sonar-web/src/main/js/helpers/constants.js
new file mode 100644 (file)
index 0000000..392adf2
--- /dev/null
@@ -0,0 +1,2 @@
+export const SEVERITIES = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
+export const STATUSES = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED'];
index 8692a73f9cb4a7a5606a4cd566b986b5c72b1d4f..8d2236d97104d16d16ab4670093ef5d3d1b92847 100644 (file)
@@ -281,7 +281,7 @@ function closeModalWindow () {
 
   function shortIntVariationFormatter (value) {
     if (value === 0) {
-      return '0';
+      return '+0';
     }
     var format = '+0,0';
     if (Math.abs(value) >= 1000) {
@@ -353,21 +353,6 @@ function closeModalWindow () {
     return value.length > 0 ? value + ' ' : value;
   }
 
-  /**
-   * Check if about sign be displayed for a work duration
-   * @param {number} days
-   * @param {number} hours
-   * @param {number} minutes
-   * @returns {boolean}
-   */
-  function shouldDisplayAbout (days, hours, minutes) {
-    var hasDays = days > 0,
-        fewDays = days < 5,
-        hasHours = hours > 0,
-        hasMinutes = minutes > 0;
-    return (hasDays && fewDays && hasHours) || (!hasDays && hasHours && hasMinutes);
-  }
-
   /**
    * Format a work duration based on parameters
    * @param {bool} isNegative
@@ -414,9 +399,6 @@ function closeModalWindow () {
       formatted = addSpaceIfNeeded(formatted);
       formatted += tp('work_duration.x_minutes', isNegative && formatted.length === 0 ? -1 * minutes : minutes);
     }
-    if (shouldDisplayAbout(days, hours, minutes)) {
-      formatted = tp('work_duration.about', formatted);
-    }
     return formatted;
   }
 
@@ -432,9 +414,9 @@ function closeModalWindow () {
     var hoursInDay = window.SS.hoursInDay || 8,
         isNegative = value < 0,
         absValue = Math.abs(value);
-    var days = Math.floor(absValue / hoursInDay / 60);
+    var days = Math.round(absValue / hoursInDay / 60);
     var remainingValue = absValue - days * hoursInDay * 60;
-    var hours = Math.floor(remainingValue / 60);
+    var hours = Math.round(remainingValue / 60);
     remainingValue -= hours * 60;
     return formatDuration(isNegative, days, hours, remainingValue);
   };
@@ -462,6 +444,7 @@ function closeModalWindow () {
   /**
    * Format a work duration variation
    * @param {number} value
+   * @returns {string}
    */
   var durationVariationFormatter = function (value) {
     if (value === 0) {
@@ -471,6 +454,19 @@ function closeModalWindow () {
     return formatted[0] !== '-' ? '+' + formatted : formatted;
   };
 
+  /**
+   * Format a work duration variation
+   * @param {number} value
+   * @returns {string}
+   */
+  var shortDurationVariationFormatter = function (value) {
+    if (value === 0) {
+      return '0';
+    }
+    var formatted = shortDurationFormatter(value);
+    return formatted[0] !== '-' ? '+' + formatted : formatted;
+  };
+
   /**
    * Format a rating measure
    * @param {number} value
@@ -552,9 +548,10 @@ function closeModalWindow () {
             return value === 0 ? '0' : numeral(value).format('+0,0.0');
           },
           'PERCENT': function (value) {
-            return value === 0 ? '0%' : numeral(+value / 100).format('+0,0.0%');
+            return value === 0 ? '+0%' : numeral(+value / 100).format('+0,0.0%');
           },
-          'WORK_DUR': durationVariationFormatter
+          'WORK_DUR': durationVariationFormatter,
+          'SHORT_WORK_DUR': shortDurationVariationFormatter
         };
     if (measure != null && type != null) {
       formatted = formatters[type] != null ? formatters[type](measure) : measure;
index 072a4cea06235637cbb0577b81135e7ae35475da..2e519588943192c71a398bd3019e15e2de68d6ff 100644 (file)
 @import (reference) "../init/type";
 @import (reference) "../init/links";
 
-@side-padding: 30px;
+@side-padding: 20px;
 
 .overview {
   display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
   width: 100%;
   min-height: ~"calc(100vh - @{navbarGlobalHeight} - @{navbarContextHeight} - @{pageFooterHeight})";
-}
-
-.overview > .panel {
-  flex: 1;
+  overflow-x: hidden;
+  animation: fadeIn 0.5s forwards;
 }
 
 .overview-main {
-  flex: 1;
+  flex: 4;
   box-sizing: border-box;
-  background-color: #fff;
+  background-color: @barBackgroundColor;
+  transition: transform 0.5s ease, opacity 0.5s ease;
 }
 
-.overview-gate {
-  .clearfix;
-  padding: 50px 0 25px;
-}
+/*
+ * Gate
+ */
 
-.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 {
+  margin-right: 20px;
+  padding: 15px 0;
+  border-bottom: 1px solid @barBorderColor;
+  background-color: @barBackgroundColor;
 
-.overview-gate-box-warn {
-  background-color: @orange;
+  .overview-title {
+    margin: 0 @side-padding;
+  }
 }
 
-.overview-gate-box-ok {
-  background-color: @green;
+.overview-gate-conditions-list {
+  display: flex;
+  flex-wrap: wrap;
 }
 
-.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 {
+  padding: 10px @side-padding;
 }
 
 .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;
+  margin-right: 4px;
   font-weight: 300;
-  font-size: 22px;
+  font-size: 24px;
 }
 
-.overview-gate-condition-itself {
-  padding-left: 4px;
-  color: mix(@baseFontColor, @barBackgroundColor, 70%);
-  font-size: 13px;
-  font-weight: 400;
+.overview-gate-warning {
+  margin: 15px @side-padding 0;
 }
 
-.overview-gate-condition-level {
-  margin-top: 8px;
-}
-
-.overview-leak {
-  padding: 50px 0 25px;
-  border-top: 1px solid @barBorderColor;
-  border-bottom: 1px solid @barBorderColor;
-}
+/*
+ * Title
+ */
 
 .overview-title {
-  padding: 0 @side-padding;
-  font-size: 18px;
+  font-size: 16px;
   font-weight: 400;
 
   & > .badge {
     position: relative;
     top: -2px;
     margin-left: 15px;
-    padding: 8px 15px;
-    font-size: 16px;
-    letter-spacing: 0.04em;
+    padding: 6px 12px;
+    font-size: 14px;
+    letter-spacing: 0.05em;
   }
 }
 
-.overview-leak-period {
-  margin-left: 10px;
-  font-size: 14px;
-}
-
-.overview-nutshell {
-  padding: 50px 0 25px;
-}
+/*
+ * Cards
+ * TODO drop it
+ */
 
 .overview-cards {
   display: flex;
+  flex-wrap: wrap;
 }
 
-.overview-card {
-  flex: 1 0 25%;
-  padding: 25px @side-padding;
-  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: @side-padding;
-  }
-
-  .measure-big .measure-name {
-    margin-top: 0;
-    margin-bottom: 2px;
-  }
-
-  .list-inline {
-    margin-left: -10px;
-    margin-right: -10px;
-
-    & > li {
-      padding-left: 10px;
-      padding-right: 10px;
-    }
-  }
-}
-
-.overview-card-section {
-  padding: 0;
-  text-align: center;
-
-  a {
-    display: block;
-    padding: 25px @side-padding;
-    .link-no-underline;
-    cursor: pointer;
-    transition: none;
-  }
-}
-
-.overview-card-section.active a,
-.overview-card-section a:hover {
-  background-color: #2c3946;
-  color: mix(#fff, #2c3946, 75%);
-}
-
-.overview-measure {
-  font-size: 28px;
-}
-
-.overview-measure-label {
-  font-size: 16px;
-}
+/*
+ * Meta
+ */
 
 .overview-meta {
-  width: 240px;
-  border-left: 1px solid @barBorderColor;
+  flex: 1;
   box-sizing: border-box;
   background-color: @barBackgroundColor;
-
-  .panel {
-    border: none !important;
-  }
 }
 
-.overview-meta .overview-card {
-  width: auto;
+.overview-meta-card {
+  min-width: 200px;
+  padding: @side-padding;
+  box-sizing: border-box;
 }
 
 .overview-meta-description {
   }
 }
 
-.overview-domain {
-  margin-top: -25px;
-}
-
-.overview-domain-dark {
-  background-color: #2c3946;
-  color: mix(#fff, #2c3946, 75%);
-
-  a {
-    color: @blue;
-    border-bottom-color: @darkBlue;
-
-    &:hover, &:focus {
-      border-bottom-color: @blue;
-    }
-  }
+/*
+ * Domain
+ */
 
-  .overview-title {
-    color: mix(#fff, #2c3946, 75%);
-  }
-
-  table.data.zebra > tbody > tr:nth-child(odd) {
-    background-color: mix(#fff, #2c3946, 5%);;
-  }
+.overview-domains {
+  animation: fadeIn 0.5s forwards;
 }
 
-.overview-domain-section {
-  padding: 50px @side-padding;
-
-  .overview-title {
-    margin-bottom: 25px;
-    padding-left: 0;
-    padding-right: 0;
-  }
+.overview-domain {
+  margin: 30px @side-padding;
 }
 
 .overview-domain-header {
   display: flex;
   align-items: baseline;
-  margin-bottom: 20px;;
-  padding: 50px @side-padding 0;
+  justify-content: space-between;
+  margin-bottom: 10px;
 
   .overview-title {
     flex: 1;
-    margin: 0;
-    padding: 0;
   }
 }
 
-.overview-timeline {
+.overview-domain-panel {
+  display: flex;
+  margin-top: 10px;
+  border: 1px solid @barBorderColor;
+  background-color: #fff;
+  overflow: hidden;
+}
+
+.overview-domain-nutshell,
+.overview-domain-leak {
   position: relative;
+  padding: 30px 10px;
+}
 
-  .line-chart {
+.overview-domain-nutshell {
+  flex: 2;
 
+  .line-chart-backdrop {
+    fill: #e5f1f9;
   }
+}
 
-  .line-chart-grid {
-    shape-rendering: crispedges;
-    stroke: #384653;
-  }
+.overview-domain-leak {
+  flex: 1;
+  background-color: #fffae7;
 
-  .line-chart-path {
-    fill: none;
-    stroke-width: 2;
-    stroke: @blue;
+  .line-chart-backdrop {
+    fill: #f1ecd1;
   }
+}
 
-  .line-chart-point {
-    fill: @blue;
-    stroke: none;
-  }
+.overview-domain-measures {
+  position: relative;
+  z-index: 2;
+  display: flex;
+  flex: 1;
+  justify-content: space-around;
+  align-items: center;
+}
+
+.overview-domain-measures + .overview-domain-measures {
+  margin-top: 30px;
 
-  .line-chart-tick {
-    fill: mix(#fff, #2c3946);
-    font-size: 11px;
-    text-anchor: middle;
+  .overview-domain-measure-value {
+    font-size: 14px;
+    font-weight: 400;
   }
 
-  .line-chart-backdrop {
-    fill: #4b9fd5;
-    fill-opacity: 0.2;
+  .overview-domain-measure-label {
+    margin-top: 4px;
   }
 }
 
-.overview-timeline-select {
-  height: @formControlHeight;
-  border: 1px solid mix(#fff, #2c3946);
-  background-color: transparent;
-  color: mix(#fff, #2c3946);
-}
+.overview-domain-measure {
 
-.overview-bubble-chart {
-  .bubble-chart-tick {
-    fill: mix(#fff, #2c3946);
-    font-size: 11px;
-    text-anchor: middle;
-  }
+}
 
-  .bubble-chart-tick-y {
-    text-anchor: end;
-  }
+.overview-domain-measure-value {
+  line-height: 1;
+  font-size: 36px;
+  font-weight: 300;
+  text-align: center;
+}
 
-  .bubble-chart-bubble {
-    stroke: @blue;
-    fill: @blue;
-    fill-opacity: 0.2;
-    transition: fill-opacity 0.2s ease;
+.overview-domain-measure-label {
+  margin-top: 10px;
+  text-align: center;
+}
 
-    &:hover {
-      fill-opacity: 0.5;
-    }
-  }
+.overview-domain-timeline {
+  position: absolute;
+  z-index: 1;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  animation: fadeIn 0.5s forwards;
 
-  .bubble-chart-grid {
-    stroke: #ccc;
+  .line-chart-path {
+    fill: none;
+    stroke: none;
   }
 }
 
-.overview-bar-chart {
-  .bar-chart-bar {
-    fill: @blue;
-  }
 
-  .bar-chart-tick {
-    fill: @baseFontColor;
-    font-size: 11px;
-    text-anchor: middle;
+
+/*
+ * Responsive Stuff
+ */
+
+@media (max-width: 1200px) {
+  .overview {
+    display: block;
   }
-}
 
-.overview-treemap {
-  .overview-domain-header {
-    padding-top: 0;
-    padding-left: 0;
-    padding-right: 0;
+  .overview-meta {
+    display: flex;
+    justify-content: flex-start;
   }
-}
 
-.overview-chart-placeholder {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  align-content: center;
+  .overview-meta .overview-meta-card {
+    max-width: 25%;
+  }
 }
 
-.overview-paragraph {
-  padding: 0 @side-padding;
-}
 
-.overview-more {
-  padding-top: 50px;
-  padding-bottom: 25px;
-  border-top: 1px solid @barBorderColor;
 
-  .overview-title {
-    padding-bottom: 25px;
-  }
+/*
+ * Animations
+ */
 
-  .overview-card + .overview-card {
-    border-left: 1px solid @barBorderColor;
-  }
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
 }
index c86e998a5181837ab071bbb624ece0f6579c04aa..b9447a88eb59c563212e038beae7ffb8065c5c83 100644 (file)
       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(1) -%>',
-        <% end %>
-        <% if @snapshot.measure('tests') %>
-        tests: '<%= @snapshot.measure('tests').variation(1) -%>',
-        <% end %>
-
-        // duplications
-        duplications: '<%= @snapshot.measure('duplicated_lines_density').variation(1) -%>',
-        duplicatedLines: '<%= @snapshot.measure('duplicated_lines').variation(1) -%>',
-        duplicatedBlocks: '<%= @snapshot.measure('duplicated_blocks').variation(1) -%>',
-
-        // size
-        lines: '<%= @snapshot.measure('lines').variation(1) -%>',
-        files: '<%= @snapshot.measure('files').variation(1) -%>'
-        <% end %>
-      };
-
       window.sonarqube.overview = {
         component: component,
-        gate: gate,
-        measures: measures,
-        leak: leak
+        gate: gate
       };
     })();
   </script>
diff --git a/server/sonar-web/tests/apps/overview-test.js b/server/sonar-web/tests/apps/overview-test.js
new file mode 100644 (file)
index 0000000..2ba57a1
--- /dev/null
@@ -0,0 +1,44 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import TestUtils from 'react-addons-test-utils';
+import { expect } from 'chai';
+
+import Gate from '../../src/main/js/apps/overview/gate/gate';
+import GateConditions from '../../src/main/js/apps/overview/gate/gate-conditions';
+import GateCondition from '../../src/main/js/apps/overview/gate/gate-condition';
+
+describe('Overview', function () {
+
+  describe('Quality Gate', function () {
+    it('should display a badge', function () {
+      let output = TestUtils.renderIntoDocument(<Gate gate={{ level: 'ERROR', conditions: [] }} component={{ }}/>);
+      TestUtils.findRenderedDOMComponentWithClass(output, 'badge-error');
+    });
+
+    it('should not be displayed', function () {
+      let output = TestUtils.renderIntoDocument(<Gate component={{ }}/>);
+      expect(TestUtils.scryRenderedDOMComponentsWithClass(output, 'overview-gate')).to.be.empty;
+    });
+
+    it('should display empty gate', function () {
+      let output = TestUtils.renderIntoDocument(<Gate component={{ qualifier: 'TRK' }}/>);
+      TestUtils.findRenderedDOMComponentWithClass(output, 'overview-gate');
+      TestUtils.findRenderedDOMComponentWithClass(output, 'overview-gate-warning');
+    });
+
+    it('should filter out passed conditions', function () {
+      const conditions = [
+        { level: 'OK' },
+        { level: 'ERROR', metric: { name: 'error metric' } },
+        { level: 'WARN', metric: { name: 'warn metric' } },
+        { level: 'OK' }
+      ];
+
+      let renderer = TestUtils.createRenderer();
+      renderer.render(<GateConditions gate={{ conditions }} component={{}}/>);
+      let output = renderer.getRenderOutput();
+      expect(output.props.children).to.have.length(2);
+    });
+  });
+
+});
index d414fc20436563dac530da55156ee65a8ee4566d..7097755d5db6d5d42bffb2bc858989379a3dd4e1 100644 (file)
@@ -3110,17 +3110,20 @@ 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.debt=Debt
 overview.metric.new_issues=New Issues
-overview.metric.lines=Lines
-overview.metric.files=Files
+overview.metric.new_debt=New Debt
+overview.metric.new_blocker_issues=New Blocker
+overview.metric.new_critical_issues=New Critical
+overview.metric.new_open_issues=New Open
 overview.metric.coverage=Coverage
+overview.metric.tests=Tests
+overview.metric.new_coverage=Coverage on New Code
+overview.metric.duplications=Duplications
+overview.metric.duplicated_blocks=Duplicated Blocks
+overview.metric.ncloc=Lines of Code
+overview.metric.files=Files
 
 overview.period.previous_version=since {0}
 overview.period.previous_analysis=since previous analysis