]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-37 & SONAR-2911 Add comparison tool
authorFabrice Bellingard <fabrice.bellingard@sonarsource.com>
Thu, 29 Nov 2012 10:28:02 +0000 (11:28 +0100)
committerFabrice Bellingard <fabrice.bellingard@sonarsource.com>
Thu, 29 Nov 2012 12:46:38 +0000 (13:46 +0100)
- To compare projects to each others (SONAR-37)
- To compare X versions of a project (SONAR-2911)

13 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DefaultResourceTypes.java
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/resources/ResourceType.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/comparison_controller.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb
sonar-server/src/main/webapp/WEB-INF/app/views/comparison/_versions.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/comparison/index.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_tools.html.erb
sonar-server/src/main/webapp/images/controls/move_down.png [new file with mode: 0644]
sonar-server/src/main/webapp/images/controls/move_left.png [new file with mode: 0644]
sonar-server/src/main/webapp/images/controls/move_right.png [new file with mode: 0644]
sonar-server/src/main/webapp/images/controls/move_up.png [new file with mode: 0644]

index bc3125b2d07d8b97a357638338d2dbc955d0569e..72bd2cb244ede8c3043e165a5d01c7f68da144ea 100644 (file)
@@ -39,27 +39,28 @@ public final class DefaultResourceTypes extends ExtensionProvider implements Bat
             .setProperty("hasRolePolicy", true)
             .setProperty("updatable_key", true)
             .setProperty("supports_measure_filters", true)
+            .setProperty("comparable", true)
             .build())
         .addType(ResourceType.builder(Qualifiers.MODULE)
             .setProperty("updatable_key", true)
             .setProperty("supports_measure_filters", true)
             .build())
         .addType(ResourceType.builder(Qualifiers.DIRECTORY)
-          .setProperty("supports_measure_filters", true)
-          .build())
+            .setProperty("supports_measure_filters", true)
+            .build())
         .addType(ResourceType.builder(Qualifiers.PACKAGE)
-          .build())
+            .build())
         .addType(ResourceType.builder(Qualifiers.FILE)
-          .hasSourceCode()
-          .setProperty("supports_measure_filters", true)
-          .build())
+            .hasSourceCode()
+            .setProperty("supports_measure_filters", true)
+            .build())
         .addType(ResourceType.builder(Qualifiers.CLASS)
-          .hasSourceCode()
-          .build())
+            .hasSourceCode()
+            .build())
         .addType(ResourceType.builder(Qualifiers.UNIT_TEST_FILE)
-          .hasSourceCode()
-          .setProperty("supports_measure_filters", true)
-          .build())
+            .hasSourceCode()
+            .setProperty("supports_measure_filters", true)
+            .build())
 
         .addRelations(Qualifiers.PROJECT, Qualifiers.MODULE)
         .addRelations(Qualifiers.MODULE, Qualifiers.DIRECTORY, Qualifiers.PACKAGE)
index c882eef84357d4020816535779da90541e1704bc..8becc0439de5091fd2dca33704762066ffe66b8e 100644 (file)
@@ -354,6 +354,7 @@ update_key.page=Update Key
 project_quality_profiles.page=Quality Profiles
 bulk_deletion.page=Bulk Deletion
 system_measure_filters.page=System Measure Filters
+comparison.page=Comparison
 
 
 # GWT pages
@@ -511,6 +512,24 @@ reviews.filtered_by.from=From date
 reviews.filtered_by.to=To date
 
 
+#------------------------------------------------------------------------------
+#
+# COMPARISON
+#
+#------------------------------------------------------------------------------
+
+comparison.compare=Compare
+comparison.add_a_new_metric=Add a new metric
+comparison.select_resource_to_compare=Select a resource to compare
+comparison.select_version=Select a version
+comparison.remove_resource=Remove resource
+comparison.remove_metric=Remove metric
+comparison.move_left=Move left
+comparison.move_right=Move right
+comparison.move_down=Move down
+comparison.move_up=Move up
+
+
 #------------------------------------------------------------------------------
 #
 # ACTION PLANS
