From b9a7fa1711b0549417c48fc3100a01726f26b378 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Mon, 6 Feb 2012 16:46:54 +0100 Subject: [PATCH] SONAR-2069 New treemap widget --- .../org/sonar/plugins/core/CorePlugin.java | 1 + .../plugins/core/widgets/TreemapWidget.java | 44 +++++ .../plugins/core/widgets/treemap.html.erb | 7 + .../app/controllers/dashboard_controller.rb | 3 + .../app/controllers/treemap_controller.rb | 62 +++++++ .../webapp/WEB-INF/app/models/treemap2.rb | 172 ++++++++++++++++++ .../views/components/_treemap_gradient.rhtml | 3 +- .../app/views/treemap/_gradient.html.erb | 19 ++ .../app/views/treemap/_treemap.html.erb | 9 + .../views/treemap/_treemap_container.html.erb | 37 ++++ .../webapp/WEB-INF/lib/treemap/html_output.rb | 1 + .../main/webapp/WEB-INF/lib/treemap/node.rb | 4 +- .../main/webapp/javascripts/application.js | 69 ++++++- .../src/main/webapp/stylesheets/style.css | 12 +- 14 files changed, 438 insertions(+), 5 deletions(-) create mode 100644 plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java create mode 100644 plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/treemap.html.erb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/controllers/treemap_controller.rb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/models/treemap2.rb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_gradient.html.erb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_treemap.html.erb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_treemap_container.html.erb diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index 48c94a861e8..1bbb8c29ecb 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -267,6 +267,7 @@ public class CorePlugin extends SonarPlugin { extensions.add(UnplannedReviewsWidget.class); extensions.add(ActionPlansWidget.class); extensions.add(ReviewsMetricsWidget.class); + extensions.add(TreemapWidget.class); // dashboards extensions.add(DefaultDashboard.class); diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java new file mode 100644 index 00000000000..733e0a07780 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java @@ -0,0 +1,44 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.core.widgets; + +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.web.*; + +@WidgetCategory("Reporting") +@WidgetProperties({ + @WidgetProperty(key = "heightInPercents", type= WidgetPropertyType.INTEGER, defaultValue = "100", description = "Height, in percents of width"), + @WidgetProperty(key = "sizeMetric", type= WidgetPropertyType.METRIC, defaultValue = CoreMetrics.NCLOC_KEY, description = "Default metric for size"), + @WidgetProperty(key = "colorMetric", type= WidgetPropertyType.METRIC, defaultValue = CoreMetrics.VIOLATIONS_DENSITY_KEY, description = "Default metric for color") +}) +public class TreemapWidget extends AbstractRubyTemplate implements RubyRailsWidget { + public String getId() { + return "tm"; // avoid conflict with CSS style "treemap" + } + + public String getTitle() { + return "Treemap of components"; + } + + @Override + protected String getTemplatePath() { + return "/Users/sbrandhof/projects/github/sonar/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/treemap.html.erb"; + } +} \ No newline at end of file diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/treemap.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/treemap.html.erb new file mode 100644 index 00000000000..93b2527aebf --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/treemap.html.erb @@ -0,0 +1,7 @@ +<%= render :partial => 'treemap/treemap_container', :locals => { + :treemap_id => widget.id, + :size_metric => widget_properties['sizeMetric'], + :color_metric => widget_properties['colorMetric'], + :heightInPercents => widget_properties['heightInPercents'], + :resource_id => @resource.id + } -%> \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb index e73e418b279..6b741f87e9c 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb @@ -184,6 +184,9 @@ class DashboardController < ApplicationController def load_resource @resource=Project.by_key(params[:id]) not_found("Resource not found") unless @resource + + @resource=Project.find(@resource.copy_resource_id) if @resource.copy_resource_id + access_denied unless has_role?(:user, @resource) @snapshot = @resource.last_snapshot not_found("Snapshot not found") unless @snapshot diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/treemap_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/treemap_controller.rb new file mode 100644 index 00000000000..ad96d43989b --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/treemap_controller.rb @@ -0,0 +1,62 @@ +# +# Sonar, entreprise quality control tool. +# Copyright (C) 2008-2012 SonarSource +# mailto:contact AT sonarsource DOT com +# +# Sonar 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. +# +# Sonar 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 Sonar; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +# +class TreemapController < ApplicationController + helper :metrics + + SECTION=Navigation::SECTION_HOME + + def index + html_id = params[:id] + bad_request('Missing required property: id') if html_id.blank? + + width = params[:width] + bad_request('Missing required property: width') if width.blank? + bad_request('Bad width') if width.to_i<=0 + + height = params[:height] + bad_request('Missing required property: height') if height.blank? + bad_request('Bad height') if height.to_i<=0 + + size_metric=Metric.by_key(params[:size_metric]||'lines') + bad_request('Unknown metric: ' + params[:size_metric]) unless size_metric + + color_metric=(params[:color_metric].present? ? Metric.by_key(params[:color_metric]) : nil) + + resource = nil + if params[:resource] + resource = Project.by_key(params[:resource]) + bad_request('Unknown resource: ' + params[:resource]) unless resource + access_denied unless has_role?(:user, resource) + end + + treemap = Treemap2.new(html_id, size_metric, width.to_i, height.to_i, { + :color_metric => color_metric, + :root_snapshot => (resource ? resource.last_snapshot : nil), + :period_index => params[:period_index].to_i, + :browsable => true + }) + + render :update do |page| + page.replace_html "tm-#{html_id}", :partial => 'treemap', :object => treemap + page.replace_html "tm-gradient-#{html_id}", :partial => 'gradient', :locals => {:metric => color_metric} + end + end + +end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/treemap2.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/treemap2.rb new file mode 100644 index 00000000000..cb17402b0c4 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/treemap2.rb @@ -0,0 +1,172 @@ +# +# Sonar, entreprise quality control tool. +# Copyright (C) 2008-2012 SonarSource +# mailto:contact AT sonarsource DOT com +# +# Sonar 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. +# +# Sonar 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 Sonar; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +# +class Treemap2 + include ActionView::Helpers::UrlHelper + + attr_accessor :size_metric, :color_metric, :width, :height, :root_snapshot, :period_index, + :id, :id_counter, :measures, :browsable + + def initialize(id, size_metric, width, height, options={}) + @id_counter = 0 + @id = id + @size_metric = size_metric + @width = width + @height = height + + @color_metric = options[:color_metric] + @root_snapshot = options[:root_snapshot] + @measures = options[:measures] # pre-computed measures, for example by filters + if options[:period_index] && options[:period_index]>0 + @period_index = options[:period_index] + end + end + + def generate_html + root = Treemap::Node.new(:id => -1, :label => '') + build_tree(root) + + output = Sonar::HtmlOutput.new do |o| + o.width = @width + o.height = @height + o.full_html = false + o.details_at_depth = 1 + end + output.to_html(root) + end + + def empty? + @id_counter==0 + end + + protected + + def measures + @measures ||= + begin + metric_ids=[@size_metric.id] + metric_ids << @color_metric.id if @color_metric && @color_metric.id!=@size_metric.id + + sql_conditions = 'snapshots.islast=? AND project_measures.characteristic_id IS NULL and project_measures.rule_id IS NULL ' + + 'and project_measures.rule_priority IS NULL and project_measures.metric_id in (?)' + sql_values = [true, metric_ids] + if @root_snapshot + sql_conditions += " AND snapshots.parent_snapshot_id=?" + sql_values << @root_snapshot.id + else + sql_conditions<<" AND snapshots.scope='PRJ' and snapshots.qualifier='TRK'" + end + + ProjectMeasure.find(:all, :include => {:snapshot => :project}, :conditions => [sql_conditions].concat(sql_values)) + end + end + + def build_tree(node) + color_measures_by_sid={} + if @color_metric + measures.each do |measure| + color_measures_by_sid[measure.snapshot_id]=measure if measure.metric_id==@color_metric.id + end + end + + measures.each do |measure| + if measure.metric_id==@size_metric.id + color_measure = color_measures_by_sid[measure.snapshot_id] + resource = measure.snapshot.project + child = Treemap::Node.new(:id => "#{@id}-#{@id_counter += 1}", + :size => size_value(measure), + :label => resource.name(false), + :title => escape_javascript(resource.name(true)), + :tooltip => tooltip(resource, measure, color_measure), + :color => html_color(color_measure), + :rid => resource.id, + :browsable => resource.display_dashboard?) + node.add_child(child) + end + end + end + + def tooltip(resource,size_measure, color_measure) + html=CGI::escapeHTML(resource.name(true)) + html += " - #{CGI::escapeHTML(@size_metric.short_name)}: #{CGI::escapeHTML(size_measure.formatted_value)}" + if color_measure + html += " - #{CGI::escapeHTML(@color_metric.short_name)}: #{CGI::escapeHTML(color_measure.formatted_value)}" + end + html + end + + def size_value(measure) + if @period_index + var=measure.variation(@period_index) + var ? var.to_f.abs : 0.0 + elsif measure.value + measure.value.to_f.abs||0.0 + else + 0.0 + end + end + + def html_color(measure) + MeasureColor.color(measure).html + end + +end + +class Sonar::HtmlOutput < Treemap::HtmlOutput + + def draw_node(node) + return "" if node.bounds.nil? + + html = '' + html += "
" + html += "
0) + node.children.each do |c| + html += draw_node(c) + end + end + html += "
" + end + + def draw_label(node) + label= "" + label += node_label(node) + label += "" + label + end + + def draw_tooltips(node) + '' + end +end \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/components/_treemap_gradient.rhtml b/sonar-server/src/main/webapp/WEB-INF/app/views/components/_treemap_gradient.rhtml index 635136285f6..7fbfab8f0ca 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/components/_treemap_gradient.rhtml +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/components/_treemap_gradient.rhtml @@ -14,7 +14,8 @@ if color_metric && color_metric.worst_value && color_metric.best_value id = 'treemap_gradient_direction_negative' end %> - <%= min -%><%= color_metric.suffix -%> <%= max -%><%= color_metric.suffix -%> + <%= min -%><%= color_metric.suffix -%> <%= max + -%><%= color_metric.suffix -%> <% end %> \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_gradient.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_gradient.html.erb new file mode 100644 index 00000000000..e1114603021 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_gradient.html.erb @@ -0,0 +1,19 @@ +<% +if metric && metric.worst_value && metric.best_value + if metric.worst_value + <%= min -%><%= metric.suffix -%> <%= max + -%><%= metric.suffix -%> +<% +end +%> \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_treemap.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_treemap.html.erb new file mode 100644 index 00000000000..61e67e7c8a4 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_treemap.html.erb @@ -0,0 +1,9 @@ +
+ <%= treemap.generate_html() -%> +
+ diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_treemap_container.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_treemap_container.html.erb new file mode 100644 index 00000000000..b6b533af1fd --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_treemap_container.html.erb @@ -0,0 +1,37 @@ +
+ + + + + + +
+ <%= message('size') -%> +
+ <%= select_tag "size", options_grouped_by_domain(Sonar::TreemapBuilder.size_metrics, (size_metric ? size_metric.key : nil), :include_empty => true), + :id => "tm-size-#{treemap_id}", :class => 'small spacer-right', :onchange => "refreshTm(#{treemap_id}, #{resource_id})" %> +
+ <%= message('color') -%> + +
+ <%= select_tag 'color', options_grouped_by_domain(Sonar::TreemapBuilder.color_metrics, (color_metric ? color_metric.key : nil), :include_empty => true), + :id => "tm-color-#{treemap_id}", :class => 'small', :onchange => "refreshTm(#{treemap_id}, #{resource_id})" %> + <%= image_tag 'loading.gif', :id => "tm-loading-#{treemap_id}", :style => 'vertical-align: top;display: none' -%> +
+ +
+ +
+ +
+
Left click to zoom in. Right click to zoom out.
+
/
+
+ + diff --git a/sonar-server/src/main/webapp/WEB-INF/lib/treemap/html_output.rb b/sonar-server/src/main/webapp/WEB-INF/lib/treemap/html_output.rb index 18b509b59e6..dac5eddcfa8 100644 --- a/sonar-server/src/main/webapp/WEB-INF/lib/treemap/html_output.rb +++ b/sonar-server/src/main/webapp/WEB-INF/lib/treemap/html_output.rb @@ -136,5 +136,6 @@ CSS end def draw_tooltips(node) + '' end end diff --git a/sonar-server/src/main/webapp/WEB-INF/lib/treemap/node.rb b/sonar-server/src/main/webapp/WEB-INF/lib/treemap/node.rb index 0b277e3d276..dbcc6e4e0fa 100644 --- a/sonar-server/src/main/webapp/WEB-INF/lib/treemap/node.rb +++ b/sonar-server/src/main/webapp/WEB-INF/lib/treemap/node.rb @@ -37,7 +37,7 @@ module Treemap # # class Treemap::Node - attr_accessor :id, :label, :color, :size, :bounds, :parent, :tooltip, :url, :title + attr_accessor :id, :label, :color, :size, :bounds, :parent, :tooltip, :url, :title, :rid, :browsable attr_reader :children # @@ -64,6 +64,8 @@ module Treemap @url = opts[:url] @tooltip = opts[:tooltip] @children = [] + @rid = opts[:rid] + @browsable = opts[:browsable] if(@id.nil?) make_id diff --git a/sonar-server/src/main/webapp/javascripts/application.js b/sonar-server/src/main/webapp/javascripts/application.js index b7a40fd9ee0..eb13eac4303 100644 --- a/sonar-server/src/main/webapp/javascripts/application.js +++ b/sonar-server/src/main/webapp/javascripts/application.js @@ -138,4 +138,71 @@ var SelectBox = { box.options[i].selected = 'selected'; } } -}; \ No newline at end of file +}; + +var treemapContexts = {}; + +function addTmEvent(treemap_id, elt_index) { + var elt = $('tm-node-' + treemap_id + '-' + elt_index); + elt.oncontextmenu = function () { + return false + }; + elt.observe('mouseup', function (event) { + context = treemapContexts[treemap_id]; + onTmClick(treemap_id, event, context); + }); +} + +function onTmClick(treemap_id, event, context) { + if (Event.isLeftClick(event)) { + var link = event.findElement('a'); + if (link != null) { + event.stopPropagation(); + return false; + } + + var elt = event.findElement('div'); + var rid = elt.readAttribute('rid'); + var browsable = elt.hasAttribute('b'); + if (browsable) { + var label = elt.innerText || elt.textContent; + context.push([rid, label]); + refreshTm(treemap_id, rid); + } else { + openResource(rid); + } + + } else if (Event.isRightClick(event)) { + if (context.length > 1) { + context.pop(); + var rid = context[context.length - 1][0]; + refreshTm(treemap_id, rid); + } + } +} + +function refreshTm(treemap_id, resource_id) { + var size = $F('tm-size-' + treemap_id); + var color = $F('tm-color-' + treemap_id); + var width = $('tm-' + treemap_id).getWidth() - 10; + var height = Math.round(width * parseFloat($F('tm-h-' + treemap_id) / 100.0)); + + context = treemapContexts[treemap_id]; + var output = ''; + context.each(function (elt) { + output += elt[1] + ' / '; + }); + $('tm-bc-' + treemap_id).innerHTML = output; + $('tm-loading-' + treemap_id).show(); + + new Ajax.Request( + baseUrl + '/treemap/index?id=' + treemap_id + '&width=' + width + '&height=' + height + '&size_metric=' + size + '&color_metric=' + color + '&resource=' + resource_id, + {asynchronous:true, evalScripts:true}); + + return false; +} + +function openResource(key) { + document.location = baseUrl + '/dashboard/index/' + key; + return false; +} \ No newline at end of file diff --git a/sonar-server/src/main/webapp/stylesheets/style.css b/sonar-server/src/main/webapp/stylesheets/style.css index 8070830abcf..afc4f151da1 100644 --- a/sonar-server/src/main/webapp/stylesheets/style.css +++ b/sonar-server/src/main/webapp/stylesheets/style.css @@ -59,6 +59,7 @@ a { /* LAYOUT */ div#container { height: auto !important; + min-width: 940px; } div#hd { @@ -516,10 +517,17 @@ h4, .h4 { position: relative; cursor: pointer; } - .treemap .label { color: #fff; - padding: 2px; +} +.treemap a { + color: #FFF; + text-decoration: none; + font-size: 12px; + padding: 1px; +} +.treemap a:hover { + text-decoration: underline; } /* ------------------- MESSAGES ------------------- */ -- 2.39.5