]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2614 Provide a bulk project deletion page
authorFabrice Bellingard <bellingard@gmail.com>
Wed, 4 Jul 2012 08:16:36 +0000 (10:16 +0200)
committerFabrice Bellingard <bellingard@gmail.com>
Wed, 4 Jul 2012 08:18:21 +0000 (10:18 +0200)
plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties
sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/bulk_deletion_controller.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/models/database_migration_manager.rb
sonar-server/src/main/webapp/WEB-INF/app/models/resource_deletion_manager.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/index.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/bulk_deletion/pending_deletions.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb

index fb9a261cbb9d600623a5852dbddb4581680d13cc..7a7d91c3bc6b91511679681aa083b2e7e5243598 100644 (file)
@@ -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
@@ -1246,6 +1247,26 @@ my_profile.password.mismatch=Password mismatch
 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
index 4c126d653ca40dbd950b8c083e6f0cfd2f3ef1f6..f930f742a0a47216cc426c76c32b3de3d37a195b 100644 (file)
@@ -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 (file)
index 0000000..a7d74f7
--- /dev/null
@@ -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
index 5d412ea62844e137d65d78e17a1af93263cec16d..f68e8abadbaccaf00f8989945fd63b110040b820 100644 (file)
@@ -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 (file)
index 0000000..f828cc3
--- /dev/null
@@ -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 (file)
index 0000000..80ce6a6
--- /dev/null
@@ -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>&laquo; <%= 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 (file)
index 0000000..e5e67a6
--- /dev/null
@@ -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
index 37a9379ace6288b158c9879c4c37218ed4183b82..989ec8f92ca058cdcebc33254c6a75c190a93215 100644 (file)
                 <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')