index bab797e764b319abe0748a8df1066841fcb2ea1f..bda5124bff6d20401755fee6e5e8bbde5ff96271 100644 (file)
@@ -45,8 +45,10 @@ import java.util.Map;
  * <li>"updatable_key" (since 3.2): if set to "true", then it is possible to update the key of this resource</li>
  * <li>"supportsGlobalDashboards" (since 3.2): if true, this resource can be displayed in global dashboards</li>
  * <li>"hasRolePolicy" : if true, roles configuration is available in sidebar</li>
+ * <li>"comparable" (since 3.4) : if true, the resource can be compared to other resources</li>
  * </ul>
  *
+ * @see DefaultResourceTypes in Sonar Core Plugin to see the default resource types
  * @since 2.14
  */
 @Beta
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/comparison_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/comparison_controller.rb
new file mode 100644 (file)
index 0000000..6de0761
--- /dev/null
@@ -0,0 +1,87 @@
+#
+# 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 ComparisonController < ApplicationController
+
+  def index
+    snapshots = []
+    resource_key = params[:resource]
+    if resource_key && !resource_key.blank?
+      # the request comes from a project: let's select its 5 latest versions
+      project = Project.by_key(resource_key)
+      snapshots = project.events.select { |event| !event.snapshot_id.nil? && event.category==EventCategory::KEY_VERSION }[0..5].reverse.map {|e| e.snapshot}
+    else
+      # the request comes from the comparison page: let's compare the given snapshots
+      sids = get_params_as_array(:sids)
+      unless sids.empty?
+        selected_snapshots = Snapshot.find(:all, :conditions => ['id in (?)', sids])
+        # next loop is required to keep the order that was decided by the user and which comes from the "sids" parameter
+        sids.each do |id|
+          selected_snapshots.each do |s|
+            snapshots << s if id==s.id.to_s
+          end 
+        end
+      end
+    end    
+    @snapshots = select_authorized(:user, snapshots)
+    
+    metrics = get_params_as_array(:metrics)
+    if metrics.empty?
+      metrics = [        
+        'ncloc',
+        'complexity',
+        'comment_lines_density',
+        'duplicated_lines_density',
+        'violations',
+        'coverage'
+      ]
+    end
+    @metrics = Metric.by_keys(metrics)
+    
+    @metric_to_choose = Metric.all.select {|m| m.display? && !@metrics.include?(m)}.sort_by(&:short_name)
+    
+  end
+  
+  def versions
+    key = params[:resource]
+    sids = get_params_as_array(:sids)
+    
+    unless key.blank?
+      resource = Project.by_key(params[:resource])
+      # we look for the events that are versions and that are not linked to snapshots already displayed on the page
+      @versions = resource.events.select { |event| !event.snapshot_id.nil? && event.category==EventCategory::KEY_VERSION && !sids.include?(event.snapshot_id.to_s) }
+    end
+    
+    render :partial => 'versions'
+  end
+
+  
+  private
+  
+  def get_params_as_array(name)
+    list = params[name]
+    if list.blank?
+      []
+    else
+      list.split(',')
+    end
+  end
+
+end
\ No newline at end of file
index 76bc0264591d828f341927f11e1c592c0864d378..e44991b418930ee77c17bb076718787eb77fb352 100644 (file)
@@ -718,6 +718,7 @@ module ApplicationHelper
       # add a select <option> with empty value
       select_tag_prompt=''
     end
