diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-03-30 13:42:42 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-03-30 13:42:42 +0200 |
commit | fcc90027f5b624b333f4e00045a04dacb7d67394 (patch) | |
tree | c82aadae3cc621a1832440a5bbb338966cc3ca50 | |
parent | 3fa0d737fb0177414765ca800f0271b6859437d9 (diff) | |
download | sonarqube-fcc90027f5b624b333f4e00045a04dacb7d67394.tar.gz sonarqube-fcc90027f5b624b333f4e00045a04dacb7d67394.zip |
SONAR-6331 add a project overview page
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'}} <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> ' + 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; + }); + })(); 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 |