@@ -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: |
@@ -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'}} <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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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 |
@@ -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> ' + t('severity', severity) | |||
'<i class="icon-severity-' + severity.toLowerCase() + '"></i> ' + 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> ' + t('issue.status', status); | |||
if (resolution != null) { | |||
s = s + ' (' + 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; | |||
}); | |||
})(); |
@@ -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); |
@@ -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); |
@@ -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 | |||
}); |
@@ -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(); | |||
}); | |||
}); |
@@ -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); | |||
} | |||
}); | |||
}); |
@@ -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 }); | |||
}); | |||
} | |||
}); | |||
}); |
@@ -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')); | |||
} | |||
} | |||
}); | |||
}); |
@@ -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'); | |||
} | |||
}); | |||
}); |
@@ -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')); | |||
} | |||
} | |||
}); | |||
}); |
@@ -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'); | |||
} | |||
}); | |||
}); |
@@ -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'); | |||
} | |||
}); | |||
}); |
@@ -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')); | |||
} | |||
} | |||
}); | |||
}); |
@@ -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; } | |||
} |
@@ -63,6 +63,10 @@ | |||
.page-actions { | |||
float: right; | |||
.badge { | |||
margin: 3px 0; | |||
} | |||
} | |||
.page-description { |
@@ -23,3 +23,8 @@ | |||
.panel { | |||
padding: 10px; | |||
} | |||
.panel-info { | |||
border: 1px solid @blue; | |||
background-color: @lightBlue; | |||
} |
@@ -48,6 +48,7 @@ ol, ul { | |||
.list-inline > li { | |||
display: inline-block; | |||
vertical-align: top; | |||
padding-right: 5px; | |||
padding-left: 5px; | |||
} |
@@ -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; |
@@ -25,3 +25,4 @@ | |||
@import "pages/issues"; | |||
@import "pages/libraries"; | |||
@import "pages/quality-gates"; | |||
@import "pages/overview"; |
@@ -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; | |||
} |
@@ -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 |
@@ -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 |
@@ -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" |
@@ -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> |
@@ -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 |