+    js_options.merge!(options[:select2_options]) if options[:select2_options]
 
     extra_values = options[:extra_values]
     metrics_by_domain={}
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/comparison/_versions.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/comparison/_versions.html.erb
new file mode 100644 (file)
index 0000000..813d8d4
--- /dev/null
@@ -0,0 +1,8 @@
+<% if @versions %>
+
+  <option value=""></option>
+  <% @versions.each do |version| %>
+    <option value="<%= version.snapshot_id -%>"><%= version.name -%></option>
+  <% end %>
+  
+<% end %>
\ No newline at end of file
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/comparison/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/comparison/index.html.erb
new file mode 100644 (file)
index 0000000..938441a
--- /dev/null
@@ -0,0 +1,216 @@
+<style type="text/css">
+  #comparison-page td {
+    vertical-align: middle;
+  }
+  
+  .move-actions img {
+    visibility: hidden;
+  }
+  
+  .move-actions:hover img {
+    visibility: visible;
+  }
+</style>
+
+<div id="comparison-page">
+  <form method="GET" id="compare-form" action="<%= ApplicationController.root_context -%>/comparison/index">
+    <input type="hidden" name="sids" id="sids" value="<%= @snapshots.map {|s| s.id.to_s}.join(',') -%>">
+    <input type="hidden" name="metrics" id="metrics" value="<%= @metrics.map {|m| m.key}.join(',') -%>">
+    
+    <table class="data">
+        
+      <thead>
+        <th class="thin" style="vertical-align: bottom">
+          <%= metric_select_tag 'new_metric', @metric_to_choose, {
+              :allow_empty => true, 
+              :select2_options => {'placeholder' => "'" + message('comparison.add_a_new_metric') + "'"}
+              } -%>
+          <script>
+            // we don't want this parameter to be submitted, so we set its name to ''
+            $j('#new_metric').attr('name', '');
+            $j('#new_metric').on("change", function(e) { 
+              if (e.val!=null) {
+                var currentMetrics = $j('#metrics').val();
+                if (currentMetrics.length > 0) {
+                  currentMetrics += ',';
+                }
+                currentMetrics += e.val;
+                $j('#metrics').val(currentMetrics);
+                $j('#compare-form').submit();
+              }
+            });
+          </script>
+        </th>
+        
+        <%
+          last_index = @snapshots.size-1 
+          @snapshots.each_with_index do |s, index| 
+        %>
+          <th class="thin move-actions" style="padding: 5px !important;">
+            <table>
+              <tr>
+                <td style="vertical-align: top;">
+                  <% if index > 0 %>
+                    <a href="#" onclick="moveLeft(<%= index -%>)"><img src="<%= ApplicationController.root_context -%>/images/controls/move_left.png" title="<%= message('comparison.move_left') -%>"/></a>
+                  <% else %>
+                    <img src="<%= ApplicationController.root_context -%>/images/transparent_16.gif"/>
+                  <% end %>
+                </td>
+                <td class="nowrap" style="text-align: center;">
+                  <div style="width: 100%; text-align: center;"><a href="#" onclick="removeFromList(<%= index -%>, $j('#sids'))"><img src="<%= ApplicationController.root_context -%>/images/controls/cross.png" title="<%= message('comparison.remove_resource') -%>"/></a></div>
+                  <a href="<%= ApplicationController.root_context -%>/dashboard/index/<%= s.resource.key -%>"><%= s.resource.name(true) -%></a>
+                  <br/>
+                  <span class="note"><b><%= s.event(EventCategory::KEY_VERSION).name -%></b></span>
+                  <br/>
+                  <span class="note"><%= human_short_date s.created_at -%></span>
+                </td>
+                <td class="thin" style="vertical-align: top;">
+                  <% if index < last_index %>
+                    <a href="#" onclick="moveRight(<%= index -%>)"><img src="<%= ApplicationController.root_context -%>/images/controls/move_right.png" title="<%= message('comparison.move_right') -%>"/></a>
+                  <% else %>
+                    <img src="<%= ApplicationController.root_context -%>/images/transparent_16.gif"/>
+                  <% end %>
+                </td>
+              </tr>
+            </table>
+          </th>
+        <% end %>
+        
+        <th style="padding-left: 20px; vertical-align: bottom">
+          <%= resource_select_tag 'new_resource', {
+              :resource_type_property => 'comparable',
+              :width => '250px', 
+              :select2_options => {'placeholder' => "'" + message('comparison.select_resource_to_compare') + "'"}
+              } -%>
+          <script>
+            // we don't want this parameter to be submitted, so we set its name to ''
+            $j('#new_resource').attr('name', '');
+            $j('#new_resource').on("change", function(e) { 
+              if (e.val!=null) {
+                $j.ajax({
+                     type: 'GET',
+                     url: '<%= ApplicationController.root_context -%>/comparison/versions?resource=' 
+                          + e.val + '&sids='
+                          + $j('#sids').val(),
+                     success: function(data){
+                                 $j('#new_version').html(data);
+                                 $j('#new_version').select2({placeholder: '<%= message('comparison.select_version') -%>'});
+                                 $j('#version_div').show();
+                                 $j('#new_version').select2("focus");
+                              }
+               });
+              }
+            });
+            $j('#new_resource').select2("focus");
+          </script>
+          
+          <span id="version_div" style="display: none">
+            <select id="new_version">
+            </select>
+            <script>
+              $j('#new_version').on("change", function(e) {
+                if (e.val!=null) {
+                  var currentSnapshotIds = $j('#sids').val();
+                  if (currentSnapshotIds.length > 0) {
+                    currentSnapshotIds += ',';
+                  }
+                  currentSnapshotIds += e.val;
+                  $j('#sids').val(currentSnapshotIds);
+                  $j('#compare-form').submit();
+                }
+              });
+            </script>
+          </span>  
+        </th>
+        
+        <th class="thin" style="vertical-align: top">
+          <a href="<%= url_for :controller => 'comparison', :action => 'index', :sids => @snapshots.map {|s| s.id.to_s}.join(','), :metrics => @metrics.map {|m| m.key}.join(',') -%>">
+            <img src="<%= ApplicationController.root_context -%>/images/permalink.png" title="<%= message('permalink') -%>"/>
+          </a>
+        </th>
+      </thead>
+        
+      <tbody>
+        <% 
+          last_index = @metrics.size-1 
+          @metrics.each_with_index do |m, index|
+        %>
+          <tr class="<%= cycle 'even', 'odd' -%> move-actions">
+            <td>
+              <div style="float: left; vertical-align: bottom;"><%= m.short_name -%></div>
+              <div style="float: right">
+                <% if index > 0 %>
+                  <a href="#" onclick="moveUp(<%= index -%>)"><img src="<%= ApplicationController.root_context -%>/images/controls/move_up.png" title="<%= message('comparison.move_up') -%>"/></a>
+                <% end %>
+                <% if index < last_index %>
+                  <a href="#" onclick="moveDown(<%= index -%>)"><img src="<%= ApplicationController.root_context -%>/images/controls/move_down.png" title="<%= message('comparison.move_down') -%>"/></a>
+                <% end %>
+                <a href="#" onclick="removeFromList(<%= index -%>, $j('#metrics'))"><img src="<%= ApplicationController.root_context -%>/images/controls/cross.png" title="<%= message('comparison.remove_metric') -%>"/></a>
+              </div>
+            </td>
+            
+            <% @snapshots.each do |s| %>
+              <td style="text-align: center">
+                <%= format_measure s.measure(m) -%>
+              </td>
+            <% end %>
+            
+            <td></td>
+            <td></td>
+          </tr>
+        <% end %> 
+      </tbody>
+        
+    </table>
+    
+  <form>
+</div>
+
+<script>
+  function submitForm() {
+    $j('#compare-form').submit();
+  }
+  
+  function moveLeft(index) {
+    sids = $j('#sids').val().split(',');
+    idToLeft = sids[index];
+    idToRight = sids[index-1];
+    sids.splice(index-1, 2, [idToLeft, idToRight]);
+    $j('#sids').val(sids.join(','));
+    submitForm();
+  }
+  
+  function moveRight(index) {
+    sids = $j('#sids').val().split(',');
+    idToRight = sids[index];
+    idToLeft = sids[index+1];
+    sids.splice(index, 2, [idToLeft, idToRight]);
+    $j('#sids').val(sids.join(','));
+    submitForm();
+  }
+  
+  function moveUp(index) {
+    metrics = $j('#metrics').val().split(',');
+    idToUp = metrics[index];
+    idToBottom = metrics[index-1];
+    metrics.splice(index-1, 2, [idToUp, idToBottom]);
+    $j('#metrics').val(metrics.join(','));
+    submitForm();
+  }
+  
+  function moveDown(index) {
+    metrics = $j('#metrics').val().split(',');
+    idToBottom = metrics[index];
+    idToUp = metrics[index+1];
+    metrics.splice(index, 2, [idToUp, idToBottom]);
+    $j('#metrics').val(metrics.join(','));
+    submitForm();
+  }
+  
+  function removeFromList(index, inputField) {
+    value = inputField.val().split(',');
+    value.splice(index, 1);
+    inputField.val(value.join(','));
+    submitForm();
+  }
+</script>
index 9fb3b7bb9cbf33d43a85adc22248e7e657f595dc..ef763e402764b746bd68de05d2a0df0b8546a0c6 100644 (file)
               <a href="<%= ApplicationController.root_context -%>/components/index/<%= @project.id -%>"><%= message('components.page') -%></a></li>
             <li class="<%= 'selected' if request.request_uri.include?('/drilldown/violations') -%>">
               <a href="<%= ApplicationController.root_context -%>/drilldown/violations/<%= @project.id -%><%= "?"+period_param if period_param -%>"><%= message('violations_drilldown.page') -%></a>
