diff options
8 files changed, 382 insertions, 2 deletions
diff --git a/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties b/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties index fb9a261cbb9..7a7d91c3bc6 100644 --- a/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties +++ b/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties @@ -332,6 +332,7 @@ dependencies.page=Dependencies resource_deletion.page={0} Deletion update_key.page=Update Key project_quality_profile.page=Quality Profile +bulk_deletion.page=Bulk Deletion # GWT pages @@ -1248,6 +1249,26 @@ my_profile.password.wrong_old=Wrong old password #------------------------------------------------------------------------------ # +# BULK RESOURCE DELETION +# +#------------------------------------------------------------------------------ +bulk_deletion.resource.projects=Projects +bulk_deletion.resource.views=Views +bulk_deletion.resource.devs=Developers +bulk_deletion.no_resource_to_delete=No resource to delete +bulk_deletion.resource_name_filter_by_name=Filter resources by name +bulk_deletion.search=Search +bulk_deletion.page_size=Page size +bulk_deletion.select_all=Select all +bulk_deletion.select_all_x_resources=Select all {0} resources +bulk_deletion.clear_selection=Clear selection of all {0} resources +bulk_deletion.following_deletions_failed=The following resources could not be deleted. Please check the logs to know more about it. +bulk_deletion.hide_message=Hide message +bulk_deletion.sure_to_delete_the_resources=Are you sure you want to delete the selected resources? + + +#------------------------------------------------------------------------------ +# # TREEMAP # #------------------------------------------------------------------------------ diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java index 4c126d653ca..f930f742a0a 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java @@ -447,7 +447,12 @@ public final class JRubyFacade { } public void deleteResourceTree(long rootProjectId) { - getContainer().getComponentByType(PurgeDao.class).deleteResourceTree(rootProjectId); + try { + getContainer().getComponentByType(PurgeDao.class).deleteResourceTree(rootProjectId); + } catch (RuntimeException e) { + LoggerFactory.getLogger(JRubyFacade.class).error("Fail to delete resource with ID: " + rootProjectId, e); + throw e; + } } public void logError(String message) { diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/bulk_deletion_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/bulk_deletion_controller.rb new file mode 100644 index 00000000000..a7d74f7887d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/bulk_deletion_controller.rb @@ -0,0 +1,92 @@ +# +# 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 BulkDeletionController < ApplicationController + + SECTION=Navigation::SECTION_CONFIGURATION + + before_filter :admin_required + verify :method => :post, :only => [:delete_resources], :redirect_to => { :action => :index } + + def index + deletion_manager = ResourceDeletionManager.instance + + if deletion_manager.currently_deleting_resources? || + (!deletion_manager.currently_deleting_resources? && deletion_manager.deletion_failures_occured?) + # a mass deletion is happening or it has just finished with errors => display the message from the Resource Deletion Manager + @deletion_manager = deletion_manager + render :template => 'bulk_deletion/pending_deletions' + else + @selected_tab = params[:resource_type] || 'projects' + + # search if there are VIEWS or DEVS to know if we should display the tabs or not + @should_display_views_tab = Project.count(:all, :conditions => {:qualifier => 'VW'}) > 0 + @should_display_devs_tab = Project.count(:all, :conditions => {:qualifier => 'DEV'}) > 0 + + # Search for resources + conditions = "scope=:scope AND qualifier=:qualifier" + values = {:scope => 'PRJ'} + qualifier = 'TRK' + if @selected_tab == 'views' + qualifier = 'VW' + elsif @selected_tab == 'devs' + qualifier = 'DEV' + end + values[:qualifier] = qualifier + if params[:name_filter] + conditions += " AND name LIKE :name" + values[:name] = '%' + params[:name_filter].strip + '%' + end + + @resources = Project.find(:all, :conditions => [conditions, values]) + end + end + + def delete_resources + resource_to_delete = params[:resources] || [] + resource_to_delete = params[:all_resources].split(',') if params[:all_resources] && !params[:all_resources].blank? + + # Ask the resource deletion manager to start the migration + # => this is an asynchronous AJAX call + ResourceDeletionManager.instance.delete_resources(resource_to_delete) + + # and return some text that will actually never be displayed + render :text => ResourceDeletionManager.instance.message + end + + def pending_deletions + deletion_manager = ResourceDeletionManager.instance + + if deletion_manager.currently_deleting_resources? || + (!deletion_manager.currently_deleting_resources? && deletion_manager.deletion_failures_occured?) + # display the same page again and again + @deletion_manager = deletion_manager + else + redirect_to :action => 'index' + end + end + + def dismiss_message + # It is important to reinit the ResourceDeletionManager so that the deletion screens can be available again + ResourceDeletionManager.instance.reinit + + redirect_to :action => 'index' + end + +end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/database_migration_manager.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/database_migration_manager.rb index 5d412ea6284..f68e8abadba 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/database_migration_manager.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/database_migration_manager.rb @@ -19,7 +19,7 @@ # # -# Class taht centralizes the management the DB migration +# Class that centralizes the management the DB migration # require 'singleton' diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/resource_deletion_manager.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/resource_deletion_manager.rb new file mode 100644 index 00000000000..f828cc3e085 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/resource_deletion_manager.rb @@ -0,0 +1,110 @@ +# +# 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 that centralizes the management of resource deletions in Sonar Web UI +# + +require 'singleton' +require 'thread' + +class ResourceDeletionManager + + # mixin the singleton module to ensure we have only one instance of the class + # it will be accessible with "ResourceDeletionManager.instance" + include Singleton + + # the status of the migration + @status + AVAILABLE = "AVAILABLE" + WORKING = "WORKING" + + # the corresponding message that can be given to the user + @message + + # list of resources that could not be deleted because of a problem + @failed_deletions + + def initialize + reinit() + end + + def reinit + @message = nil + @status = AVAILABLE + @failed_deletions = [] + end + + def message + @message + end + + def currently_deleting_resources? + @status==WORKING + end + + def deletion_failures_occured? + !failed_deletions.empty? + end + + def failed_deletions + @failed_deletions + end + + def delete_resources(resource_ids=[]) + # Use an exclusive block of code to ensure that only 1 thread will be able to proceed with the deletion + can_start_deletion = false + Thread.exclusive do + unless currently_deleting_resources? + reinit() + @status = WORKING + @message = "Deleting resources..." + can_start_deletion = true + end + end + + if can_start_deletion + if resource_ids.empty? + @status = AVAILABLE + @message = "No resource to delete." + else + java_facade = Java::OrgSonarServerUi::JRubyFacade.getInstance() + # launch the deletion + resource_ids.each_with_index do |resource_id, index| + resource = Project.find(:first, :conditions => {:id => resource_id.to_i}) + @message = "Currently deleting resources... (" + (index+1).to_s + " out of " + resource_ids.size.to_s + ")" + if resource && + # next line add 'VW' and 'DEV' tests because those resource types don't have the 'deletable' property yet... + (java_facade.getResourceTypeBooleanProperty(resource.qualifier, 'deletable') || resource.qualifier=='VW' || resource.qualifier=='DEV') + begin + java_facade.deleteResourceTree(resource.id) + rescue Exception => e + @failed_deletions << resource.name + # no need to rethrow the exception as it has been logged by the JRubyFacade + end + end + end + @status = AVAILABLE + @message = "Resource deletion completed." + end + end + end + +end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/index.html.erb new file mode 100644 index 00000000000..80ce6a6f31d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/index.html.erb @@ -0,0 +1,116 @@ +<h1 class="marginbottom10"><%= message('bulk_deletion.page') -%></h1> + +<ul class="tabs"> + <li> + <a href="<%= url_for :action => 'index', :resource_type => 'projects' %>" <%= "class='selected'" if @selected_tab=='projects' -%>><%= message('bulk_deletion.resource.projects') -%></a> + </li> + <% if @should_display_views_tab %> + <li> + <a href="<%= url_for :action => 'index', :resource_type => 'views' -%>" <%= "class='selected'" if @selected_tab=='views' -%>><%= message('bulk_deletion.resource.views') -%></a> + </li> + <% end %> + <% if @should_display_devs_tab %> + <li> + <a href="<%= url_for :action => 'index', :resource_type => 'devs' -%>" <%= "class='selected'" if @selected_tab=='devs' -%>><%= message('bulk_deletion.resource.devs') -%></a> + </li> + <% end %> +</ul> + +<div class="tabs-panel marginbottom10"> + +<% + found_resources_count = @resources.size + found_resources_ids = @resources.map {|r| r.id.to_s}.join(',') + page_size = (params[:page_size] && params[:page_size].to_i) || 20 +%> + + <% form_tag( {:action => 'index'}, :method => :get ) do %> + <%= message('bulk_deletion.resource_name_filter_by_name') -%>: <input type="text" id="resource_filter" name="name_filter" size="40px" value=""/> + <input type="hidden" name="resource_type" value="<%= @selected_tab -%>"/> + <%= submit_tag message('bulk_deletion.search'), :id => 'filter_resources' %> + <% end %> + + <% if @resources.empty? %> + <br/> + <%= message('bulk_deletion.no_resource_to_delete') -%> + <% else %> + + <% form_remote_tag( :url => {:action => 'delete_resources'}, :loading => "window.location='#{url_for :action => 'pending_deletions'}';") do %> + + <table class="data"> + <tfoot> + <tr> + <td colspan="2"><%= paginate @resources, {:page_size => page_size} %></td> + </tr> + <tr> + <td colspan="2"> + <%= submit_tag message('delete'), :id => 'delete_resources', :class => 'action red-button', :confirm => message('bulk_deletion.sure_to_delete_the_resources') %> + </td> + </tr> + </tfoot> + <thead> + <tr> + <th><input id="r-all" type="checkbox" onclick="selectOrDeselect()"></th> + <th> + <span>« <%= message('bulk_deletion.select_all') -%></span> + <% if found_resources_count - @resources.size > 0 %> + <a id="select_all_action" style="padding-left: 10px; font-weight: normal; display: none" + href="#" onclick="handleSelectAllAction(); return false;"><%= message('bulk_deletion.select_all_x_resources', :params => found_resources_count) -%></a> + <input type="hidden" id="all_resources" name="all_resources" value=""/> + <% end %> + </th> + </tr> + </thead> + <tbody> + <% @resources.each_with_index do |resource, index| %> + <tr class="<%= cycle 'even', 'odd' -%>"> + <td class="thin"> + <input id="r-<%= index -%>" type="checkbox" value="<%= resource.id -%>" name="resources[]"> + </td> + <td><%= resource.name -%></td> + </tr> + <% end %> + </tbody> + </table> + + <% end %> + + <script> + function selectOrDeselect() { + status = $('r-all').checked + $$('tbody input').each(function(input) { + input.checked = status; + }); + <% if found_resources_count - @resources.size > 0 %> + selectNotAllResources(); + if (status) { + $('select_all_action').show(); + } else { + $('select_all_action').hide(); + } + <% end %> + } + + function handleSelectAllAction() { + if ($('all_resources').value=='') { + selectAllResources(); + } else { + $('r-all').checked = false; + selectOrDeselect(); + } + } + + function selectAllResources() { + $('all_resources').value = '<%= found_resources_ids -%>'; + $('select_all_action').text = '<%= message('bulk_deletion.clear_selection', :params => found_resources_count) -%>'; + } + + function selectNotAllResources() { + $('all_resources').value = ''; + $('select_all_action').text = '<%= message('bulk_deletion.select_all_x_resources', :params => found_resources_count) -%>'; + } + </script> + + <% end %> + +</div>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/pending_deletions.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/pending_deletions.html.erb new file mode 100644 index 00000000000..e5e67a6d57d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/pending_deletions.html.erb @@ -0,0 +1,34 @@ +<% + pending_deletions = @deletion_manager.currently_deleting_resources? + failed_deletions = @deletion_manager.failed_deletions +%> + +<% if pending_deletions %> + <meta http-equiv='refresh' content='5;'> +<% end %> + +<h1 class="marginbottom10"><%= message('bulk_deletion.page') -%></h1> + +<div class="<%= pending_deletions ? 'admin' : 'error' -%>" style="padding:10px"> + <% if pending_deletions %> + <%= image_tag 'loading.gif' -%> + <% end %> + + <b><%= @deletion_manager.message -%></b> + <br/> + <br/> + + <% if !pending_deletions && !failed_deletions.empty? %> + <p> + <%= message('bulk_deletion.following_deletions_failed') -%> + <ul style="list-style: none outside; padding-left: 30px;"> + <% failed_deletions.each do |name| %> + <li style="list-style: disc outside; padding: 2px;"><%= name -%></li> + <% end %> + </ul> + </p> + <p> + <%= link_to message('bulk_deletion.hide_message'), :action => 'dismiss_message' -%> + </p> + <% end %> +</div>
\ No newline at end of file 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 37a9379ace6..989ec8f92ca 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 @@ -158,6 +158,8 @@ <a href="<%= ApplicationController.root_context -%>/settings/index"><%= message('settings.page') -%></a></li> <li class="<%= 'selected' if controller.controller_path=='backup' -%>"><a href="<%= ApplicationController.root_context -%>/backup"><%= message('backup.page') -%></a> </li> + <li class="<%= 'selected' if controller.controller_path=='bulk_deletion' -%>"><a href="<%= ApplicationController.root_context -%>/bulk_deletion"><%= message('bulk_deletion.page') -%></a> + </li> <li class="<%= 'selected' if controller.controller_path=='system' -%>"> <a href="<%= ApplicationController.root_context -%>/system"><%= message('system_info.page') -%></a></li> <% update_center_activated = controller.java_facade.getSettings().getBoolean('sonar.updatecenter.activate') |