diff options
author | Fabrice Bellingard <fabrice.bellingard@sonarsource.com> | 2012-11-29 11:28:02 +0100 |
---|---|---|
committer | Fabrice Bellingard <fabrice.bellingard@sonarsource.com> | 2012-11-29 13:46:38 +0100 |
commit | c0449e0a612fc44c4a4bff06a35c1f744c898608 (patch) | |
tree | 322fce23a111977b4c27933c6f67576e0553998a | |
parent | 9322b252de6c294af1b54377c9df3a7c737771f8 (diff) | |
download | sonarqube-c0449e0a612fc44c4a4bff06a35c1f744c898608.tar.gz sonarqube-c0449e0a612fc44c4a4bff06a35c1f744c898608.zip |
SONAR-37 & SONAR-2911 Add comparison tool
- To compare projects to each others (SONAR-37)
- To compare X versions of a project (SONAR-2911)
13 files changed, 350 insertions, 11 deletions
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DefaultResourceTypes.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DefaultResourceTypes.java index bc3125b2d07..72bd2cb244e 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DefaultResourceTypes.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/DefaultResourceTypes.java @@ -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) diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties b/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties index c882eef8435..8becc0439de 100644 --- a/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties @@ -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 @@ -513,6 +514,24 @@ 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 # #------------------------------------------------------------------------------ diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/resources/ResourceType.java b/sonar-plugin-api/src/main/java/org/sonar/api/resources/ResourceType.java index bab797e764b..bda5124bff6 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/resources/ResourceType.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/resources/ResourceType.java @@ -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 index 00000000000..6de07617062 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/comparison_controller.rb @@ -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 diff --git a/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb b/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb index 76bc0264591..e44991b4189 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/helpers/application_helper.rb @@ -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 index 00000000000..813d8d4ba12 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/comparison/_versions.html.erb @@ -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 index 00000000000..938441aaff4 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/comparison/index.html.erb @@ -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> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb index 9fb3b7bb9cb..ef763e40276 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb @@ -80,6 +80,10 @@ <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| diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_tools.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_tools.html.erb index b1bca085e65..a5c93c4ecf0 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_tools.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_tools.html.erb @@ -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 Binary files differnew file mode 100644 index 00000000000..2ae468a07e2 --- /dev/null +++ b/sonar-server/src/main/webapp/images/controls/move_down.png diff --git a/sonar-server/src/main/webapp/images/controls/move_left.png b/sonar-server/src/main/webapp/images/controls/move_left.png Binary files differnew file mode 100644 index 00000000000..8c166e875cf --- /dev/null +++ b/sonar-server/src/main/webapp/images/controls/move_left.png diff --git a/sonar-server/src/main/webapp/images/controls/move_right.png b/sonar-server/src/main/webapp/images/controls/move_right.png Binary files differnew file mode 100644 index 00000000000..5c9a1df1d6a --- /dev/null +++ b/sonar-server/src/main/webapp/images/controls/move_right.png diff --git a/sonar-server/src/main/webapp/images/controls/move_up.png b/sonar-server/src/main/webapp/images/controls/move_up.png Binary files differnew file mode 100644 index 00000000000..83c005763c2 --- /dev/null +++ b/sonar-server/src/main/webapp/images/controls/move_up.png |