+            <% if controller.java_facade.getResourceTypeBooleanProperty(@project.qualifier, 'comparable') %>
+            <li class="<%= 'selected' if request.request_uri.include?('/comparison/index') -%>">
+              <a href="<%= ApplicationController.root_context -%>/comparison/index?resource=<%= @project.key -%>"><%= message('comparison.page') -%></a></li>
+            <% end %>
             <li class="<%= 'selected' if request.request_uri.include?('/cloud/index') -%>">
               <a href="<%= ApplicationController.root_context -%>/cloud/index/<%= @project.id -%>"><%= message('clouds.page') -%></a></li>
             <% controller.java_facade.getPages(Navigation::SECTION_RESOURCE, @project.scope, @project.qualifier, @project.language, @project.last_snapshot.metric_keys.to_java(:string)).each do |page|
index b1bca085e65e1fc1b46fb7494c713878b58173eb..a5c93c4ecf02434ce6e74c352ab50d6a357a78e0 100644 (file)
@@ -4,6 +4,7 @@
   <div id="tools-menu" class="dropdown-menu" style="display: none">
     <ul>
       <li><a href="<%= ApplicationController.root_context -%>/dependencies/index"><%= message('dependencies.page') -%></a></li>
+      <li><a href="<%= ApplicationController.root_context -%>/comparison/index"><%= message('comparison.page') -%></a></li>
       <li><a href="<%= ApplicationController.root_context -%>/reviews/index"><%= message('reviews.page') -%></a></li>
     </ul>
   </div>
diff --git a/sonar-server/src/main/webapp/images/controls/move_down.png b/sonar-server/src/main/webapp/images/controls/move_down.png
new file mode 100644 (file)
index 0000000..2ae468a
Binary files /dev/null and b/sonar-server/src/main/webapp/images/controls/move_down.png differ
diff --git a/sonar-server/src/main/webapp/images/controls/move_left.png b/sonar-server/src/main/webapp/images/controls/move_left.png
new file mode 100644 (file)
index 0000000..8c166e8
Binary files /dev/null and b/sonar-server/src/main/webapp/images/controls/move_left.png differ
diff --git a/sonar-server/src/main/webapp/images/controls/move_right.png b/sonar-server/src/main/webapp/images/controls/move_right.png
new file mode 100644 (file)
index 0000000..5c9a1df
Binary files /dev/null and b/sonar-server/src/main/webapp/images/controls/move_right.png differ
diff --git a/sonar-server/src/main/webapp/images/controls/move_up.png b/sonar-server/src/main/webapp/images/controls/move_up.png
new file mode 100644 (file)
index 0000000..83c0057
Binary files /dev/null and b/sonar-server/src/main/webapp/images/controls/move_up.png differ