aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-03-30 13:42:42 +0200
committerStas Vilchik <vilchiks@gmail.com>2015-03-30 13:42:42 +0200
commitfcc90027f5b624b333f4e00045a04dacb7d67394 (patch)
treec82aadae3cc621a1832440a5bbb338966cc3ca50
parent3fa0d737fb0177414765ca800f0271b6859437d9 (diff)
downloadsonarqube-fcc90027f5b624b333f4e00045a04dacb7d67394.tar.gz
sonarqube-fcc90027f5b624b333f4e00045a04dacb7d67394.zip
SONAR-6331 add a project overview page
-rw-r--r--server/sonar-web/Gruntfile.coffee9
-rw-r--r--server/sonar-web/src/main/hbs/nav/nav-context-navbar.hbs8
-rw-r--r--server/sonar-web/src/main/hbs/overview/overview-coverage.hbs65
-rw-r--r--server/sonar-web/src/main/hbs/overview/overview-debt.hbs35
-rw-r--r--server/sonar-web/src/main/hbs/overview/overview-duplications.hbs34
-rw-r--r--server/sonar-web/src/main/hbs/overview/overview-gate.hbs22
-rw-r--r--server/sonar-web/src/main/hbs/overview/overview-issues.hbs40
-rw-r--r--server/sonar-web/src/main/hbs/overview/overview-layout.hbs6
-rw-r--r--server/sonar-web/src/main/hbs/overview/overview-size.hbs28
-rw-r--r--server/sonar-web/src/main/js/application.js141
-rw-r--r--server/sonar-web/src/main/js/common/handlebars-extensions.js239
-rw-r--r--server/sonar-web/src/main/js/graphics/sparkline.js101
-rw-r--r--server/sonar-web/src/main/js/graphics/timeline.js173
-rw-r--r--server/sonar-web/src/main/js/measures.js0
-rw-r--r--server/sonar-web/src/main/js/nav/context-navbar-view.js8
-rw-r--r--server/sonar-web/src/main/js/overview/app.js42
-rw-r--r--server/sonar-web/src/main/js/overview/layout.js47
-rw-r--r--server/sonar-web/src/main/js/overview/models/state.js357
-rw-r--r--server/sonar-web/src/main/js/overview/views/coverage-view.js39
-rw-r--r--server/sonar-web/src/main/js/overview/views/debt-view.js43
-rw-r--r--server/sonar-web/src/main/js/overview/views/duplications-view.js39
-rw-r--r--server/sonar-web/src/main/js/overview/views/gate-view.js43
-rw-r--r--server/sonar-web/src/main/js/overview/views/issues-view.js43
-rw-r--r--server/sonar-web/src/main/js/overview/views/size-view.js38
-rw-r--r--server/sonar-web/src/main/less/components/badges.less14
-rw-r--r--server/sonar-web/src/main/less/components/page.less4
-rw-r--r--server/sonar-web/src/main/less/components/panels.less5
-rw-r--r--server/sonar-web/src/main/less/init/lists.less1
-rw-r--r--server/sonar-web/src/main/less/init/misc.less4
-rw-r--r--server/sonar-web/src/main/less/pages.less1
-rw-r--r--server/sonar-web/src/main/less/pages/overview.less90
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb30
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb31
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/helpers/dashboard_helper.rb6
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb16
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties14
36 files changed, 1632 insertions, 184 deletions
diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee
index 906e0a034d3..2ad0c61f9b7 100644
--- a/server/sonar-web/Gruntfile.coffee
+++ b/server/sonar-web/Gruntfile.coffee
@@ -108,6 +108,7 @@ module.exports = (grunt) ->
'<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/treemap.js'
'<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/pie-chart.js'
'<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/timeline.js'
+ '<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/sparkline.js'
'<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/barchart.js'
'<%= grunt.option("assetsDir") || pkg.assets %>js/sortable.js'
'<%= grunt.option("assetsDir") || pkg.assets %>js/common/inputs.js'
@@ -145,6 +146,7 @@ module.exports = (grunt) ->
'<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/widget.js'
'<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/bubble-chart.js'
'<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/timeline.js'
+ '<%= grunt.option("assetsDir") || pkg.assets %>js/graphics/sparkline.js'
'<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/stack-area.js'
'<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/pie-chart.js'
'<%= grunt.option("assetsDir") || pkg.assets %>js/widgets/histogram.js'
@@ -250,6 +252,10 @@ module.exports = (grunt) ->
name: 'widgets/issue-filter'
out: '<%= grunt.option("assetsDir") || pkg.assets %>build/js/widgets/issue-filter.js'
+ overview: options:
+ name: 'overview/app'
+ out: '<%= grunt.option("assetsDir") || pkg.assets %>build/js/overview/app.js'
+
handlebars:
options:
@@ -309,6 +315,9 @@ module.exports = (grunt) ->
'<%= grunt.option("assetsDir") || pkg.assets %>js/templates/workspace.js': [
'<%= pkg.sources %>hbs/workspace/**/*.hbs'
]
+ '<%= grunt.option("assetsDir") || pkg.assets %>js/templates/overview.js': [
+ '<%= pkg.sources %>hbs/overview/**/*.hbs'
+ ]
clean:
diff --git a/server/sonar-web/src/main/hbs/nav/nav-context-navbar.hbs b/server/sonar-web/src/main/hbs/nav/nav-context-navbar.hbs
index b0b106f1fd6..5dcff71a941 100644
--- a/server/sonar-web/src/main/hbs/nav/nav-context-navbar.hbs
+++ b/server/sonar-web/src/main/hbs/nav/nav-context-navbar.hbs
@@ -21,8 +21,8 @@
</div>
<ul class="nav navbar-nav nav-tabs">
- <li {{#if isOverviewActive}}class="active"{{/if}}>
- <a href="{{dashboardUrl contextKey}}">{{t 'overview.page'}}</a>
+ <li {{#isActiveLink '/overview'}}class="active"{{/isActiveLink}}>
+ <a href="{{link '/overview/index?id=' contextKeyEncoded }}">{{t 'overview.page'}}</a>
</li>
<li {{#isActiveLink '/components'}}class="active"{{/isActiveLink}}>
<a href="{{link '/components/index/' contextId }}">{{t 'components.page'}}</a>
@@ -46,11 +46,11 @@
<a class="dropdown-toggle" data-toggle="dropdown" href="#">{{t 'more'}}&nbsp;<i class="icon-dropdown"></i></a>
<ul class="dropdown-menu">
<li class="dropdown-header">{{t 'layout.dashboards'}}</li>
- {{#withoutFirst contextDashboards}}
+ {{#each contextDashboards}}
<li>
<a href="{{link url}}">{{name}}</a>
</li>
- {{/withoutFirst}}
+ {{/each}}
{{#if canManageContextDashboards}}
<li class="small-divider"></li>
<li>
diff --git a/server/sonar-web/src/main/hbs/overview/overview-coverage.hbs b/server/sonar-web/src/main/hbs/overview/overview-coverage.hbs
new file mode 100644
index 00000000000..2f7b2ae8e8a
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/overview/overview-coverage.hbs
@@ -0,0 +1,65 @@
+<h6 class="note">{{t 'overview.coverage'}}</h6>
+
+<table class="width100">
+ <tr>
+ <td class="width-55">
+ <div class="overview-main-measure">
+ <a href="{{urlForDrilldown componentKey 'overall_coverage'}}">{{formatMeasure coverage 'PERCENT'}}</a>
+ </div>
+ <div class="overview-trend">
+ <div id="overview-coverage-trend" data-width="100" data-height="30" data-color="#4b9fd5"></div>
+ </div>
+ </td>
+ <td class="width-15">
+ {{#notNull coverage1}}
+ <a class="overview-measure" href="{{urlForDrilldown componentKey 'overall_coverage' 1}}">
+ {{formatMeasureVariation coverage1 'PERCENT'}}
+ </a>
+ <p class="note">{{period1Name}}</p>
+ {{/notNull}}
+ </td>
+ <td class="width-15">
+ {{#notNull coverage2}}
+ <a class="overview-measure" href="{{urlForDrilldown componentKey 'overall_coverage' 2}}">
+ {{formatMeasureVariation coverage2 'PERCENT'}}
+ </a>
+ <p class="note">{{period2Name}}</p>
+ {{/notNull}}
+ </td>
+ <td class="width-15">
+ {{#notNull coverage3}}
+ <a class="overview-measure" href="{{urlForDrilldown componentKey 'overall_coverage' 3}}">
+ {{formatMeasureVariation coverage3 'PERCENT'}}
+ </a>
+ <p class="note">{{period3Name}}</p>
+ {{/notNull}}
+ </td>
+ </tr>
+ <tr>
+ <td class="width-55"></td>
+ <td class="width-15">
+ {{#notNull newCoverage1}}
+ <a class="overview-measure" href="{{urlForDrilldown componentKey 'new_overall_coverage' 1}}">
+ {{formatMeasure newCoverage1 'PERCENT'}}
+ </a>
+ <p class="note">on new code</p>
+ {{/notNull}}
+ </td>
+ <td class="width-15">
+ {{#notNull newCoverage2}}
+ <a class="overview-measure spacer-top" href="{{urlForDrilldown componentKey 'new_overall_coverage' 2}}">
+ {{formatMeasure newCoverage2 'PERCENT'}}
+ </a>
+ <p class="note">on new code</p>
+ {{/notNull}}
+ </td>
+ <td class="width-15">
+ {{#notNull newCoverage3}}
+ <a class="overview-measure spacer-top" href="{{urlForDrilldown componentKey 'new_overall_coverage' 3}}">
+ {{formatMeasure newCoverage3 'PERCENT'}}
+ </a>
+ <p class="note">on new code</p>
+ {{/notNull}}
+ </td>
+ </tr>
+</table>
diff --git a/server/sonar-web/src/main/hbs/overview/overview-debt.hbs b/server/sonar-web/src/main/hbs/overview/overview-debt.hbs
new file mode 100644
index 00000000000..232cc2228fe
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/overview/overview-debt.hbs
@@ -0,0 +1,35 @@
+<h6 class="note">{{t 'overview.debt'}}</h6>
+
+<table class="width100">
+ <tr>
+ <td class="width-55">
+ <div class="overview-main-measure">
+ <a href="{{urlForDrilldown componentKey 'sqale_index'}}">{{formatMeasure debt 'WORK_DUR'}}</a>
+ </div>
+ <div class="overview-trend">
+ <div id="overview-debt-trend" data-width="100" data-height="30" data-color="#4b9fd5"></div>
+ </div>
+ </td>
+ <td class="width-15">
+ <a href="{{urlForDrilldown componentKey 'sqale_index' 1}}"
+ class="overview-measure {{#gt deb1 0}}text-danger{{/gt}}{{#lt debt1 0}}text-success{{/lt}}">
+ {{formatMeasureVariation debt1 'WORK_DUR'}}
+ </a>
+ <p class="note">{{period1Name}}</p>
+ </td>
+ <td class="width-15">
+ <a href="{{urlForDrilldown componentKey 'sqale_index' 2}}"
+ class="overview-measure {{#gt debt2 0}}text-danger{{/gt}}{{#lt debt2 0}}text-success{{/lt}}">
+ {{formatMeasureVariation debt2 'WORK_DUR'}}
+ </a>
+ <p class="note">{{period2Name}}</p>
+ </td>
+ <td class="width-15">
+ <a href="{{urlForDrilldown componentKey 'sqale_index' 3}}"
+ class="overview-measure {{#gt debt3 0}}text-danger{{/gt}}{{#lt debt3 0}}text-success{{/lt}}">
+ {{formatMeasureVariation debt3 'WORK_DUR'}}
+ </a>
+ <p class="note">{{period3Name}}</p>
+ </td>
+ </tr>
+</table>
diff --git a/server/sonar-web/src/main/hbs/overview/overview-duplications.hbs b/server/sonar-web/src/main/hbs/overview/overview-duplications.hbs
new file mode 100644
index 00000000000..2f670d35cc4
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/overview/overview-duplications.hbs
@@ -0,0 +1,34 @@
+<h6 class="note">{{t 'overview.duplications'}}</h6>
+
+<table class="width100">
+ <tr>
+ <td class="width-55">
+ <div class="overview-main-measure">
+ <a href="{{urlForDrilldown componentKey 'duplicated_lines_density'}}">
+ {{formatMeasure duplications 'PERCENT'}}
+ </a>
+ </div>
+ <div class="overview-trend">
+ <div id="overview-duplications-trend" data-width="100" data-height="30" data-color="#4b9fd5"></div>
+ </div>
+ </td>
+ <td class="width-15">
+ <a class="overview-measure" href="{{urlForDrilldown componentKey 'duplicated_lines_density' 1}}">
+ {{formatMeasureVariation duplications1 'PERCENT'}}
+ </a>
+ <p class="note">{{period1Name}}</p>
+ </td>
+ <td class="width-15">
+ <a class="overview-measure" href="{{urlForDrilldown componentKey 'duplicated_lines_density' 2}}">
+ {{formatMeasureVariation duplications2 'PERCENT'}}
+ </a>
+ <p class="note">{{period2Name}}</p>
+ </td>
+ <td class="width-15">
+ <a class="overview-measure" href="{{urlForDrilldown componentKey 'duplicated_lines_density' 3}}">
+ {{formatMeasureVariation duplications3 'PERCENT'}}
+ </a>
+ <p class="note">{{period3Name}}</p>
+ </td>
+ </tr>
+</table>
diff --git a/server/sonar-web/src/main/hbs/overview/overview-gate.hbs b/server/sonar-web/src/main/hbs/overview/overview-gate.hbs
new file mode 100644
index 00000000000..a43f972baac
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/overview/overview-gate.hbs
@@ -0,0 +1,22 @@
+<div class="text-center">
+ {{#notEmpty gateConditions}}
+ <ul class="list-inline spacer-top" style="display: inline-block;">
+ {{#each gateConditions}}
+ <li>
+ {{#canHaveDrilldownUrl metric period}}
+ <a href="{{urlForDrilldown ../../componentKey metric period periodDate}}"
+ class="overview-status overview-status-{{level}}"
+ title="{{#notEq level 'OK'}}{{t 'quality_gates.operator' op 'short'}} {{/notEq}}{{#eq level 'ERROR'}}{{formatMeasure error type}}{{/eq}}{{#eq level 'WARN'}}{{formatMeasure warning type}}{{/eq}}"
+ data-toggle="tooltip" data-placement="bottom">{{formatMeasure actual type}}</a>
+ {{else}}
+ <span class="overview-status overview-status-{{level}}"
+ title="{{#notEq level 'OK'}}{{t 'quality_gates.operator' op 'short'}} {{/notEq}}{{#eq level 'ERROR'}}{{formatMeasure error type}}{{/eq}}{{#eq level 'WARN'}}{{formatMeasure warning type}}{{/eq}}"
+ data-toggle="tooltip" data-placement="bottom">{{formatMeasure actual type}}</span>
+ {{/canHaveDrilldownUrl}}
+ <p class="note text-lowercase" style="padding-top: 4px;">{{t 'metric' metric 'name'}}</p>
+ <p class="note">{{default periodName period}}</p>
+ </li>
+ {{/each}}
+ </ul>
+ {{/notEmpty}}
+</div>
diff --git a/server/sonar-web/src/main/hbs/overview/overview-issues.hbs b/server/sonar-web/src/main/hbs/overview/overview-issues.hbs
new file mode 100644
index 00000000000..2aa91ce663c
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/overview/overview-issues.hbs
@@ -0,0 +1,40 @@
+<h6 class="note">{{t 'overview.issues'}}</h6>
+
+<table class="width100">
+ <tr>
+ <td class="width-55">
+ <div class="overview-main-measure">
+ <a href="{{urlForIssuesOverview componentKey}}">{{formatMeasure issues 'INT'}}</a>
+ </div>
+ <div class="overview-trend">
+ <div id="overview-issues-trend" data-width="100" data-height="30" data-color="#4b9fd5"></div>
+ </div>
+ </td>
+ <td class="width-15">
+ <a href="{{urlForIssuesOverview componentKey period1Date}}"
+ class="overview-measure {{#gt issues1 0}}text-danger{{else}}text-success{{/gt}}">
+ {{formatMeasureVariation issues1 'INT'}}
+ </a>
+ <span class="note">new</span>
+ <p class="note">{{period1Name}}</p>
+ </td>
+ <td class="width-15">
+ <div style="display: inline-block; vertical-align: middle;">
+ <a href="{{urlForIssuesOverview componentKey period2Date}}"
+ class="overview-measure {{#gt issues2 0}}text-danger{{else}}text-success{{/gt}}">
+ {{formatMeasureVariation issues2 'INT'}}
+ </a>
+ <span class="note">new</span>
+ <p class="note">{{period2Name}}</p>
+ </div>
+ </td>
+ <td class="width-15">
+ <a href="{{urlForIssuesOverview componentKey period3Date}}"
+ class="overview-measure {{#gt issues3 0}}text-danger{{else}}text-success{{/gt}}">
+ {{formatMeasureVariation issues3 'INT'}}
+ </a>
+ <span class="note">new</span>
+ <p class="note">{{period3Name}}</p>
+ </td>
+ </tr>
+</table>
diff --git a/server/sonar-web/src/main/hbs/overview/overview-layout.hbs b/server/sonar-web/src/main/hbs/overview/overview-layout.hbs
new file mode 100644
index 00000000000..46bf13084f5
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/overview/overview-layout.hbs
@@ -0,0 +1,6 @@
+<div class="overview-card" id="overview-gate"></div>
+<div class="overview-card" id="overview-size"></div>
+<div class="overview-card" id="overview-issues"></div>
+<div class="overview-card" id="overview-debt"></div>
+<div class="overview-card" id="overview-coverage"></div>
+<div class="overview-card" id="overview-duplications"></div>
diff --git a/server/sonar-web/src/main/hbs/overview/overview-size.hbs b/server/sonar-web/src/main/hbs/overview/overview-size.hbs
new file mode 100644
index 00000000000..f3e18a8ef6f
--- /dev/null
+++ b/server/sonar-web/src/main/hbs/overview/overview-size.hbs
@@ -0,0 +1,28 @@
+<h6 class="note">{{t 'overview.lines_of_code'}}</h6>
+
+<table class="width100">
+ <tr>
+ <td class="width-55">
+ <div>
+ <div class="overview-main-measure">
+ <a href="{{urlForDrilldown componentKey 'ncloc'}}">{{formatMeasure ncloc 'INT'}}</a>
+ </div>
+ <div class="overview-trend">
+ <div id="overview-size-trend" data-width="100" data-height="30" data-color="#4b9fd5"></div>
+ </div>
+ </div>
+ </td>
+ <td class="width-15">
+ <span class="overview-measure">{{formatMeasureVariation ncloc1 'INT'}}</span>
+ <p class="note">{{period1Name}}</p>
+ </td>
+ <td class="width-15">
+ <span class="overview-measure">{{formatMeasureVariation ncloc2 'INT'}}</span>
+ <p class="note">{{period2Name}}</p>
+ </td>
+ <td class="width-15">
+ <span class="overview-measure">{{formatMeasureVariation ncloc3 'INT'}}</span>
+ <p class="note">{{period3Name}}</p>
+ </td>
+ </tr>
+</table>
diff --git a/server/sonar-web/src/main/js/application.js b/server/sonar-web/src/main/js/application.js
index de273f721dd..24088b89b81 100644
--- a/server/sonar-web/src/main/js/application.js
+++ b/server/sonar-web/src/main/js/application.js
@@ -17,27 +17,29 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-function showMessage(div_id, message) {
+function showMessage (div_id, message) {
$j('#' + div_id + 'msg').html(message);
$j('#' + div_id).show();
}
-function error(message) {
+function error (message) {
showMessage('error', message);
}
-function warning(message) {
+function warning (message) {
showMessage('warning', message);
}
-function info(message) {
+function info (message) {
showMessage('info', message);
}
-function toggleFav(resourceId, elt) {
- $j.ajax({type: 'POST', dataType: 'json', url: baseUrl + '/favourites/toggle/' + resourceId,
+function toggleFav (resourceId, elt) {
+ $j.ajax({
+ type: 'POST', dataType: 'json', url: baseUrl + '/favourites/toggle/' + resourceId,
success: function (data) {
var star = $j(elt);
star.removeClass('icon-favorite icon-not-favorite');
star.addClass(data.css);
star.attr('title', data.title);
- }});
+ }
+ });
}
function dashboardParameters (urlHasSomething) {
@@ -68,7 +70,7 @@ function dashboardParameters (urlHasSomething) {
var treemaps = {};
-function treemapById(id) {
+function treemapById (id) {
return treemaps[id];
}
var TreemapContext = function (rid, label) {
@@ -163,7 +165,7 @@ Treemap.prototype.initNodes = function () {
});
};
-function openModalWindow(url, options) {
+function openModalWindow (url, options) {
var width = (options && options.width) || 540;
var $dialog = $j('#modal');
if (!$dialog.length) {
@@ -199,7 +201,7 @@ function openModalWindow(url, options) {
return this.each(function () {
var obj = $j(this);
var url = obj.attr('modal-url') || obj.attr('href');
- return openModalWindow(url, {'width': obj.attr('modal-width')});
+ return openModalWindow(url, { 'width': obj.attr('modal-width') });
});
},
modal: function () {
@@ -247,12 +249,12 @@ function openModalWindow(url, options) {
});
})(jQuery);
-function closeModalWindow() {
+function closeModalWindow () {
$j('#modal').dialog('close');
return false;
}
-function supportsHTML5Storage() {
+function supportsHTML5Storage () {
try {
return 'localStorage' in window && window.localStorage !== null;
} catch (e) {
@@ -262,7 +264,7 @@ function supportsHTML5Storage() {
//******************* HANDLING OF ACCORDION NAVIGATION [BEGIN] ******************* //
-function openAccordionItem(url) {
+function openAccordionItem (url) {
return $j.ajax({
url: url
}).fail(function (jqXHR, textStatus) {
@@ -305,11 +307,11 @@ var clickOnDropdownMenuLink = function (event) {
}
};
-function showDropdownMenu(menuId) {
+function showDropdownMenu (menuId) {
showDropdownMenuOnElement($j('#' + menuId));
}
-function showDropdownMenuOnElement(elt) {
+function showDropdownMenuOnElement (elt) {
var dropdownElt = $j(elt);
if (dropdownElt === currentlyDisplayedDropdownMenu) {
@@ -327,7 +329,7 @@ function showDropdownMenuOnElement(elt) {
//******************* HANDLING OF DROPDOWN MENUS [END] ******************* //
-function openPopup(url, popupId) {
+function openPopup (url, popupId) {
window.open(url, popupId, 'height=800,width=900,scrollbars=1,resizable=1');
return false;
}
@@ -366,6 +368,113 @@ function fileFromPath (path) {
}
+/*
+ * Measures
+ */
+
+(function () {
+
+ /**
+ * Format a work duration measure
+ * @param {number} value
+ * @returns {string}
+ */
+ var durationFormatter = function (value) {
+ if (value === 0) {
+ return '0';
+ }
+ var hoursInDay = window.SS.hoursInDay || 8,
+ isNegative = value < 0,
+ absValue = Math.abs(value);
+ var days = Math.floor(absValue / hoursInDay / 60);
+ var remainingValue = absValue - days * hoursInDay * 60;
+ var hours = Math.floor(remainingValue / 60);
+ remainingValue -= hours * 60;
+ var minutes = remainingValue;
+ var formatted = '';
+ if (days > 0) {
+ formatted += tp('work_duration.x_days', isNegative ? -1 * days : days);
+ }
+ if (hours > 0 && days < 10) {
+ if (formatted.length > 0) {
+ formatted += ' ';
+ }
+ formatted += tp('work_duration.x_hours', isNegative && formatted.length === 0 ? -1 * hours : hours);
+ }
+ if (minutes > 0 && hours < 10 && days === 0) {
+ if (formatted.length > 0) {
+ formatted += ' ';
+ }
+ formatted += tp('work_duration.x_minutes', isNegative && formatted.length === 0 ? -1 * minutes : minutes);
+ }
+ return formatted;
+ };
+
+ /**
+ * Format a work duration variation
+ * @param value
+ */
+ var durationVariationFormatter = function (value) {
+ if (value === 0) {
+ return '0';
+ }
+ var formatted = durationFormatter(value);
+ return formatted[0] !== '-' ? '+' + formatted : formatted;
+ };
+
+ /**
+ * Format a measure according to its type
+ * @param measure
+ * @param {string} type
+ * @returns {string|null}
+ */
+ window.formatMeasure = function (measure, type) {
+ var formatted = null,
+ formatters = {
+ 'INT': function (value) {
+ return numeral(value).format('0,0');
+ },
+ 'FLOAT': function (value) {
+ return numeral(value).format('0,0.0');
+ },
+ 'PERCENT': function (value) {
+ return numeral(+value / 100).format('0,0.0%');
+ },
+ 'WORK_DUR': durationFormatter
+ };
+ if (measure != null && type != null) {
+ formatted = formatters[type] != null ? formatters[type](measure) : measure;
+ }
+ return formatted;
+ };
+
+ /**
+ * Format a measure variation according to its type
+ * @param measure
+ * @param {string} type
+ * @returns {string|null}
+ */
+ window.formatMeasureVariation = function (measure, type) {
+ var formatted = null,
+ formatters = {
+ 'INT': function (value) {
+ return value === 0 ? '0' : numeral(value).format('+0,0');
+ },
+ 'FLOAT': function (value) {
+ return value === 0 ? '0' : numeral(value).format('+0,0.0');
+ },
+ 'PERCENT': function (value) {
+ return value === 0 ? '0%' : numeral(+value / 100).format('+0,0.0%');
+ },
+ 'WORK_DUR': durationVariationFormatter
+ };
+ if (measure != null && type != null) {
+ formatted = formatters[type] != null ? formatters[type](measure) : measure;
+ }
+ return formatted;
+ };
+})();
+
jQuery(function () {
// Process login link in order to add the anchor
diff --git a/server/sonar-web/src/main/js/common/handlebars-extensions.js b/server/sonar-web/src/main/js/common/handlebars-extensions.js
index a77e318c13e..d7e161325ee 100644
--- a/server/sonar-web/src/main/js/common/handlebars-extensions.js
+++ b/server/sonar-web/src/main/js/common/handlebars-extensions.js
@@ -20,17 +20,25 @@
(function () {
var defaultActions = ['comment', 'assign', 'assign_to_me', 'plan', 'set_severity', 'set_tags'];
- Handlebars.registerHelper('log', function() {
+ function isIssuesMetric (metric) {
+ var METRICS = ['violations', 'blocker_violations', 'critical_violations', 'major_violations', 'minor_violations',
+ 'info_violations', 'new_blocker_violations', 'new_critical_violations', 'new_major_violations',
+ 'new_minor_violations', 'new_info_violations', 'open_issues', 'reopened_issues', 'confirmed_issues',
+ 'false_positive_issues'];
+ return METRICS.indexOf(metric) !== -1;
+ }
+
+ Handlebars.registerHelper('log', function () {
var args = Array.prototype.slice.call(arguments, 0, -1);
console.log.apply(console, args);
});
- Handlebars.registerHelper('link', function() {
+ Handlebars.registerHelper('link', function () {
var url = Array.prototype.slice.call(arguments, 0, -1).join('');
return baseUrl + url;
});
- Handlebars.registerHelper('isActiveLink', function() {
+ Handlebars.registerHelper('isActiveLink', function () {
var args = Array.prototype.slice.call(arguments, 0, -1),
options = arguments[arguments.length - 1],
prefix = args.join(''),
@@ -39,29 +47,29 @@
return match ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('capitalize', function(string) {
+ Handlebars.registerHelper('capitalize', function (string) {
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
});
- Handlebars.registerHelper('severityIcon', function(severity) {
+ Handlebars.registerHelper('severityIcon', function (severity) {
return new Handlebars.SafeString(
'<i class="icon-severity-' + severity.toLowerCase() + '"></i>'
);
});
- Handlebars.registerHelper('severity', function(severity) {
+ Handlebars.registerHelper('severity', function (severity) {
return new Handlebars.SafeString(
- '<i class="icon-severity-' + severity.toLowerCase() + '"></i>&nbsp;' + t('severity', severity)
+ '<i class="icon-severity-' + severity.toLowerCase() + '"></i>&nbsp;' + t('severity', severity)
);
});
- Handlebars.registerHelper('statusIcon', function(status) {
+ Handlebars.registerHelper('statusIcon', function (status) {
return new Handlebars.SafeString(
'<i class="icon-status-' + status.toLowerCase() + '"></i>'
);
});
- Handlebars.registerHelper('statusHelper', function(status, resolution) {
+ Handlebars.registerHelper('statusHelper', function (status, resolution) {
var s = '<i class="icon-status-' + status.toLowerCase() + '"></i>&nbsp;' + t('issue.status', status);
if (resolution != null) {
s = s + '&nbsp;(' + t('issue.resolution', resolution) + ')';
@@ -69,41 +77,41 @@
return new Handlebars.SafeString(s);
});
- Handlebars.registerHelper('testStatusIcon', function(status) {
+ Handlebars.registerHelper('testStatusIcon', function (status) {
return new Handlebars.SafeString(
- '<i class="icon-test-status-' + status.toLowerCase() + '"></i>'
+ '<i class="icon-test-status-' + status.toLowerCase() + '"></i>'
);
});
- Handlebars.registerHelper('testStatusIconClass', function(status) {
+ Handlebars.registerHelper('testStatusIconClass', function (status) {
return new Handlebars.SafeString('' +
- 'icon-test-status-' + status.toLowerCase()
+ 'icon-test-status-' + status.toLowerCase()
);
});
- Handlebars.registerHelper('alertIconClass', function(alert) {
+ Handlebars.registerHelper('alertIconClass', function (alert) {
return new Handlebars.SafeString(
'icon-alert-' + alert.toLowerCase()
);
});
- Handlebars.registerHelper('qualifierIcon', function(qualifier) {
+ Handlebars.registerHelper('qualifierIcon', function (qualifier) {
return new Handlebars.SafeString(
- qualifier ? '<i class="icon-qualifier-' + qualifier.toLowerCase() + '"></i>': ''
+ qualifier ? '<i class="icon-qualifier-' + qualifier.toLowerCase() + '"></i>' : ''
);
});
- Handlebars.registerHelper('default', function() {
+ Handlebars.registerHelper('default', function () {
var args = Array.prototype.slice.call(arguments, 0, -1);
- return args.reduce(function(prev, current) {
+ return args.reduce(function (prev, current) {
return prev != null ? prev : current;
}, null);
});
- Handlebars.registerHelper('show', function() {
+ Handlebars.registerHelper('show', function () {
var args = Array.prototype.slice.call(arguments),
ret = null;
- args.forEach(function(arg) {
+ args.forEach(function (arg) {
if (typeof arg === 'string' && ret == null) {
ret = arg;
}
@@ -111,7 +119,7 @@
return ret || '';
});
- Handlebars.registerHelper('percent', function(value, total) {
+ Handlebars.registerHelper('percent', function (value, total) {
if (total > 0) {
return '' + ((value || 0) / total * 100) + '%';
} else {
@@ -158,53 +166,57 @@
return ret;
});
- Handlebars.registerHelper('eq', function(v1, v2, options) {
+ Handlebars.registerHelper('eq', function (v1, v2, options) {
// use `==` instead of `===` to ignore types
return v1 == v2 ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('notEq', function(v1, v2, options) {
+ Handlebars.registerHelper('notEq', function (v1, v2, options) {
// use `==` instead of `===` to ignore types
return v1 != v2 ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('gt', function(v1, v2, options) {
+ Handlebars.registerHelper('gt', function (v1, v2, options) {
return v1 > v2 ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('notNull', function(value, options) {
+ Handlebars.registerHelper('lt', function (v1, v2, options) {
+ return v1 < v2 ? options.fn(this) : options.inverse(this);
+ });
+
+ Handlebars.registerHelper('notNull', function (value, options) {
return value != null ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('notEmpty', function(array, options) {
+ Handlebars.registerHelper('notEmpty', function (array, options) {
var cond = _.isArray(array) && array.length > 0;
return cond ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('empty', function(array, options) {
+ Handlebars.registerHelper('empty', function (array, options) {
var cond = _.isArray(array) && array.length > 0;
return cond ? options.inverse(this) : options.fn(this);
});
- Handlebars.registerHelper('all', function() {
+ Handlebars.registerHelper('all', function () {
var args = Array.prototype.slice.call(arguments, 0, -1),
options = arguments[arguments.length - 1],
- all = args.reduce(function(prev, current) {
+ all = args.reduce(function (prev, current) {
return prev && current;
}, true);
return all ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('any', function() {
+ Handlebars.registerHelper('any', function () {
var args = Array.prototype.slice.call(arguments, 0, -1),
options = arguments[arguments.length - 1],
- any = args.reduce(function(prev, current) {
+ any = args.reduce(function (prev, current) {
return prev || current;
}, false);
return any ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('inArray', function(array, element, options) {
+ Handlebars.registerHelper('inArray', function (array, element, options) {
if (_.isArray(array)) {
if (array.indexOf(element) !== -1) {
return options.fn(this);
@@ -214,20 +226,20 @@
}
});
- Handlebars.registerHelper('ifNotEmpty', function() {
+ Handlebars.registerHelper('ifNotEmpty', function () {
var args = Array.prototype.slice.call(arguments, 0, -1),
options = arguments[arguments.length - 1],
- notEmpty = args.reduce(function(prev, current) {
+ notEmpty = args.reduce(function (prev, current) {
return prev || (current && current.length > 0);
}, false);
return notEmpty ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('join', function(array, separator) {
+ Handlebars.registerHelper('join', function (array, separator) {
return array.join(separator);
});
- Handlebars.registerHelper('eachReverse', function(array, options) {
+ Handlebars.registerHelper('eachReverse', function (array, options) {
var ret = '';
if (array && array.length > 0) {
@@ -241,7 +253,7 @@
return ret;
});
- Handlebars.registerHelper('joinEach', function(array, separator, options) {
+ Handlebars.registerHelper('joinEach', function (array, separator, options) {
var ret = '';
if (array && array.length > 0) {
@@ -258,14 +270,14 @@
return ret;
});
- Handlebars.registerHelper('sum', function(a, b) {
+ Handlebars.registerHelper('sum', function (a, b) {
var args = Array.prototype.slice.call(arguments, 0, -1);
return args.reduce(function (p, c) {
return p + +c;
}, 0);
});
- Handlebars.registerHelper('dashboardUrl', function(componentKey, componentQualifier) {
+ Handlebars.registerHelper('dashboardUrl', function (componentKey, componentQualifier) {
var url = baseUrl + '/dashboard/index?id=' + encodeURIComponent(componentKey);
if (componentQualifier === 'FIL' || componentQualifier === 'CLA') {
url += '&metric=sqale_index';
@@ -273,57 +285,57 @@
return url;
});
- Handlebars.registerHelper('translate', function() {
+ Handlebars.registerHelper('translate', function () {
var args = Array.prototype.slice.call(arguments, 0, -1);
return window.translate.apply(this, args);
});
- Handlebars.registerHelper('t', function() {
+ Handlebars.registerHelper('t', function () {
var args = Array.prototype.slice.call(arguments, 0, -1);
return window.t.apply(this, args);
});
- Handlebars.registerHelper('tp', function() {
+ Handlebars.registerHelper('tp', function () {
var args = Array.prototype.slice.call(arguments, 0, -1);
return window.tp.apply(this, args);
});
- Handlebars.registerHelper('d', function(date) {
+ Handlebars.registerHelper('d', function (date) {
return moment(date).format('LL');
});
- Handlebars.registerHelper('dt', function(date) {
+ Handlebars.registerHelper('dt', function (date) {
return moment(date).format('LLL');
});
- Handlebars.registerHelper('ds', function(date) {
+ Handlebars.registerHelper('ds', function (date) {
return moment(date).format('YYYY-MM-DD');
});
- Handlebars.registerHelper('fromNow', function(date) {
+ Handlebars.registerHelper('fromNow', function (date) {
return moment(date).fromNow();
});
- Handlebars.registerHelper('durationFromNow', function(date, units) {
+ Handlebars.registerHelper('durationFromNow', function (date, units) {
return moment(new Date()).diff(date, units);
});
- Handlebars.registerHelper('numberShort', function(number) {
+ Handlebars.registerHelper('numberShort', function (number) {
if (number > 9999) {
return numeral(number).format('0.[0]a');
} else {
- return number;
+ return numeral(number).format('0,0');
}
});
- Handlebars.registerHelper('pluginActions', function(actions, options) {
+ Handlebars.registerHelper('pluginActions', function (actions, options) {
var pluginActions = _.difference(actions, defaultActions);
- return pluginActions.reduce(function(prev, current) {
+ return pluginActions.reduce(function (prev, current) {
return prev + options.fn(current);
}, '');
});
- Handlebars.registerHelper('ifHasExtraActions', function(actions, options) {
+ Handlebars.registerHelper('ifHasExtraActions', function (actions, options) {
var actionsLeft = _.difference(actions, defaultActions);
if (actionsLeft.length > 0) {
return options.fn(this);
@@ -332,7 +344,7 @@
}
});
- Handlebars.registerHelper('withFirst', function(list, options) {
+ Handlebars.registerHelper('withFirst', function (list, options) {
if (list && list.length > 0) {
return options.fn(list[0]);
} else {
@@ -340,7 +352,7 @@
}
});
- Handlebars.registerHelper('withLast', function(list, options) {
+ Handlebars.registerHelper('withLast', function (list, options) {
if (list && list.length > 0) {
return options.fn(list[list.length - 1]);
} else {
@@ -348,9 +360,9 @@
}
});
- Handlebars.registerHelper('withoutFirst', function(list, options) {
+ Handlebars.registerHelper('withoutFirst', function (list, options) {
if (list && list.length > 1) {
- return list.slice(1).reduce(function(prev, current) {
+ return list.slice(1).reduce(function (prev, current) {
return prev + options.fn(current);
}, '');
} else {
@@ -359,27 +371,27 @@
});
var audaciousFn;
- Handlebars.registerHelper('recursive', function(children, options) {
+ Handlebars.registerHelper('recursive', function (children, options) {
var out = '';
if (options.fn !== undefined) {
audaciousFn = options.fn;
}
- children.forEach(function(child){
+ children.forEach(function (child) {
out = out + audaciousFn(child);
});
return out;
});
- Handlebars.registerHelper('sources', function(source, scm, options) {
+ Handlebars.registerHelper('sources', function (source, scm, options) {
if (options == null) {
options = scm;
scm = null;
}
- var sources = _.map(source, function(code, line) {
+ var sources = _.map(source, function (code, line) {
return {
lineNumber: line,
code: code,
@@ -387,20 +399,20 @@
};
});
- return sources.reduce(function(prev, current, index) {
+ return sources.reduce(function (prev, current, index) {
return prev + options.fn(_.extend({ first: index === 0 }, current));
}, '');
});
- Handlebars.registerHelper('operators', function(metricType, options) {
+ Handlebars.registerHelper('operators', function (metricType, options) {
var ops = ['LT', 'GT', 'EQ', 'NE'];
- return ops.reduce(function(prev, current) {
+ return ops.reduce(function (prev, current) {
return prev + options.fn(current);
}, '');
});
- Handlebars.registerHelper('changelog', function(diff) {
+ Handlebars.registerHelper('changelog', function (diff) {
var message = '';
if (diff.newValue != null) {
message = tp('issue.changelog.changed_to', t('issue.changelog.field', diff.key), diff.newValue);
@@ -415,7 +427,7 @@
return message;
});
- Handlebars.registerHelper('ifMeasureShouldBeShown', function(measure, period, options) {
+ Handlebars.registerHelper('ifMeasureShouldBeShown', function (measure, period, options) {
if (measure != null || period != null) {
return options.fn(this);
} else {
@@ -423,18 +435,18 @@
}
});
- Handlebars.registerHelper('ifSCMChanged', function(source, line, options) {
+ Handlebars.registerHelper('ifSCMChanged', function (source, line, options) {
var currentLine = _.findWhere(source, { lineNumber: line }),
prevLine = _.findWhere(source, { lineNumber: line - 1 }),
changed = true;
if (currentLine && prevLine && currentLine.scm && prevLine.scm) {
changed = (currentLine.scm.author !== prevLine.scm.author) ||
- (currentLine.scm.date !== prevLine.scm.date) || (!prevLine.show);
+ (currentLine.scm.date !== prevLine.scm.date) || (!prevLine.show);
}
return changed ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('ifSCMChanged2', function(source, line, options) {
+ Handlebars.registerHelper('ifSCMChanged2', function (source, line, options) {
var currentLine = _.findWhere(source, { line: line }),
prevLine = _.findWhere(source, { line: line - 1 }),
changed = true;
@@ -444,7 +456,7 @@
return changed ? options.fn(this) : options.inverse(this);
});
- Handlebars.registerHelper('ifTestData', function(test, options) {
+ Handlebars.registerHelper('ifTestData', function (test, options) {
if ((test.status !== 'OK') || ((test.status === 'OK') && test.coveredLines)) {
return options.fn(this);
} else {
@@ -496,4 +508,93 @@
return str.length > LIMIT ? str.substr(0, LIMIT) + '...' : str;
});
+ Handlebars.registerHelper('withSign', function (number) {
+ return number >= 0 ? '+' + number : number;
+ });
+
+ Handlebars.registerHelper('formatMeasure', function (measure, type) {
+ return window.formatMeasure(measure, type);
+ });
+
+ Handlebars.registerHelper('formatMeasureVariation', function (measure, type) {
+ return window.formatMeasureVariation(measure, type);
+ });
+
+ Handlebars.registerHelper('urlForDrilldown', function (component, metric, period, periodDate) {
+
+ function buildIssuesUrl (component, metric, periodDate) {
+ var url = baseUrl + '/component_issues/index?id=' + encodeURIComponent(component) + '#';
+ if (periodDate != null) {
+ url += 'createdAfter=' + encodeURIComponent(periodDate) + '|';
+ }
+ switch (metric) {
+ case 'blocker_violations':
+ case 'new_blocker_violations':
+ url += 'resolved=false|severities=BLOCKER';
+ break;
+ case 'critical_violations':
+ case 'new_critical_violations':
+ url += 'resolved=false|severities=CRITICAL';
+ break;
+ case 'major_violations':
+ case 'new_major_violations':
+ url += 'resolved=false|severities=MAJOR';
+ break;
+ case 'minor_violations':
+ case 'new_minor_violations':
+ url += 'resolved=false|severities=MINOR';
+ break;
+ case 'info_violations':
+ case 'new_info_violations':
+ url += 'resolved=false|severities=INFO';
+ break;
+ case 'open_issues':
+ url += 'resolved=false|statuses=OPEN';
+ break;
+ case 'reopened_issues':
+ url += 'resolved=false|statuses=REOPENED';
+ break;
+ case 'confirmed_issues':
+ url += 'resolved=false|statuses=CONFIRMED';
+ break;
+ case 'false_positive_issues':
+ url += 'resolutions=FALSE-POSITIVE';
+ break;
+ default:
+ url += 'resolved=false';
+ }
+ return url;
+ }
+
+ var url;
+ if (isIssuesMetric(metric)) {
+ url = buildIssuesUrl(component, metric, periodDate);
+ } else {
+ if (metric === 'package_cycles') {
+ url = baseUrl + '/design/index?id=' + encodeURIComponent(component);
+ } else {
+ url = baseUrl + '/drilldown/measures?id=' + encodeURIComponent(component) + '&metric=' + metric;
+ if (period != null && !_.isObject(period)) {
+ url += '&period=' + period;
+ }
+ }
+ }
+ return url;
+ });
+
+ Handlebars.registerHelper('canHaveDrilldownUrl', function (metric, period, options) {
+ var isDifferentialMetric = metric.indexOf('new_') === 0,
+ _isIssuesMetric = isIssuesMetric(metric),
+ r = isDifferentialMetric || period == null || _isIssuesMetric;
+ return r ? options.fn(this) : options.inverse(this);
+ });
+
+ Handlebars.registerHelper('urlForIssuesOverview', function (componentKey, periodDate) {
+ var url = baseUrl + '/component_issues/index?id=' + encodeURIComponent(componentKey) + '#resolved=false';
+ if (typeof periodDate === 'string') {
+ url += '|createdAfter=' + encodeURIComponent(periodDate);
+ }
+ return url;
+ });
+
})();
diff --git a/server/sonar-web/src/main/js/graphics/sparkline.js b/server/sonar-web/src/main/js/graphics/sparkline.js
new file mode 100644
index 00000000000..c4c22920271
--- /dev/null
+++ b/server/sonar-web/src/main/js/graphics/sparkline.js
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+(function ($) {
+
+ function trans (left, top) {
+ return 'translate(' + left + ', ' + top + ')';
+ }
+
+ var defaults = {
+ height: 30,
+ color: '#1f77b4',
+ interpolate: 'bundle',
+ tension: 1
+ };
+
+ /*
+ * data = [
+ * { val: '2015-01-30', count: 30 },
+ * ...
+ * ]
+ */
+
+ $.fn.sparkline = function (data, opts) {
+ $(this).each(function () {
+ var options = _.defaults(opts || {}, $(this).data(), defaults);
+ if (!options.width) {
+ _.extend(options, { width: $(this).width() });
+ }
+
+ var container = d3.select(this),
+ svg = container.append('svg')
+ .attr('width', options.width + 1)
+ .attr('height', options.height + 1)
+ .classed('sonar-d3', true),
+
+ plot = svg.append('g')
+ .classed('plot', true),
+
+ xScale = d3.time.scale()
+ .domain(d3.extent(data, function (d) {
+ return new Date(d.val);
+ })),
+
+ yScale = d3.scale.linear()
+ .domain(d3.extent(data, function (d) {
+ return d.count;
+ })),
+
+ line = d3.svg.line()
+ .x(function (d) {
+ return xScale(new Date(d.val));
+ })
+ .y(function (d) {
+ return yScale(d.count);
+ })
+ .interpolate(options.interpolate)
+ .tension(options.tension);
+
+ _.extend(options, {
+ marginLeft: 1,
+ marginRight: 1,
+ marginTop: 6,
+ marginBottom: 6
+ });
+
+ _.extend(options, {
+ availableWidth: options.width - options.marginLeft - options.marginRight,
+ availableHeight: options.height - options.marginTop - options.marginBottom
+ });
+
+ plot.attr('transform', trans(options.marginLeft, options.marginTop));
+ xScale.range([0, options.availableWidth]);
+ yScale.range([options.availableHeight, 0]);
+
+ plot.append('path')
+ .datum(data)
+ .attr('d', line)
+ .classed('line', true)
+ .style('stroke', options.color);
+ }
+ );
+ };
+
+})(window.jQuery);
diff --git a/server/sonar-web/src/main/js/graphics/timeline.js b/server/sonar-web/src/main/js/graphics/timeline.js
index 145b119719b..ef9fd2b0273 100644
--- a/server/sonar-web/src/main/js/graphics/timeline.js
+++ b/server/sonar-web/src/main/js/graphics/timeline.js
@@ -26,7 +26,8 @@
var defaults = {
height: 140,
color: '#1f77b4',
- interpolate: 'basis'
+ interpolate: 'basis',
+ type: 'UNKNOWN'
};
/*
@@ -36,82 +37,100 @@
* ]
*/
- $.fn.timeline = function (data) {
+ $.fn.timeline = function (data, opts) {
$(this).each(function () {
- var options = _.defaults($(this).data(), defaults);
- _.extend(options, { width: $(this).width() });
-
- var container = d3.select(this),
- svg = container.append('svg')
- .attr('width', options.width + 2)
- .attr('height', options.height + 2)
- .classed('sonar-d3', true),
-
- plot = svg.append('g')
- .classed('plot', true),
-
- xScale = d3.time.scale()
- .domain(d3.extent(data, function (d) {
- return new Date(d.val);
- })),
-
- yScale = d3.scale.linear()
- .domain([
- 0, d3.max(data, function (d) {
- return d.count;
- })
- ]),
-
- line = d3.svg.line()
- .x(function (d) {
- return xScale(new Date(d.val));
- })
- .y(function (d) {
- return yScale(d.count);
- })
- .interpolate(options.interpolate),
-
- minDate = xScale.domain()[0],
- minDateTick = svg.append('text')
- .classed('subtitle', true)
- .text(moment(minDate).format('LL')),
-
- maxDate = xScale.domain()[1],
- maxDateTick = svg.append('text')
- .classed('subtitle', true)
- .text(moment(maxDate).format('LL'))
- .style('text-anchor', 'end');
-
- _.extend(options, {
- marginLeft: 1,
- marginRight: 1,
- marginTop: 1,
- marginBottom: 1 + maxDateTick.node().getBBox().height
- });
-
- _.extend(options, {
- availableWidth: options.width - options.marginLeft - options.marginRight,
- availableHeight: options.height - options.marginTop - options.marginBottom
- });
-
- plot.attr('transform', trans(options.marginLeft, options.marginTop));
- xScale.range([0, options.availableWidth]);
- yScale.range([0, options.availableHeight]);
-
- minDateTick
- .attr('x', options.marginLeft)
- .attr('y', options.height);
- maxDateTick
- .attr('x', options.width - options.marginRight)
- .attr('y', options.height);
-
- plot.append('path')
- .datum(data)
- .attr('d', line)
- .attr('class', 'line')
- .style('stroke', options.color);
-
- });
+ var options = _.defaults(opts || {}, $(this).data(), defaults);
+ _.extend(options, { width: $(this).width() });
+
+ var container = d3.select(this),
+ svg = container.append('svg')
+ .attr('width', options.width + 12)
+ .attr('height', options.height + 12)
+ .classed('sonar-d3', true),
+
+ extra = svg.append('g'),
+
+ plot = svg.append('g')
+ .classed('plot', true),
+
+ xScale = d3.time.scale()
+ .domain(d3.extent(data, function (d) {
+ return new Date(d.val);
+ })),
+
+ yScale = d3.scale.linear()
+ .domain(d3.extent(data, function (d) {
+ return d.count;
+ })),
+
+ line = d3.svg.line()
+ .x(function (d) {
+ return xScale(new Date(d.val));
+ })
+ .y(function (d) {
+ return yScale(d.count);
+ })
+ .interpolate(options.interpolate);
+
+ // Medians
+ var medianValue = getNiceMedian(0.5, data, function (d) {
+ return d.count;
+ }),
+ medianLabel = extra.append('text')
+ .text(window.formatMeasure(medianValue, options.type))
+ .style('text-anchor', 'end')
+ .style('font-size', '10px')
+ .style('fill', '#ccc')
+ .attr('dy', '0.32em'),
+ medianLabelWidth = medianLabel.node().getBBox().width;
+
+ _.extend(options, {
+ marginLeft: 1,
+ marginRight: 1 + medianLabelWidth + 4,
+ marginTop: 6,
+ marginBottom: 6
+ });
+
+ _.extend(options, {
+ availableWidth: options.width - options.marginLeft - options.marginRight,
+ availableHeight: options.height - options.marginTop - options.marginBottom
+ });
+
+ plot.attr('transform', trans(options.marginLeft, options.marginTop));
+ xScale.range([0, options.availableWidth]);
+ yScale.range([options.availableHeight, 0]);
+
+ plot.append('path')
+ .datum(data)
+ .attr('d', line)
+ .classed('line', true)
+ .style('stroke', options.color);
+
+ medianLabel
+ .attr('x', options.width - 1)
+ .attr('y', options.marginTop + yScale(medianValue));
+ extra.append('line')
+ .attr('x1', options.marginLeft)
+ .attr('y1', options.marginTop + yScale(medianValue))
+ .attr('x2', options.availableWidth + options.marginLeft)
+ .attr('y2', options.marginTop + yScale(medianValue))
+ .style('stroke', '#eee')
+ .style('shape-rendering', 'crispedges');
+ }
+ )
+ ;
};
-})(window.jQuery);
+ function getNiceMedian (p, array, accessor) {
+ var min = d3.min(array, accessor),
+ max = d3.max(array, accessor),
+ median = d3.median(array, accessor),
+ threshold = (max - min) / 2,
+ threshold10 = Math.pow(10, Math.floor(Math.log(threshold) / Math.LN10) - 1);
+ return (p - 0.5) > 0.0001 ?
+ Math.floor(median / threshold10) * threshold10 :
+ Math.ceil(median / threshold10) * threshold10;
+ }
+
+})
+(window.jQuery);
diff --git a/server/sonar-web/src/main/js/measures.js b/server/sonar-web/src/main/js/measures.js
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/server/sonar-web/src/main/js/measures.js
diff --git a/server/sonar-web/src/main/js/nav/context-navbar-view.js b/server/sonar-web/src/main/js/nav/context-navbar-view.js
index 6107a4276a2..7c8d3e911c9 100644
--- a/server/sonar-web/src/main/js/nav/context-navbar-view.js
+++ b/server/sonar-web/src/main/js/nav/context-navbar-view.js
@@ -23,7 +23,7 @@ define([
var $ = jQuery,
OVERVIEW_URLS = [
- '/design', '/libraries', '/dashboards'
+ '/design', '/libraries', '/dashboards', '/dashboard'
],
SETTINGS_URLS = [
'/project/settings', '/project/profile', '/project/qualitygate', '/manual_measures/index',
@@ -63,17 +63,15 @@ define([
search = window.location.search,
isMoreActive = _.some(OVERVIEW_URLS, function (url) {
return href.indexOf(url) !== -1;
- }) || (href.indexOf('/dashboard') !== -1 && search.indexOf('did=') !== -1),
+ });
isSettingsActive = _.some(SETTINGS_URLS, function (url) {
return href.indexOf(url) !== -1;
- }),
- isOverviewActive = !isMoreActive && href.indexOf('/dashboard') !== -1 && search.indexOf('did=') === -1;
+ });
return _.extend(Marionette.Layout.prototype.serializeData.apply(this, arguments), {
canManageContextDashboards: !!window.SS.user,
contextKeyEncoded: encodeURIComponent(this.model.get('contextKey')),
- isOverviewActive: isOverviewActive,
isSettingsActive: isSettingsActive,
isMoreActive: isMoreActive
});
diff --git a/server/sonar-web/src/main/js/overview/app.js b/server/sonar-web/src/main/js/overview/app.js
new file mode 100644
index 00000000000..425ee384c9f
--- /dev/null
+++ b/server/sonar-web/src/main/js/overview/app.js
@@ -0,0 +1,42 @@
+requirejs([
+ 'overview/layout',
+ 'overview/models/state',
+ 'overview/views/gate-view',
+ 'overview/views/size-view',
+ 'overview/views/issues-view',
+ 'overview/views/debt-view',
+ 'overview/views/coverage-view',
+ 'overview/views/duplications-view'
+], function (Layout,
+ State,
+ GateView,
+ SizeView,
+ IssuesView,
+ DebtView,
+ CoverageView,
+ DuplicationsView) {
+
+ var $ = jQuery,
+ App = new Marionette.Application();
+
+ App.addInitializer(function () {
+ $('body').addClass('dashboard-page');
+ this.state = new State(window.overviewConf);
+ this.layout = new Layout({
+ el: '.overview',
+ model: this.state
+ }).render();
+ this.layout.gateRegion.show(new GateView({ model: this.state }));
+ this.layout.sizeRegion.show(new SizeView({ model: this.state }));
+ this.layout.issuesRegion.show(new IssuesView({ model: this.state }));
+ this.layout.debtRegion.show(new DebtView({ model: this.state }));
+ this.layout.coverageRegion.show(new CoverageView({ model: this.state }));
+ this.layout.duplicationsRegion.show(new DuplicationsView({ model: this.state }));
+ this.state.fetch();
+ });
+
+ window.requestMessages().done(function () {
+ App.start();
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/overview/layout.js b/server/sonar-web/src/main/js/overview/layout.js
new file mode 100644
index 00000000000..adca32d5261
--- /dev/null
+++ b/server/sonar-web/src/main/js/overview/layout.js
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+define([
+ 'templates/overview'
+], function () {
+
+ return Marionette.Layout.extend({
+ template: Templates['overview-layout'],
+
+ regions: {
+ gateRegion: '#overview-gate',
+ sizeRegion: '#overview-size',
+ issuesRegion: '#overview-issues',
+ debtRegion: '#overview-debt',
+ coverageRegion: '#overview-coverage',
+ duplicationsRegion: '#overview-duplications'
+ },
+
+ modelEvents: {
+ 'change:gateConditions': 'toggleGate'
+ },
+
+ toggleGate: function () {
+ var conditions = this.model.get('gateConditions'),
+ hasGate = _.isArray(conditions) && conditions.length > 0;
+ this.$(this.gateRegion.el).toggle(hasGate);
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/overview/models/state.js b/server/sonar-web/src/main/js/overview/models/state.js
new file mode 100644
index 00000000000..750670a32db
--- /dev/null
+++ b/server/sonar-web/src/main/js/overview/models/state.js
@@ -0,0 +1,357 @@
+define(function () {
+
+ var $ = jQuery;
+
+ return Backbone.Model.extend({
+ defaults: function () {
+ return {
+ qualityGateStatus: 'ERROR'
+ };
+ },
+
+ fetch: function () {
+ return $.when(
+ this.fetchGate(),
+
+ this.fetchSize(),
+ this.fetchSizeTrend(),
+
+ this.fetchIssues(),
+ this.fetchIssues1(),
+ this.fetchIssues2(),
+ this.fetchIssues3(),
+ this.fetchIssuesTrend(),
+
+ this.fetchDebt(),
+ this.fetchDebtTrend(),
+
+ this.fetchCoverage(),
+ this.fetchCoverageTrend(),
+
+ this.fetchDuplications(),
+ this.fetchDuplicationsTrend()
+ );
+ },
+
+ fetchGate: function () {
+ var that = this,
+ url = baseUrl + '/api/resources/index',
+ options = {
+ resource: this.get('componentKey'),
+ metrics: 'quality_gate_details'
+ };
+ return $.get(url, options).done(function (r) {
+ var gateData = JSON.parse(r[0].msr[0].data),
+ gateConditions = gateData.conditions,
+ urlMetrics = baseUrl + '/api/metrics';
+ $.get(urlMetrics).done(function (r) {
+ var gateConditionsWithMetric = gateConditions.map(function (c) {
+ var metric = _.findWhere(r, { key: c.metric }),
+ type = metric != null ? metric.val_type : null,
+ periodDate = that.get('period' + c.period + 'Date'),
+ periodName = that.get('period' + c.period + 'Name');
+ return _.extend(c, {
+ type: type,
+ periodName: periodName,
+ periodDate: periodDate
+ });
+ });
+ that.set({
+ gateStatus: gateData.level,
+ gateConditions: gateConditionsWithMetric
+ });
+ });
+ });
+ },
+
+ fetchSize: function () {
+ var that = this,
+ url = baseUrl + '/api/resources/index',
+ options = {
+ resource: this.get('componentKey'),
+ metrics: 'ncloc,ncloc_language_distribution,function_complexity,file_complexity',
+ includetrends: true
+ };
+ return $.get(url, options).done(function (r) {
+ var msr = r[0].msr,
+ nclocMeasure = _.findWhere(msr, { key: 'ncloc' }),
+ nclocLangMeasure = _.findWhere(msr, { key: 'ncloc_language_distribution' }),
+ nclocLangParsed = nclocLangMeasure.data.split(';').map(function (token) {
+ var tokens = token.split('=');
+ return { key: tokens[0], value: +tokens[1] };
+ }),
+ nclocLangSorted = _.sortBy(nclocLangParsed, function (item) {
+ return -item.value;
+ }),
+ nclocLang = _.first(nclocLangSorted, 2),
+ functionComplexityMeasure = _.findWhere(msr, { key: 'function_complexity' }),
+ fileComplexityMeasure = _.findWhere(msr, { key: 'file_complexity' });
+ that.set({
+ ncloc: nclocMeasure.val,
+ ncloc1: nclocMeasure.var1,
+ ncloc2: nclocMeasure.var2,
+ ncloc3: nclocMeasure.var3,
+ nclocLang: nclocLang,
+
+ functionComplexity: functionComplexityMeasure.val,
+ functionComplexity1: functionComplexityMeasure.var1,
+ functionComplexity2: functionComplexityMeasure.var2,
+ functionComplexity3: functionComplexityMeasure.var3,
+
+ fileComplexity: fileComplexityMeasure.val,
+ fileComplexity1: fileComplexityMeasure.var1,
+ fileComplexity2: fileComplexityMeasure.var2,
+ fileComplexity3: fileComplexityMeasure.var3
+ });
+ });
+ },
+
+ fetchSizeTrend: function () {
+ var that = this,
+ url = baseUrl + '/api/timemachine/index',
+ options = {
+ resource: this.get('componentKey'),
+ metrics: 'ncloc'
+ };
+ return $.get(url, options).done(function (r) {
+ var trend = r[0].cells.map(function (cell) {
+ return { val: cell.d, count: cell.v[0] };
+ });
+ that.set({ sizeTrend: trend });
+ });
+ },
+
+ fetchIssues: function () {
+ var that = this,
+ url = baseUrl + '/api/issues/search',
+ options = {
+ ps: 1,
+ resolved: 'false',
+ componentUuids: this.get('componentUuid'),
+ facets: 'severities,statuses,tags'
+ };
+ return $.get(url, options).done(function (r) {
+ var severityFacet = _.findWhere(r.facets, { property: 'severities' }),
+ statusFacet = _.findWhere(r.facets, { property: 'statuses' }),
+ tagFacet = _.findWhere(r.facets, { property: 'tags' }),
+ tags = _.first(tagFacet.values, 10),
+ minTagCount = _.min(tags, function (t) {
+ return t.count;
+ }).count,
+ maxTagCount = _.max(tags, function (t) {
+ return t.count;
+ }).count,
+ tagScale = d3.scale.linear().domain([minTagCount, maxTagCount]).range([10, 24]),
+ sizedTags = tags.map(function (tag) {
+ return _.extend(tag, { size: tagScale(tag.count) });
+ });
+ that.set({
+ issues: r.total,
+ blockerIssues: _.findWhere(severityFacet.values, { val: 'BLOCKER' }).count,
+ criticalIssues: _.findWhere(severityFacet.values, { val: 'CRITICAL' }).count,
+ majorIssues: _.findWhere(severityFacet.values, { val: 'MAJOR' }).count,
+ openIssues: _.findWhere(statusFacet.values, { val: 'OPEN' }).count +
+ _.findWhere(statusFacet.values, { val: 'REOPENED' }).count,
+ issuesTags: sizedTags
+ });
+ });
+ },
+
+ fetchIssues1: function () {
+ var that = this,
+ url = baseUrl + '/api/issues/search',
+ options = {
+ ps: 1,
+ resolved: 'false',
+ createdAfter: this.get('period1Date'),
+ componentUuids: this.get('componentUuid'),
+ facets: 'severities,statuses'
+ };
+ return $.get(url, options).done(function (r) {
+ var severityFacet = _.findWhere(r.facets, { property: 'severities' }),
+ statusFacet = _.findWhere(r.facets, { property: 'statuses' });
+ that.set({
+ issues1: r.total,
+ blockerIssues1: _.findWhere(severityFacet.values, { val: 'BLOCKER' }).count,
+ criticalIssues1: _.findWhere(severityFacet.values, { val: 'CRITICAL' }).count,
+ majorIssues1: _.findWhere(severityFacet.values, { val: 'MAJOR' }).count,
+ openIssues1: _.findWhere(statusFacet.values, { val: 'OPEN' }).count +
+ _.findWhere(statusFacet.values, { val: 'REOPENED' }).count
+ });
+ });
+ },
+
+ fetchIssues2: function () {
+ var that = this,
+ url = baseUrl + '/api/issues/search',
+ options = {
+ ps: 1,
+ resolved: 'false',
+ createdAfter: this.get('period2Date'),
+ componentUuids: this.get('componentUuid'),
+ facets: 'severities,statuses'
+ };
+ return $.get(url, options).done(function (r) {
+ var severityFacet = _.findWhere(r.facets, { property: 'severities' }),
+ statusFacet = _.findWhere(r.facets, { property: 'statuses' });
+ that.set({
+ issues2: r.total,
+ blockerIssues2: _.findWhere(severityFacet.values, { val: 'BLOCKER' }).count,
+ criticalIssues2: _.findWhere(severityFacet.values, { val: 'CRITICAL' }).count,
+ majorIssues2: _.findWhere(severityFacet.values, { val: 'MAJOR' }).count,
+ openIssues2: _.findWhere(statusFacet.values, { val: 'OPEN' }).count +
+ _.findWhere(statusFacet.values, { val: 'REOPENED' }).count
+ });
+ });
+ },
+
+ fetchIssues3: function () {
+ var that = this,
+ url = baseUrl + '/api/issues/search',
+ options = {
+ ps: 1,
+ resolved: 'false',
+ createdAfter: this.get('period3Date'),
+ componentUuids: this.get('componentUuid'),
+ facets: 'severities,statuses'
+ };
+ return $.get(url, options).done(function (r) {
+ var severityFacet = _.findWhere(r.facets, { property: 'severities' }),
+ statusFacet = _.findWhere(r.facets, { property: 'statuses' });
+ that.set({
+ issues3: r.total,
+ blockerIssues3: _.findWhere(severityFacet.values, { val: 'BLOCKER' }).count,
+ criticalIssues3: _.findWhere(severityFacet.values, { val: 'CRITICAL' }).count,
+ majorIssues3: _.findWhere(severityFacet.values, { val: 'MAJOR' }).count,
+ openIssues3: _.findWhere(statusFacet.values, { val: 'OPEN' }).count +
+ _.findWhere(statusFacet.values, { val: 'REOPENED' }).count
+ });
+ });
+ },
+
+ fetchIssuesTrend: function () {
+ var that = this,
+ url = baseUrl + '/api/timemachine/index',
+ options = {
+ resource: this.get('componentKey'),
+ metrics: 'violations'
+ };
+ return $.get(url, options).done(function (r) {
+ var trend = r[0].cells.map(function (cell) {
+ return { val: cell.d, count: cell.v[0] };
+ });
+ that.set({ issuesTrend: trend });
+ });
+ },
+
+ fetchDebt: function () {
+ var that = this,
+ url = baseUrl + '/api/resources/index',
+ options = {
+ resource: this.get('componentKey'),
+ metrics: 'sqale_index',
+ includetrends: true
+ };
+ return $.get(url, options).done(function (r) {
+ var msr = r[0].msr,
+ debtMeasure = _.findWhere(msr, { key: 'sqale_index' });
+ that.set({
+ debt: debtMeasure.val,
+ debt1: debtMeasure.var1,
+ debt2: debtMeasure.var2,
+ debt3: debtMeasure.var3
+ });
+ });
+ },
+
+ fetchDebtTrend: function () {
+ var that = this,
+ url = baseUrl + '/api/timemachine/index',
+ options = {
+ resource: this.get('componentKey'),
+ metrics: 'sqale_index'
+ };
+ return $.get(url, options).done(function (r) {
+ var trend = r[0].cells.map(function (cell) {
+ return { val: cell.d, count: cell.v[0] };
+ });
+ that.set({ debtTrend: trend });
+ });
+ },
+
+ fetchCoverage: function () {
+ var that = this,
+ url = baseUrl + '/api/resources/index',
+ options = {
+ resource: this.get('componentKey'),
+ metrics: 'overall_coverage,new_overall_coverage',
+ includetrends: true
+ };
+ return $.get(url, options).done(function (r) {
+ var msr = r[0].msr,
+ coverageMeasure = _.findWhere(msr, { key: 'overall_coverage' }),
+ newCoverageMeasure = _.findWhere(msr, { key: 'new_overall_coverage' });
+ that.set({
+ coverage: coverageMeasure.val,
+ coverage1: coverageMeasure.var1,
+ coverage2: coverageMeasure.var2,
+ coverage3: coverageMeasure.var3,
+ newCoverage1: newCoverageMeasure.var1,
+ newCoverage2: newCoverageMeasure.var2,
+ newCoverage3: newCoverageMeasure.var3
+ });
+ });
+ },
+
+ fetchCoverageTrend: function () {
+ var that = this,
+ url = baseUrl + '/api/timemachine/index',
+ options = {
+ resource: this.get('componentKey'),
+ metrics: 'coverage'
+ };
+ return $.get(url, options).done(function (r) {
+ var trend = r[0].cells.map(function (cell) {
+ return { val: cell.d, count: cell.v[0] };
+ });
+ that.set({ coverageTrend: trend });
+ });
+ },
+
+ fetchDuplications: function () {
+ var that = this,
+ url = baseUrl + '/api/resources/index',
+ options = {
+ resource: this.get('componentKey'),
+ metrics: 'duplicated_lines_density',
+ includetrends: true
+ };
+ return $.get(url, options).done(function (r) {
+ var msr = r[0].msr,
+ duplicationsMeasure = _.findWhere(msr, { key: 'duplicated_lines_density' });
+ that.set({
+ duplications: duplicationsMeasure.val,
+ duplications1: duplicationsMeasure.var1,
+ duplications2: duplicationsMeasure.var2,
+ duplications3: duplicationsMeasure.var3
+ });
+ });
+ },
+
+ fetchDuplicationsTrend: function () {
+ var that = this,
+ url = baseUrl + '/api/timemachine/index',
+ options = {
+ resource: this.get('componentKey'),
+ metrics: 'duplicated_lines_density'
+ };
+ return $.get(url, options).done(function (r) {
+ var trend = r[0].cells.map(function (cell) {
+ return { val: cell.d, count: cell.v[0] };
+ });
+ that.set({ duplicationsTrend: trend });
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/overview/views/coverage-view.js b/server/sonar-web/src/main/js/overview/views/coverage-view.js
new file mode 100644
index 00000000000..f83fbed79bf
--- /dev/null
+++ b/server/sonar-web/src/main/js/overview/views/coverage-view.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+define([
+ 'templates/overview'
+], function () {
+
+ return Marionette.Layout.extend({
+ template: Templates['overview-coverage'],
+
+ modelEvents: {
+ 'change': 'render'
+ },
+
+ onRender: function () {
+ this.$('.js-pie-chart').pieChart();
+ if (this.model.has('coverageTrend')) {
+ this.$('#overview-coverage-trend').sparkline(this.model.get('coverageTrend'));
+ }
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/overview/views/debt-view.js b/server/sonar-web/src/main/js/overview/views/debt-view.js
new file mode 100644
index 00000000000..40f4c068b51
--- /dev/null
+++ b/server/sonar-web/src/main/js/overview/views/debt-view.js
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+define([
+ 'templates/overview'
+], function () {
+
+ return Marionette.Layout.extend({
+ template: Templates['overview-debt'],
+
+ modelEvents: {
+ 'change': 'render'
+ },
+
+ onRender: function () {
+ if (this.model.has('debtTrend')) {
+ this.$('#overview-debt-trend').sparkline(this.model.get('debtTrend'));
+ }
+ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
+ },
+
+ onClose: function () {
+ this.$('[data-toggle="tooltip"]').tooltip('destroy');
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/overview/views/duplications-view.js b/server/sonar-web/src/main/js/overview/views/duplications-view.js
new file mode 100644
index 00000000000..b2738e44d4f
--- /dev/null
+++ b/server/sonar-web/src/main/js/overview/views/duplications-view.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+define([
+ 'templates/overview'
+], function () {
+
+ return Marionette.Layout.extend({
+ template: Templates['overview-duplications'],
+
+ modelEvents: {
+ 'change': 'render'
+ },
+
+ onRender: function () {
+ this.$('.js-pie-chart').pieChart();
+ if (this.model.has('duplicationsTrend')) {
+ this.$('#overview-duplications-trend').sparkline(this.model.get('duplicationsTrend'));
+ }
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/overview/views/gate-view.js b/server/sonar-web/src/main/js/overview/views/gate-view.js
new file mode 100644
index 00000000000..22b4f1c2a0c
--- /dev/null
+++ b/server/sonar-web/src/main/js/overview/views/gate-view.js
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+define([
+ 'templates/overview'
+], function () {
+
+ return Marionette.Layout.extend({
+ template: Templates['overview-gate'],
+
+ modelEvents: {
+ 'change': 'render'
+ },
+
+ onRender: function () {
+ if (this.model.has('gateStatus')) {
+ this.$el.closest('.overview-card').addClass('overview-gate-' + this.model.get('gateStatus').toLowerCase());
+ }
+ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
+ },
+
+ onClose: function () {
+ this.$('[data-toggle="tooltip"]').tooltip('destroy');
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/overview/views/issues-view.js b/server/sonar-web/src/main/js/overview/views/issues-view.js
new file mode 100644
index 00000000000..c96f57963f8
--- /dev/null
+++ b/server/sonar-web/src/main/js/overview/views/issues-view.js
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+define([
+ 'templates/overview'
+], function () {
+
+ return Marionette.Layout.extend({
+ template: Templates['overview-issues'],
+
+ modelEvents: {
+ 'change': 'render'
+ },
+
+ onRender: function () {
+ if (this.model.has('issuesTrend')) {
+ this.$('#overview-issues-trend').sparkline(this.model.get('issuesTrend'));
+ }
+ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
+ },
+
+ onClose: function () {
+ this.$('[data-toggle="tooltip"]').tooltip('destroy');
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/overview/views/size-view.js b/server/sonar-web/src/main/js/overview/views/size-view.js
new file mode 100644
index 00000000000..43d6b9f2ba6
--- /dev/null
+++ b/server/sonar-web/src/main/js/overview/views/size-view.js
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+define([
+ 'templates/overview'
+], function () {
+
+ return Marionette.Layout.extend({
+ template: Templates['overview-size'],
+
+ modelEvents: {
+ 'change': 'render'
+ },
+
+ onRender: function () {
+ if (this.model.has('sizeTrend')) {
+ this.$('#overview-size-trend').sparkline(this.model.get('sizeTrend'));
+ }
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/less/components/badges.less b/server/sonar-web/src/main/less/components/badges.less
index 4523a35517e..856125aea62 100644
--- a/server/sonar-web/src/main/less/components/badges.less
+++ b/server/sonar-web/src/main/less/components/badges.less
@@ -19,6 +19,7 @@
*/
@import (reference) "../variables";
@import (reference) "../mixins";
+@import (reference) "../init/links";
.badge {
display: inline-block;
@@ -26,7 +27,7 @@
padding: 2px 7px;
font-size: 11px;
font-weight: 300;
- color: #fff;
+ color: @white;
line-height: 12px;
vertical-align: baseline;
white-space: nowrap;
@@ -35,6 +36,10 @@
&:empty { display: none; }
+ &:hover, &:focus, &:active { color: @white; }
+
+ a& { .link-no-underline; }
+
.list-group-item > &,
.list-group-item-heading > & {
float: right;
@@ -45,3 +50,10 @@
margin-right: 5px;
}
}
+
+.badge-muted {
+ background-color: transparent;
+ color: @secondFontColor;
+
+ &:hover, &:focus, &:active { color: @blue; }
+}
diff --git a/server/sonar-web/src/main/less/components/page.less b/server/sonar-web/src/main/less/components/page.less
index 342c99b49df..a33bf77606c 100644
--- a/server/sonar-web/src/main/less/components/page.less
+++ b/server/sonar-web/src/main/less/components/page.less
@@ -63,6 +63,10 @@
.page-actions {
float: right;
+
+ .badge {
+ margin: 3px 0;
+ }
}
.page-description {
diff --git a/server/sonar-web/src/main/less/components/panels.less b/server/sonar-web/src/main/less/components/panels.less
index 71d9ed8dc64..8105fdc55b6 100644
--- a/server/sonar-web/src/main/less/components/panels.less
+++ b/server/sonar-web/src/main/less/components/panels.less
@@ -23,3 +23,8 @@
.panel {
padding: 10px;
}
+
+.panel-info {
+ border: 1px solid @blue;
+ background-color: @lightBlue;
+}
diff --git a/server/sonar-web/src/main/less/init/lists.less b/server/sonar-web/src/main/less/init/lists.less
index dafabcc138d..c352665e648 100644
--- a/server/sonar-web/src/main/less/init/lists.less
+++ b/server/sonar-web/src/main/less/init/lists.less
@@ -48,6 +48,7 @@ ol, ul {
.list-inline > li {
display: inline-block;
+ vertical-align: top;
padding-right: 5px;
padding-left: 5px;
}
diff --git a/server/sonar-web/src/main/less/init/misc.less b/server/sonar-web/src/main/less/init/misc.less
index 28b11efdaea..ce7d07dc15e 100644
--- a/server/sonar-web/src/main/less/init/misc.less
+++ b/server/sonar-web/src/main/less/init/misc.less
@@ -35,6 +35,7 @@
.note {
color: @secondFontColor;
font-size: @smallFontSize;
+ font-weight: 300;
}
.spacer-left { margin-left: 8px; }
@@ -55,8 +56,11 @@ td.spacer-top { padding-top: 8px; }
.width-100 { width: 100%; }
.width-80 { width: 80%; }
+.width-60 { width: 60%; }
+.width-55 { width: 55%; }
.width-40 { width: 40%; }
.width-20 { width: 20%; }
+.width-15 { width: 15%; }
.justify {
margin-bottom: -1em;
diff --git a/server/sonar-web/src/main/less/pages.less b/server/sonar-web/src/main/less/pages.less
index a24699b5834..ce3a8a86fd9 100644
--- a/server/sonar-web/src/main/less/pages.less
+++ b/server/sonar-web/src/main/less/pages.less
@@ -25,3 +25,4 @@
@import "pages/issues";
@import "pages/libraries";
@import "pages/quality-gates";
+@import "pages/overview";
diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less
new file mode 100644
index 00000000000..57d7f35b39c
--- /dev/null
+++ b/server/sonar-web/src/main/less/pages/overview.less
@@ -0,0 +1,90 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@import (reference) "../variables";
+@import (reference) "../mixins";
+@import (reference) "../init/links";
+
+.overview {
+ padding: 10px;
+ .box-sizing(border-box);
+ overflow: hidden;
+}
+
+.overview-card {
+ padding: 10px;
+ background: @white;
+ border: 1px solid @barBorderColor;
+}
+
+.overview-card + .overview-card {
+ margin-top: 10px;
+}
+
+.overview-gate-ok { border: 2px solid @green; }
+
+.overview-gate-warn { border: 2px solid @orange; }
+
+.overview-gate-error { border: 2px solid @red; }
+
+.overview-card-header {
+ padding-bottom: 5px;
+ border-bottom: 1px solid @barBorderColor;
+}
+
+.overview-status {
+ margin: 0;
+ padding: 0 6px;
+ color: #fff !important;
+ font-size: 24px;
+ font-weight: 300;
+
+ a& {
+ .link-no-underline;
+
+ &:hover, &:focus, &:active {
+ opacity: 0.8;
+ }
+ }
+}
+
+.overview-status-OK { background-color: @green; }
+.overview-status-WARN { background-color: @orange; }
+.overview-status-ERROR { background-color: @red; }
+
+.overview-main-measure {
+ display: inline-block;
+ vertical-align: middle;
+ font-size: 36px;
+ font-weight: 300;
+}
+
+.overview-trend {
+ display: inline-block;
+ vertical-align: middle;
+ margin-left: 15px;
+}
+
+.overview-measure {
+ font-size: 16px;
+}
+
+.overview-measure ~ .note:last-child {
+ padding-top: 3px;
+}
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb
index 7c6fdc34a30..e3b9e149b1b 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb
@@ -17,6 +17,7 @@
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
+
class DashboardController < ApplicationController
SECTION=Navigation::SECTION_RESOURCE
@@ -25,20 +26,25 @@ class DashboardController < ApplicationController
def index
load_resource()
- if !@resource || @resource.display_dashboard?
+ if @resource && !params[:did] && !params[:name]
+ url = url_for :controller => 'overview', :action => 'index'
+ url = url + '?id=' + @resource.key.to_s.gsub(/[^a-zA-Z0-9_\-.]/n){ sprintf("%%%02X", $&.unpack("C")[0]) }
+ return redirect_to url
+ end
+ if !@resource || @resource.display_dashboard?
+ redirect_if_bad_component()
+ load_dashboard()
+ load_authorized_widget_definitions()
+ else
+ if !@resource || !@snapshot
redirect_if_bad_component()
- load_dashboard()
- load_authorized_widget_definitions()
else
- if !@resource || !@snapshot
- redirect_if_bad_component()
- else
- # display the layout of the parent without the sidebar, usually the directory, but display the file viewers
- @hide_sidebar = true
- @file = @resource
- @project = @snapshot.parent.project
- @metric=params[:metric]
- render :action => 'no_dashboard'
+ # display the layout of the parent without the sidebar, usually the directory, but display the file viewers
+ @hide_sidebar = true
+ @file = @resource
+ @project = @snapshot.parent.project
+ @metric=params[:metric]
+ render :action => 'no_dashboard'
end
end
end
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb
new file mode 100644
index 00000000000..5a78c39505d
--- /dev/null
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/overview_controller.rb
@@ -0,0 +1,31 @@
+#
+# SonarQube, open source software quality management tool.
+# Copyright (C) 2008-2014 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# SonarQube is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# SonarQube is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+
+class OverviewController < ApplicationController
+ before_filter :init_resource_for_user_role
+ helper DashboardHelper
+
+ SECTION=Navigation::SECTION_RESOURCE
+
+ def index
+
+ end
+
+end
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/helpers/dashboard_helper.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/helpers/dashboard_helper.rb
index 1dd75591199..a42726f1e45 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/helpers/dashboard_helper.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/helpers/dashboard_helper.rb
@@ -78,6 +78,12 @@ module DashboardHelper
end
end
+ def short_period_label(snapshot, index)
+ if snapshot.project_snapshot
+ Api::Utils.java_facade.getPeriodLabel(index)
+ end
+ end
+
def violation_period_select_options(snapshot, index)
return nil if snapshot.nil? || snapshot.project_snapshot.nil?
mode=snapshot.project_snapshot.send "period#{index}_mode"
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb
new file mode 100644
index 00000000000..87de54463aa
--- /dev/null
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/overview/index.html.erb
@@ -0,0 +1,16 @@
+<div class="overview"></div>
+
+<script>
+ window.overviewConf = {
+ componentKey: '<%= @resource.key -%>',
+ componentUuid: '<%= @resource.uuid -%>',
+
+ period1Name: '<%= short_period_label(@snapshot, 1) -%>',
+ period1Date: '<%= @snapshot.period_datetime(1).strftime('%FT%T%z') -%>',
+ period2Name: '<%= short_period_label(@snapshot, 2) -%>',
+ period2Date: '<%= @snapshot.period_datetime(2).strftime('%FT%T%z') -%>',
+ period3Name: '<%= short_period_label(@snapshot, 3) -%>',
+ period3Date: '<%= @snapshot.period_datetime(3).strftime('%FT%T%z') -%>'
+ };
+ require(['overview/app']);
+</script>
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index ae9f609567a..933ec045d4e 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -2984,3 +2984,17 @@ workspace.minimize=Minimize
workspace.full_window=Expand to full window
workspace.normal_size=Collapse to normal size
workspace.close=Remove from the list of pinned files
+
+
+
+
+#------------------------------------------------------------------------------
+#
+# OVERVIEW
+#
+#------------------------------------------------------------------------------
+overview.lines_of_code=Lines of Code
+overview.issues=Issues
+overview.debt=Debt
+overview.coverage=Coverage
+overview.duplications=Duplications