]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2069 New treemap widget
authorSimon Brandhof <simon.brandhof@gmail.com>
Mon, 6 Feb 2012 15:46:54 +0000 (16:46 +0100)
committerSimon Brandhof <simon.brandhof@gmail.com>
Mon, 6 Feb 2012 18:47:12 +0000 (19:47 +0100)
14 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/treemap.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/controllers/treemap_controller.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/models/treemap2.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/components/_treemap_gradient.rhtml
sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_gradient.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_treemap.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/treemap/_treemap_container.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/lib/treemap/html_output.rb
sonar-server/src/main/webapp/WEB-INF/lib/treemap/node.rb
sonar-server/src/main/webapp/javascripts/application.js
sonar-server/src/main/webapp/stylesheets/style.css

index 48c94a861e8e92c39c006c4bbfec1bcea13be7d4..1bbb8c29ecbdb385a4af528a050cae88f560a270 100644 (file)
@@ -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 (file)
index 0000000..733e0a0
--- /dev/null
@@ -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 (file)
index 0000000..93b2527
--- /dev/null
@@ -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
index e73e418b2796d550e702c68ce6a695c538feb7d1..6b741f87e9cac0ea9c211990db6099b4b810798f 100644 (file)
@@ -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 (file)
index 0000000..ad96d43
--- /dev/null
@@ -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 (file)
index 0000000..cb17402
--- /dev/null
@@ -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 += "<div id=\"node-#{node.id}\" style=\""
+    html += "overflow:hidden;position:absolute;"
+    html += "left:#{node.bounds.x1}px; top:#{node.bounds.y1}px;"
+    html += "width:#{node.bounds.width}px;height: #{node.bounds.height}px;"
+    html += "background-color:#FFF;"
+    html += "\" alt='#{node.tooltip}' title='#{node.tooltip}'>"
+    html += "<div rid='#{node.rid}' id=\"tm-node-#{node.id}\" style='margin: 1px;background-color: #{node.color}; height: #{node.bounds.height-4}px;
+border: 1px solid #{node.color};' "
+    if node.browsable
+      html += "b=1 "
+    end
+    if @details_at_depth==node.depth
+      html += "onmouseover=\"this.style.borderColor='#444';\" onmouseout=\"this.style.borderColor='#{node.color}';\""
+    end
+    html += ' >'
+    html += draw_node_body(node)
+
+    if (!node.children.nil? && node.children.size > 0)
+      node.children.each do |c|
+        html += draw_node(c)
+      end
+    end
+    html += "</div></div>"
+  end
+
+  def draw_label(node)
+    label= "<a href='#' onclick='return openResource(#{node.rid})'>"
+    label += node_label(node)
+    label += "</a>"
+    label
+  end
+
+  def draw_tooltips(node)
+    ''
+  end
+end
\ No newline at end of file
index 635136285f66641d1057a745e2a9021b02b5893a..7fbfab8f0ca0b904126258761677eb445539493c 100644 (file)
@@ -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 -%> <img id="<%= id -%>" src="<%= image_path image -%>" style="border: 1px solid #000; vertical-align:middle"> <%= max -%><%= color_metric.suffix -%>
+  <span class="note"><%= min -%><%= color_metric.suffix -%> <img id="<%= id -%>" src="<%= image_path image -%>" style="border: 1px solid #000; vertical-align:middle"> <%= max
+  -%><%= color_metric.suffix -%></span>
 <%
 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 (file)
index 0000000..e111460
--- /dev/null
@@ -0,0 +1,19 @@
+<%
+if metric && metric.worst_value && metric.best_value
+  if metric.worst_value<metric.best_value
+    min=metric.worst_value
+    max=metric.best_value
+    image = 'treemap_gradient.png'
+    id = 'treemap_gradient_direction_positive'
+  else
+    min=metric.best_value
+    max=metric.worst_value
+    image = 'treemap_gradient_inverted.png'
+    id = 'treemap_gradient_direction_negative'
+  end
+%>
+  <%= min -%><%= metric.suffix -%> <img id="<%= id -%>" src="<%= image_path image -%>" style="border: 1px solid #000; vertical-align:middle"> <%= 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 (file)
index 0000000..61e67e7
--- /dev/null
@@ -0,0 +1,9 @@
+<div style="width: <%= treemap.width -%>px; height:<%= treemap.height %>px;">
+  <%= treemap.generate_html() -%>
+</div>
+<script>
+  for (var i = 1; i <= <%= treemap.id_counter -%>; i++) {
+    addTmEvent(<%= treemap.id -%>, i);
+  }
+  $('tm-loading-<%= treemap.id -%>').hide();
+</script>
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 (file)
index 0000000..b6b533a
--- /dev/null
@@ -0,0 +1,37 @@
+<div id="tm-container-<%= treemap_id -%>">
+  <table width="100%" class="spacer-bottom">
+    <tr>
+      <td valign="top" class="thin nowrap">
+        <span class="comments"><%= message('size') -%></span>
+        <br/>
+        <%= 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})" %>
+      </td>
+      <td valign="top" class="thin nowrap">
+        <span class="comments"><%= message('color') -%></span>
+        <span id="tm-gradient-<%= treemap_id -%>" class="note"></span>
+        <br/>
+        <%= 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' -%>
+      </td>
+      <td></td>
+    </tr>
+  </table>
+  <input type="hidden" id="tm-h-<%= treemap_id -%>" value="<%= heightInPercents -%>"/>
+</div>
+<div id="tm-error-<%= treemap_id -%>" class="error" style="display:none"></div>
+<div id="tm-<%= treemap_id -%>" class="treemap spacer-bottom"></div>
+
+<div style="margin: 5px 0 0 0" class="notes">
+  <div style="float: right"><span>Left click to zoom in. Right click to zoom out.</span></div>
+  <div id="tm-bc-<%= treemap_id -%>">/</div>
+</div>
+
+<script>
+  treemapContexts[<%= treemap_id -%>] = [
+    [<%= resource_id -%>, '']
+  ];
+
+  refreshTm(<%= treemap_id -%>, <%= resource_id -%>);
+</script>
index 18b509b59e617a48b979eadbf11bf90282bed0a1..dac5eddcfa82f6532a159279b47bc69166c89793 100644 (file)
@@ -136,5 +136,6 @@ CSS
     end
     
     def draw_tooltips(node)
+      ''
     end
 end
index 0b277e3d2766d8cbcbbd24873994dfd33e104d54..dbcc6e4e0fabbca30b198ed4ba87dca56d1db086 100644 (file)
@@ -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
index b7a40fd9ee0c08c9990e0c470ab04bcb57ee3a21..eb13eac4303f0551f9936c8610b9be1d2ac2e8e1 100644 (file)
@@ -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] + '&nbsp;/&nbsp;';
+  });
+  $('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
index 8070830abcf162a10ef78a969bd1230177f9c216..afc4f151da13459c239baf7ef12f4f2acb9ced17 100644 (file)
@@ -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 ------------------- */