2872: Merge branch 'master' into 2872-folder-nav
authorTom Clegg <tom@curoverse.com>
Thu, 12 Jun 2014 03:57:37 +0000 (23:57 -0400)
committerTom Clegg <tom@curoverse.com>
Thu, 12 Jun 2014 03:57:37 +0000 (23:57 -0400)
Conflicts:
apps/workbench/app/assets/javascripts/pipeline_instances.js
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/controllers/jobs_controller.rb
apps/workbench/app/helpers/application_helper.rb
apps/workbench/app/models/job.rb
apps/workbench/app/views/application/_content.html.erb
apps/workbench/app/views/application/_show_metadata.html.erb
apps/workbench/app/views/pipeline_instances/_show_components.html.erb

17 files changed:
1  2 
apps/workbench/app/assets/javascripts/application.js
apps/workbench/app/assets/javascripts/editable.js
apps/workbench/app/assets/javascripts/pipeline_instances.js
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/controllers/groups_controller.rb
apps/workbench/app/controllers/jobs_controller.rb
apps/workbench/app/controllers/pipeline_instances_controller.rb
apps/workbench/app/helpers/application_helper.rb
apps/workbench/app/models/arvados_base.rb
apps/workbench/app/models/job.rb
apps/workbench/app/views/application/_content.html.erb
apps/workbench/app/views/application/_show_advanced_metadata.html.erb
apps/workbench/app/views/collections/show.html.erb
apps/workbench/app/views/layouts/application.html.erb
apps/workbench/config/routes.rb
apps/workbench/test/integration/users_test.rb

index 7274b3b09301b9ceaf6d03b0dc1e8105b9e2f178,7b09d5242cb5283f971ca216827c3105f55cc731..e35e93c5d33af1f78a0d4ef6298d827b11095ffc
@@@ -43,12 -43,20 +43,20 @@@ jQuery(function($)
          targets.fadeToggle(200);
      });
  
+     var ajaxCount = 0;
      $(document).
          on('ajax:send', function(e, xhr) {
-             $('.loading').fadeTo('fast', 1);
+             ajaxCount += 1;
+             if (ajaxCount == 1) {
+                 $('.loading').fadeTo('fast', 1);
+             }
          }).
          on('ajax:complete', function(e, status) {
-             $('.loading').fadeOut('fast', 0);
+             ajaxCount -= 1;
+             if (ajaxCount == 0) {
+                 $('.loading').fadeOut('fast', 0);
+             }
          }).
          on('click', '.removable-tag a', function(e) {
              var tag_span = $(this).parents('[data-tag-link-uuid]').eq(0)
              $('.btn').button();
          });
  
 +    $(document).
 +        on('ready ajax:complete', function() {
 +            $('[data-toggle~=tooltip]').tooltip({container:'body'});
 +        });
 +
      HeaderRowFixer = function(selector) {
          this.duplicateTheadTr = function() {
              $(selector).each(function() {
index 093a671acd10799eadf1c651e96b706f6b751141,16bb7f6cd87e9662d0c8e0774605e4baf1e6310f..ab66833c286a6c4666c3162309fe8267be03fea9
@@@ -41,9 -41,10 +41,9 @@@ $.fn.editable.defaults.validate = funct
  
  $(document).
      on('ready ajax:complete', function() {
 -        $('#editable-submit').click(function() {
 -            console.log($(this));
 -        });
          $('.editable').
 +            not('.editable-done-setup').
 +            addClass('editable-done-setup').
              editable({
                  success: function(response, newValue) {
                      // If we just created a new object, stash its UUID
@@@ -56,6 -57,9 +56,9 @@@
                          $(this).editable('option', 'url', response.href);
                      }
                      return;
+                 },
+                 error: function(response, newValue) {
+                     return response.responseJSON.errors.join();
                  }
              }).
              on('hidden', function(e, reason) {
                        });
                  }
              });
 +    }).
 +    on('ready ajax:complete', function() {
 +        $("[data-toggle~='x-editable']").
 +            not('.editable-done-setup').
 +            addClass('editable-done-setup').
 +            click(function(e) {
 +                e.stopPropagation();
 +                $($(this).attr('data-toggle-selector')).editable('toggle');
 +            });
      });
  
  $.fn.editabletypes.text.defaults.tpl = '<input type="text" name="editable-text">'
index 54595ab4f53ee4978427835b91511c34fad95d5d,c61e336c7ae2377c343fa159b6c9c39abf4fc33d..f206213ed2e97be81acf97b4f12b1cb059d02227
@@@ -1,5 -1,5 +1,5 @@@
  function run_pipeline_button_state() {
 -    var a = $('a.editable.required.editable-empty');
 +    var a = $('a.editable.required.editable-empty,input.form-control.required[value=]');
      if (a.length > 0) {
          $(".run-pipeline-button").addClass("disabled");
      }
@@@ -47,51 -47,19 +47,42 @@@ $(document).on('ready ajax:complete', f
      run_pipeline_button_state();
  });
  
- $(document).on('ajax:complete ready', function() {
-   var a = $('.arv-log-event-listener');
-   if (a.length > 0) {
-     $('.arv-log-event-listener').each(function() {
-       subscribeToEventLog(this.id);
-     });
-   }
- });
  $(document).on('arv-log-event', '.arv-log-event-handler-append-logs', function(event, eventData){
 -  var parsedData = JSON.parse(eventData);
 +    var wasatbottom = ($(this).scrollTop() + $(this).height() >=
 +                       this.scrollHeight);
 +    var parsedData = JSON.parse(eventData);
 +    var propertyText = undefined;
 +    var properties = parsedData.properties;
  
 -  var propertyText = undefined
 -
 -  var properties = parsedData.properties;
      if (properties !== null) {
 -      propertyText = properties.text;
 +        propertyText = properties.text;
      }
 -
      if (propertyText !== undefined) {
 -      $(this).append(propertyText + "<br/>");
 +        $(this).append(propertyText + "<br/>");
      } else {
 -      $(this).append(parsedData.summary + "<br/>");
 +        $(this).append(parsedData.summary + "<br/>");
      }
 +    if (wasatbottom)
 +        this.scrollTop = this.scrollHeight;
 +}).on('ready ajax:complete', function(){
 +    $('.arv-log-event-handler-append-logs').each(function() {
 +        this.scrollTop = this.scrollHeight;
 +    });
  });
 +
 +var showhide_compare = function() {
 +    var form = $('form#compare')[0];
 +    $('input[type=hidden][name="uuids[]"]', form).remove();
 +    $('input[type=submit]', form).prop('disabled',true).show();
 +    var checked_inputs = $('[data-object-uuid*=-d1hrv-] input[name="uuids[]"]:checked');
 +    if (checked_inputs.length >= 2 && checked_inputs.length <= 3) {
 +        checked_inputs.each(function(){
 +            if(this.checked) {
 +                $('input[type=submit]', form).prop('disabled',false).show();
 +                $(form).append($('<input type="hidden" name="uuids[]"/>').val(this.value));
 +            }
 +        });
 +    }
 +};
 +$('[data-object-uuid*=-d1hrv-] input[name="uuids[]"]').on('click', showhide_compare);
 +showhide_compare();
index 48b508a4a6e305d51764c6fd5ef4f94d43b76cfb,a0cadb2b4c08e91356455b49a6cff4eac5a80360..6457cd0013456d6eac0831d54ffbe2744fddf56d
@@@ -1,6 -1,5 +1,6 @@@
  class ApplicationController < ActionController::Base
    include ArvadosApiClientHelper
 +  include ApplicationHelper
  
    respond_to :html, :json, :js
    protect_from_forgery
      self.render_error status: 404
    end
  
 -  def render_index
 -    respond_to do |f|
 -      f.json { render json: @objects }
 -      f.html {
 -        if params['tab_pane']
 -          comparable = self.respond_to? :compare
 -          render(partial: 'show_' + params['tab_pane'].downcase,
 -                 locals: { comparable: comparable, objects: @objects })
 -        else
 -          render
 -        end
 -      }
 -      f.js { render }
 -    end
 -  end
 -
 -  def index
 +  def find_objects_for_index
      @limit ||= 200
      if params[:limit]
        @limit = params[:limit].to_i
      end
  
      @objects ||= model_class
 -    @objects = @objects.filter(@filters).limit(@limit).offset(@offset).all
 +    @objects = @objects.filter(@filters).limit(@limit).offset(@offset)
 +  end
 +
++  def render_index
++    respond_to do |f|
++      f.json { render json: @objects }
++      f.html {
++        if params['tab_pane']
++          comparable = self.respond_to? :compare
++          render(partial: 'show_' + params['tab_pane'].downcase,
++                 locals: { comparable: comparable, objects: @objects })
++        else
++          render
++        end
++      }
++      f.js { render }
++    end
++  end
++
++  def index
++    find_objects_for_index if !@objects
+     render_index
+   end
 +  helper_method :next_page_offset
 +  def next_page_offset
 +    if @objects.respond_to?(:result_offset) and
 +        @objects.respond_to?(:result_limit) and
 +        @objects.respond_to?(:items_available)
 +      next_offset = @objects.result_offset + @objects.result_limit
 +      if next_offset < @objects.items_available
 +        next_offset
 +      else
 +        nil
 +      end
 +    end
 +  end
 +
-   def index
-     find_objects_for_index if !@objects
-     respond_to do |f|
-       f.json { render json: @objects }
-       f.html { render }
-       f.js { render }
-     end
-   end
    def show
      if !@object
        return render_not_found("object not found")
      respond_to do |f|
        f.json { render json: @object.attributes.merge(href: url_for(@object)) }
        f.html {
-         if request.method.in? ['GET', 'HEAD']
+         if params['tab_pane']
+           comparable = self.respond_to? :compare
+           render(partial: 'show_' + params['tab_pane'].downcase,
+                  locals: { comparable: comparable, objects: @objects })
++        elsif request.method.in? ['GET', 'HEAD']
 +          render
          else
 -          if request.method == 'GET'
 -            render
 -          else
 -            redirect_to params[:return_to] || @object
 -          end
 +          redirect_to params[:return_to] || @object
          end
        }
        f.js { render }
      end
    end
  
 +  def choose
 +    params[:limit] ||= 20
 +    find_objects_for_index if !@objects
 +    respond_to do |f|
 +      if params[:partial]
 +        f.json {
 +          render json: {
 +            content: render_to_string(partial: "choose_rows.html",
 +                                      formats: [:html],
 +                                      locals: {
 +                                        multiple: params[:multiple]
 +                                      }),
 +            next_page_href: @next_page_href
 +          }
 +        }
 +      end
 +      f.js {
 +        render partial: 'choose', locals: {multiple: params[:multiple]}
 +      }
 +    end
 +  end
 +
    def render_content
      if !@object
        return render_not_found("object not found")
    end
  
    def update
 -    @updates ||= params[@object.class.to_s.underscore.singularize.to_sym]
 +    @updates ||= params[@object.resource_param_name.to_sym]
      @updates.keys.each do |attr|
        if @object.send(attr).is_a? Hash
          if @updates[attr].is_a? String
      @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
      @new_resource_attrs ||= {}
      @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' }
-     @object ||= model_class.new @new_resource_attrs
-     @object.save!
-     show
+     @object ||= model_class.new @new_resource_attrs, params["options"]
+     if @object.save
+       respond_to do |f|
+         f.json { render json: @object.attributes.merge(href: url_for(@object)) }
+         f.html {
+           redirect_to @object
+         }
+         f.js { render }
+       end
+     else
+       self.render_error status: 422
+     end
    end
  
 +  # Clone the given object, merging any attribute values supplied as
 +  # with a create action.
 +  def copy
 +    @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
 +    @new_resource_attrs ||= {}
 +    @object = @object.dup
 +    @object.update_attributes @new_resource_attrs
 +    if not @new_resource_attrs[:name] and @object.respond_to? :name
 +      if @object.name and @object.name != ''
 +        @object.name = "Copy of #{@object.name}"
 +      else
 +        @object.name = "Copy of unnamed #{@object.class_for_display.downcase}"
 +      end
 +    end
 +    @object.save!
 +    show
 +  end
 +
    def destroy
      if @object.destroy
        respond_to do |f|
    end
  
    def current_user
+     return Thread.current[:user] if Thread.current[:user]
      if Thread.current[:arvados_api_token]
-       Thread.current[:user] ||= User.current
+       if session[:user]
+         if session[:user][:is_active] != true
+           Thread.current[:user] = User.current
+         else
+           Thread.current[:user] = User.new(session[:user])
+         end
+       else
+         Thread.current[:user] = User.current
+       end
      else
        logger.error "No API token in Thread"
        return nil
    end
  
    def show_pane_list
 -    %w(Attributes Metadata JSON API)
 +    %w(Attributes Advanced)
    end
  
    protected
    def redirect_to_login
      respond_to do |f|
        f.html {
 -        if request.method == 'GET'
 +        if request.method.in? ['GET', 'HEAD']
            redirect_to arvados_api_client.arvados_login_url(return_to: request.url)
          else
            flash[:error] = "Either you are not logged in, or your session has timed out. I can't automatically log you in and re-attempt this request."
        if params[:uuid].empty?
          @object = nil
        else
 -        @object = model_class.find(params[:uuid])
 +        if (model_class != Link and
 +            resource_class_for_uuid(params[:uuid]) == Link)
 +          @name_link = Link.find(params[:uuid])
 +          @object = model_class.find(@name_link.head_uuid)
 +        else
 +          @object = model_class.find(params[:uuid])
 +        end
        end
      else
        @object = model_class.where(uuid: params[:uuid]).first
          # call to verify its authenticity.
          if verify_api_token
            session[:arvados_api_token] = params[:api_token]
 -          if !request.format.json? and request.method == 'GET'
+           u = User.current
+           session[:user] = {
+             uuid: u.uuid,
+             email: u.email,
+             first_name: u.first_name,
+             last_name: u.last_name,
+             is_active: u.is_active,
+             is_admin: u.is_admin,
+             prefs: u.prefs
+           }
 +          if !request.format.json? and request.method.in? ['GET', 'HEAD']
              # Repeat this request with api_token in the (new) session
              # cookie instead of the query string.  This prevents API
              # tokens from appearing in (and being inadvisedly copied
    end
  
    def thread_with_mandatory_api_token
 -    thread_with_api_token do
 -      yield
 +    thread_with_api_token(true) do
 +      if Thread.current[:arvados_api_token]
 +        yield
 +      elsif session[:arvados_api_token]
 +        # Expired session. Clear it before refreshing login so that,
 +        # if this login procedure fails, we end up showing the "please
 +        # log in" page instead of getting stuck in a redirect loop.
 +        session.delete :arvados_api_token
 +        redirect_to_login
 +      else
 +        render 'users/welcome'
 +      end
      end
    end
  
    end
  
    def check_user_agreements
 -    if current_user && !current_user.is_active && current_user.is_invited
 +    if current_user && !current_user.is_active
 +      if not current_user.is_invited
 +        return render 'users/inactive'
 +      end
        signatures = UserAgreement.signatures
        @signed_ua_uuids = UserAgreement.signatures.map &:head_uuid
        @required_user_agreements = UserAgreement.all.map do |ua|
    }
  
    def check_user_notifications
+     return if params['tab_pane']
      @notification_count = 0
      @notifications = []
  
      end
    end
  
 -  helper_method :my_folders
 -  def my_folders
 -    return @my_folders if @my_folders
 -    @my_folders = []
 +  helper_method :all_projects
 +  def all_projects
 +    @all_projects ||= Group.filter([['group_class','in',['project','folder']]])
 +  end
 +
 +  helper_method :my_projects
 +  def my_projects
 +    return @my_projects if @my_projects
 +    @my_projects = []
      root_of = {}
 -    Group.filter([['group_class','=','folder']]).each do |g|
 +    all_projects.each do |g|
        root_of[g.uuid] = g.owner_uuid
 -      @my_folders << g
 +      @my_projects << g
      end
      done = false
      while not done
          end
        end
      end
 -    @my_folders = @my_folders.select do |g|
 +    @my_projects = @my_projects.select do |g|
        root_of[g.uuid] == current_user.uuid
      end
    end
  
 +  helper_method :projects_shared_with_me
 +  def projects_shared_with_me
 +    my_project_uuids = my_projects.collect &:uuid
 +    all_projects.reject { |x| x.uuid.in? my_project_uuids }
 +  end
 +
 +  helper_method :recent_jobs_and_pipelines
 +  def recent_jobs_and_pipelines
 +    in_my_projects = ['owner_uuid','in',my_projects.collect(&:uuid)]
 +    (Job.limit(10).filter([in_my_projects]) |
 +     PipelineInstance.limit(10).filter([in_my_projects])).
 +      sort_by do |x|
 +      x.finished_at || x.started_at || x.created_at rescue x.created_at
 +    end
 +  end
 +
 +  helper_method :get_object
 +  def get_object uuid
 +    if @get_object.nil? and @objects
 +      @get_object = @objects.each_with_object({}) do |object, h|
 +        h[object.uuid] = object
 +      end
 +    end
 +    @get_object ||= {}
 +    @get_object[uuid]
 +  end
 +
 +  helper_method :project_breadcrumbs
 +  def project_breadcrumbs
 +    crumbs = []
 +    current = @name_link || @object
 +    while current
 +      if current.is_a?(Group) and current.group_class.in?(['project','folder'])
 +        crumbs.prepend current
 +      end
 +      if current.is_a? Link
 +        current = Group.find?(current.tail_uuid)
 +      else
 +        current = Group.find?(current.owner_uuid)
 +      end
 +    end
 +    crumbs
 +  end
 +
 +  helper_method :current_project_uuid
 +  def current_project_uuid
 +    if @object.is_a? Group and @object.group_class.in?(['project','folder'])
 +      @object.uuid
 +    elsif @name_link.andand.tail_uuid
 +      @name_link.tail_uuid
 +    elsif @object and resource_class_for_uuid(@object.owner_uuid) == Group
 +      @object.owner_uuid
 +    else
 +      nil
 +    end
 +  end
++
+   # helper method to get links for given object or uuid
+   helper_method :links_for_object
+   def links_for_object object_or_uuid
+     raise ArgumentError, 'No input argument' unless object_or_uuid
+     preload_links_for_objects([object_or_uuid])
+     uuid = object_or_uuid.is_a?(String) ? object_or_uuid : object_or_uuid.uuid
+     @all_links_for[uuid] ||= []
+   end
+   # helper method to preload links for given objects and uuids
+   helper_method :preload_links_for_objects
+   def preload_links_for_objects objects_and_uuids
+     @all_links_for ||= {}
+     raise ArgumentError, 'Argument is not an array' unless objects_and_uuids.is_a? Array
+     return @all_links_for if objects_and_uuids.empty?
+     uuids = objects_and_uuids.collect { |x| x.is_a?(String) ? x : x.uuid }
+     # if already preloaded for all of these uuids, return
+     if not uuids.select { |x| @all_links_for[x].nil? }.any?
+       return @all_links_for
+     end
+     uuids.each do |x|
+       @all_links_for[x] = []
+     end
+     # TODO: make sure we get every page of results from API server
+     Link.filter([['head_uuid', 'in', uuids]]).each do |link|
+       @all_links_for[link.head_uuid] << link
+     end
+     @all_links_for
+   end
+   # helper method to get a certain number of objects of a specific type
+   # this can be used to replace any uses of: "dataclass.limit(n)"
+   helper_method :get_n_objects_of_class
+   def get_n_objects_of_class dataclass, size
+     @objects_map_for ||= {}
+     raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class
+     raise ArgumentError, 'Argument is not a valid limit size' unless (size && size>0)
+     # if the objects_map_for has a value for this dataclass, and the
+     # size used to retrieve those objects is equal, return it
+     size_key = "#{dataclass.name}_size"
+     if @objects_map_for[dataclass.name] && @objects_map_for[size_key] &&
+         (@objects_map_for[size_key] == size)
+       return @objects_map_for[dataclass.name]
+     end
+     @objects_map_for[size_key] = size
+     @objects_map_for[dataclass.name] = dataclass.limit(size)
+   end
+   # helper method to get collections for the given uuid
+   helper_method :collections_for_object
+   def collections_for_object uuid
+     raise ArgumentError, 'No input argument' unless uuid
+     preload_collections_for_objects([uuid])
+     @all_collections_for[uuid] ||= []
+   end
+   # helper method to preload collections for the given uuids
+   helper_method :preload_collections_for_objects
+   def preload_collections_for_objects uuids
+     @all_collections_for ||= {}
+     raise ArgumentError, 'Argument is not an array' unless uuids.is_a? Array
+     return @all_collections_for if uuids.empty?
+     # if already preloaded for all of these uuids, return
+     if not uuids.select { |x| @all_collections_for[x].nil? }.any?
+       return @all_collections_for
+     end
+     uuids.each do |x|
+       @all_collections_for[x] = []
+     end
+     # TODO: make sure we get every page of results from API server
+     Collection.where(uuid: uuids).each do |collection|
+       @all_collections_for[collection.uuid] << collection
+     end
+     @all_collections_for
+   end
+   # helper method to get log collections for the given log
+   helper_method :log_collections_for_object
+   def log_collections_for_object log
+     raise ArgumentError, 'No input argument' unless log
+     preload_log_collections_for_objects([log])
+     uuid = log
+     fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(log)
+     if fixup && fixup.size>1
+       uuid = fixup[1]
+     end
+     @all_log_collections_for[uuid] ||= []
+   end
+   # helper method to preload collections for the given uuids
+   helper_method :preload_log_collections_for_objects
+   def preload_log_collections_for_objects logs
+     @all_log_collections_for ||= {}
+     raise ArgumentError, 'Argument is not an array' unless logs.is_a? Array
+     return @all_log_collections_for if logs.empty?
+     uuids = []
+     logs.each do |log|
+       fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(log)
+       if fixup && fixup.size>1
+         uuids << fixup[1]
+       else
+         uuids << log
+       end
+     end
+     # if already preloaded for all of these uuids, return
+     if not uuids.select { |x| @all_log_collections_for[x].nil? }.any?
+       return @all_log_collections_for
+     end
+     uuids.each do |x|
+       @all_log_collections_for[x] = []
+     end
+     # TODO: make sure we get every page of results from API server
+     Collection.where(uuid: uuids).each do |collection|
+       @all_log_collections_for[collection.uuid] << collection
+     end
+     @all_log_collections_for
+   end
+   # helper method to get object of a given dataclass and uuid
+   helper_method :object_for_dataclass
+   def object_for_dataclass dataclass, uuid
+     raise ArgumentError, 'No input argument dataclass' unless (dataclass && uuid)
+     preload_objects_for_dataclass(dataclass, [uuid])
+     @objects_for[uuid]
+   end
+   # helper method to preload objects for given dataclass and uuids
+   helper_method :preload_objects_for_dataclass
+   def preload_objects_for_dataclass dataclass, uuids
+     @objects_for ||= {}
+     raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class
+     raise ArgumentError, 'Argument is not an array' unless uuids.is_a? Array
+     return @objects_for if uuids.empty?
+     # if already preloaded for all of these uuids, return
+     if not uuids.select { |x| @objects_for[x].nil? }.any?
+       return @objects_for
+     end
+     dataclass.where(uuid: uuids).each do |obj|
+       @objects_for[obj.uuid] = obj
+     end
+     @objects_for
+   end
  end
index 0148b72d26837d941ce173034806179c637796d9,6a5df8754ff9b69147e8f4669dba7f70df8a4d73..88dadbba626e6adf3329891f66feca27dfdaaedb
@@@ -3,13 -3,11 +3,13 @@@ class CollectionsController < Applicati
                       only: [:show_file, :show_file_links])
    skip_before_filter(:find_object_by_uuid,
                       only: [:provenance, :show_file, :show_file_links])
 +  # We depend on show_file to display the user agreement:
 +  skip_before_filter :check_user_agreements, only: [:show_file]
  
    RELATION_LIMIT = 5
  
    def show_pane_list
 -    %w(Files Attributes Metadata Provenance_graph Used_by JSON API)
 +    %w(Files Provenance_graph Used_by Advanced)
    end
  
    def set_persistent
      end
    end
  
 +  def choose
 +    params[:limit] ||= 20
 +    @objects = Link.
 +      filter([['link_class','=','name'],
 +              ['head_uuid','is_a','arvados#collection']])
 +    find_objects_for_index
 +    @next_page_href = (next_page_offset and
 +                       url_for(offset: next_page_offset, partial: true))
 +    @name_links = @objects
 +    @objects = Collection.
 +      filter([['uuid','in',@name_links.collect(&:head_uuid)]])
 +    super
 +  end
 +
    def index
      if params[:search].andand.length.andand > 0
        tags = Link.where(any: ['contains', params[:search]])
        info[:links] << link
      end
      @request_url = request.url
+     render_index
    end
  
    def show_file_links
        end
        @output_of = jobs_with.call(output: @object.uuid)
        @log_of = jobs_with.call(log: @object.uuid)
-       project_links = Link.limit(RELATION_LIMIT).order("modified_at DESC")
 -      @folder_links = Link.limit(RELATION_LIMIT).order("modified_at DESC")
++      @project_links = Link.limit(RELATION_LIMIT).order("modified_at DESC")
          .where(head_uuid: @object.uuid, link_class: 'name').results
-       project_hash = Group.where(uuid: project_links.map(&:tail_uuid)).to_hash
-       @projects = project_links.map { |link| project_hash[link.tail_uuid] }
 -      folder_hash = Group.where(uuid: @folder_links.map(&:tail_uuid)).to_hash
 -      @folders = @folder_links.map { |link| folder_hash[link.tail_uuid] }
++      project_hash = Group.where(uuid: @project_links.map(&:tail_uuid)).to_hash
++      @projects = @project_links.map { |link| project_hash[link.tail_uuid] }
        @permissions = Link.limit(RELATION_LIMIT).order("modified_at DESC")
          .where(head_uuid: @object.uuid, link_class: 'permission',
                 name: 'can_read').results
                                                                 :direction => :top_down,
                                                                 :combine_jobs => :script_only,
                                                                 :pdata_only => true}) rescue nil
+     super
    end
  
    def sharing_popup
index f97bb20f1331aa71c1b8162a0b4e52a0ecbae40a,71327d132e7bd6238accf48236bc45194f90d5ab..7698fdba934cb68e641ea23d7ffe92635aa09ffd
@@@ -1,16 -1,14 +1,17 @@@
  class GroupsController < ApplicationController
    def index
 -    @groups = Group.filter [['group_class', 'not in', ['folder']]]
 +    @groups = Group.filter [['group_class', 'not in', ['folder', 'project']]]
      @group_uuids = @groups.collect &:uuid
      @links_from = Link.where link_class: 'permission', tail_uuid: @group_uuids
      @links_to = Link.where link_class: 'permission', head_uuid: @group_uuids
+     render_index
    end
  
    def show
 -    return redirect_to(folder_path(@object)) if @object.group_class == 'folder'
 -    super
 +    if @object.group_class.in?(['project','folder'])
 +      redirect_to(project_path(@object))
 +    else
 +      super
 +    end
    end
  end
index 8743a6ffffeb789416de26855e55347034a1dc64,b7526c949a2c6ea28daf6807fa8545350bf4ac6a..ff3ac6b98eba40ee3d1be2f34c72ec26fa9450ca
@@@ -1,6 -1,8 +1,8 @@@
  class JobsController < ApplicationController
  
    def generate_provenance(jobs)
+     return if params['tab_pane'] != "Provenance"
      nodes = []
      collections = []
      jobs.each do |j|
@@@ -16,7 -18,7 +18,7 @@@
  
      @svg = ProvenanceHelper::create_provenance_graph nodes, "provenance_svg", {
        :request => request,
-       :all_script_parameters => true, 
+       :all_script_parameters => true,
        :script_version_nodes => true}
    end
  
      if params[:uuid]
        @objects = Job.where(uuid: params[:uuid])
        generate_provenance(@objects)
+       render_index
      else
        @limit = 20
        super
      end
    end
  
+   def cancel
+     @object.cancel
+     redirect_to @object
+   end
    def show
      generate_provenance([@object])
+     super
    end
  
    def index_pane_list
@@@ -44,6 -53,6 +53,6 @@@
    end
  
    def show_pane_list
-     %w(Details Provenance Advanced)
 -    %w(Status Attributes Provenance Metadata JSON API)
++    %w(Status Details Provenance Advanced)
    end
  end
index ccbd06f85ddd6e5bf1eb800c39929d522f997ffc,500927bdb62ba240301382cc0239a50f308d34ab..a4a9d69bef8205c7df6fd652546f52fd9ad0b6a7
@@@ -3,44 -3,10 +3,46 @@@ class PipelineInstancesController < App
    before_filter :find_objects_by_uuid, only: :compare
    include PipelineInstancesHelper
  
 +  def copy
 +    @object = @object.dup
 +    @object.components.each do |cname, component|
 +      component.delete :job
 +    end
 +    @object.state = 'New'
 +    super
 +  end
 +
 +  def update
 +    @updates ||= params[@object.class.to_s.underscore.singularize.to_sym]
 +    if (components = @updates[:components])
 +      components.each do |cname, component|
 +        if component[:script_parameters]
 +          component[:script_parameters].each do |param, value_info|
 +            if value_info.is_a? Hash
 +              if resource_class_for_uuid(value_info[:value]) == Link
 +                # Use the link target, not the link itself, as script
 +                # parameter; but keep the link info around as well.
 +                link = Link.find value_info[:value]
 +                value_info[:value] = link.head_uuid
 +                value_info[:link_uuid] = link.uuid
 +                value_info[:link_name] = link.name
 +              else
 +                # Delete stale link_uuid and link_name data.
 +                value_info[:link_uuid] = nil
 +                value_info[:link_name] = nil
 +              end
 +            end
 +          end
 +        end
 +      end
 +    end
 +    super
 +  end
 +
    def graph(pipelines)
-     count = {}    
+     return nil, nil if params['tab_pane'] != "Graph"
+     count = {}
      provenance = {}
      pips = {}
      n = 1
@@@ -79,7 -45,7 +81,7 @@@
          pips[uuid] = 0 unless pips[uuid] != nil
          pips[uuid] |= n
        end
-       
        n = n << 1
      end
  
      end
  
      provenance, pips = graph(@pipelines)
+     if provenance
+       @prov_svg = ProvenanceHelper::create_provenance_graph provenance, "provenance_svg", {
+         :request => request,
+         :all_script_parameters => true,
+         :combine_jobs => :script_and_version,
+         :script_version_nodes => true,
+         :pips => pips }
+     end
  
-     @prov_svg = ProvenanceHelper::create_provenance_graph provenance, "provenance_svg", {
-       :request => request,
-       :all_script_parameters => true, 
-       :combine_jobs => :script_and_version,
-       :script_version_nodes => true,
-       :pips => pips }
      super
    end
  
  
      @prov_svg = ProvenanceHelper::create_provenance_graph provenance, "provenance_svg", {
        :request => request,
-       :all_script_parameters => true, 
+       :all_script_parameters => true,
        :combine_jobs => :script_and_version,
        :script_version_nodes => true,
        :pips => pips }
 +    @object = @objects.first
    end
  
    def show_pane_list
 -    panes = %w(Components Graph Attributes Metadata JSON API)
 +    panes = %w(Components Graph Advanced)
      if @object and @object.state.in? ['New', 'Ready']
        panes = %w(Inputs) + panes
      end
 +    if not @object.components.values.collect { |x| x[:job] }.compact.any?
 +      panes -= ['Graph']
 +    end
      panes
    end
  
-   def compare_pane_list 
+   def compare_pane_list
      %w(Compare Graph)
-   end 
+   end
  
    def index
      @limit = 20
index 784958b8b011fa23c948a24888979d3ad54b1a2e,2b7ec147a47759c03985a3c4cccac2b725328a6e..66267e028d4df4eb5dd1436fcdeb507904e466aa
@@@ -79,23 -79,21 +79,27 @@@ module ApplicationHelpe
    #
    def link_to_if_arvados_object(attrvalue, opts={}, style_opts={})
      if (resource_class = resource_class_for_uuid(attrvalue, opts))
 -      link_uuid = attrvalue.is_a?(ArvadosBase) ? attrvalue.uuid : attrvalue
 +      if attrvalue.is_a? ArvadosBase
 +        object = attrvalue
 +        link_uuid = attrvalue.uuid
 +      else
 +        object = nil
 +        link_uuid = attrvalue
 +      end
        link_name = opts[:link_text]
        if !link_name
 -        link_name = link_uuid
 +        link_name = object.andand.default_name || resource_class.default_name
  
          if opts[:friendly_name]
            if attrvalue.respond_to? :friendly_link_name
              link_name = attrvalue.friendly_link_name
            else
              begin
-               link_name = resource_class.find(link_uuid).friendly_link_name
+               if resource_class.name == 'Collection'
+                 link_name = collections_for_object(link_uuid).andand.first.andand.friendly_link_name
+               else
+                 link_name = object_for_dataclass(resource_class, link_uuid).andand.friendly_link_name
+               end
              rescue RuntimeError
                # If that lookup failed, the link will too. So don't make one.
                return attrvalue
            link_name = "#{resource_class.to_s}: #{link_name}"
          end
          if !opts[:no_tags] and resource_class == Collection
-           Link.where(head_uuid: link_uuid, link_class: ["tag", "identifier"]).each do |tag|
-             link_name += ' <span class="label label-info">' + html_escape(tag.name) + '</span>'
+           links_for_object(link_uuid).each do |tag|
+             if tag.link_class.in? ["tag", "identifier"]
+               link_name += ' <span class="label label-info">' + html_escape(tag.name) + '</span>'
+             end
            end
          end
          if opts[:thumbnail] and resource_class == Collection
            # add an image thumbnail if the collection consists of a single image file.
-           Collection.where(uuid: link_uuid).each do |c|
+           collections_for_object(link_uuid).each do |c|
              if c.files.length == 1 and CollectionsHelper::is_image c.files.first[1]
                link_name += " "
                link_name += image_tag "#{url_for c}/#{CollectionsHelper::file_path c.files.first}", style: "height: 4em; width: auto"
        if opts[:no_link]
          raw(link_name)
        else
 -        link_to raw(link_name), { controller: resource_class.to_s.tableize, action: 'show', id: link_uuid }, style_opts
 +        link_to raw(link_name), { controller: resource_class.to_s.tableize, action: 'show', id: ((opts[:name_link].andand.uuid) || link_uuid) }, style_opts
        end
      else
        # just return attrvalue if it is not recognizable as an Arvados object or uuid.
      attrvalue = object.send(attr) if attrvalue.nil?
      if !object.attribute_editable?(attr, :ever) or
          (!object.editable? and
 -         !object.owner_uuid.in?(my_folders.collect(&:uuid)))
 -      return attrvalue 
 +         !object.owner_uuid.in?(my_projects.collect(&:uuid)))
 +      return ((attrvalue && attrvalue.length > 0 && attrvalue) ||
 +              (attr == 'name' and object.andand.default_name) ||
 +              '(none)')
      end
  
      input_type = 'text'
        ajax_options['data-pk'][:defaults] = object.attributes
      end
      ajax_options['data-pk'] = ajax_options['data-pk'].to_json
 +    @unique_id ||= (Time.now.to_f*1000000).to_i
 +    span_id = object.uuid.to_s + '-' + attr.to_s + '-' + (@unique_id += 1).to_s
  
 -    content_tag 'span', attrvalue.to_s, {
 -      "data-emptytext" => "none",
 +    span_tag = content_tag 'span', attrvalue.to_s, {
 +      "data-emptytext" => (object.andand.default_name || 'none'),
        "data-placement" => "bottom",
        "data-type" => input_type,
 -      "data-title" => "Update #{attr.gsub '_', ' '}",
 +      "data-title" => "Edit #{attr.gsub '_', ' '}",
        "data-name" => attr,
        "data-object-uuid" => object.uuid,
 +      "data-toggle" => "manual",
 +      "id" => span_id,
        :class => "editable"
      }.merge(htmloptions).merge(ajax_options)
 +    edit_button = raw('<a href="#" class="btn btn-xs btn-default btn-nodecorate" data-toggle="x-editable tooltip" data-toggle-selector="#' + span_id + '" data-placement="top" title="' + (htmloptions[:tiptitle] || 'edit') + '"><i class="fa fa-fw fa-pencil"></i></a>')
 +    if htmloptions[:btnplacement] == :left
 +      edit_button + ' ' + span_tag
 +    else
 +      span_tag + ' ' + edit_button
 +    end
    end
  
    def render_pipeline_component_attribute(object, attr, subattr, value_info, htmloptions={})
      if !object or
          !object.attribute_editable?(attr, :ever) or
          (!object.editable? and
 -         !object.owner_uuid.in?(my_folders.collect(&:uuid)))
 +         !object.owner_uuid.in?(my_projects.collect(&:uuid)))
        return link_to_if_arvados_object attrvalue
      end
  
        dataclass = ArvadosBase.resource_class_for_uuid(attrvalue)
      end
  
 +    id = "#{object.uuid}-#{subattr.join('-')}"
 +    dn = "[#{attr}]"
 +    subattr.each do |a|
 +      dn += "[#{a}]"
 +    end
 +    if value_info.is_a? Hash
 +      dn += '[value]'
 +    end
 +
 +    if dataclass == Collection
 +      selection_param = object.class.to_s.underscore + dn
 +      display_value = attrvalue
 +      if value_info.is_a?(Hash)
 +        if (link = Link.find? value_info[:link_uuid])
 +          display_value = link.name
 +        elsif value_info[:link_name]
 +          display_value = value_info[:link_name]
 +        end
 +      end
 +      modal_path = choose_collections_path \
 +      ({ title: 'Choose a dataset:',
 +         filters: [['tail_uuid', '=', object.owner_uuid]].to_json,
 +         action_name: 'OK',
 +         action_href: pipeline_instance_path(id: object.uuid),
 +         action_method: 'patch',
 +         action_data: {
 +           merge: true,
 +           selection_param: selection_param,
 +           success: 'page-refresh'
 +         }.to_json,
 +        })
 +      return content_tag('div', :class => 'input-group') do
 +        html = text_field_tag(dn, display_value,
 +                              :class =>
 +                              "form-control #{'required' if required}")
 +        html + content_tag('span', :class => 'input-group-btn') do
 +          link_to('Choose',
 +                  modal_path,
 +                  { :class => "btn btn-primary",
 +                    :remote => true,
 +                    :method => 'get',
 +                  })
 +        end
 +      end
 +    end
 +
      if dataclass.andand.is_a?(Class)
        datatype = 'select'
      elsif dataclass == 'number'
        datatype = 'text'
      end
  
 -    id = "#{object.uuid}-#{subattr.join('-')}"
 -    dn = "[#{attr}]"
 -    subattr.each do |a|
 -      dn += "[#{a}]"
 -    end
 -    if value_info.is_a? Hash
 -      dn += '[value]'
 -    end
 -
+     # preload data
+     preload_uuids = []
+     items = []
      selectables = []
      attrtext = attrvalue
      if dataclass and dataclass.is_a? Class
+       objects = get_n_objects_of_class dataclass, 10
+       objects.each do |item|
+         items << item
+         preload_uuids << item.uuid
+       end
        if attrvalue and !attrvalue.empty?
-         Link.where(head_uuid: attrvalue, link_class: ["tag", "identifier"]).each do |tag|
-           attrtext += " [#{tag.name}]"
+         preload_uuids << attrvalue
+       end
+       preload_links_for_objects preload_uuids
+       if attrvalue and !attrvalue.empty?
+         links_for_object(attrvalue).each do |link|
+           if link.link_class.in? ["tag", "identifier"]
+             attrtext += " [#{link.name}]"
+           end
          end
          selectables.append({name: attrtext, uuid: attrvalue, type: dataclass.to_s})
        end
-       #dataclass.where(uuid: attrvalue).each do |item|
-       #  selectables.append({name: item.uuid, uuid: item.uuid, type: dataclass.to_s})
-       #end
        itemuuids = []
-       dataclass.limit(10).each do |item|
+       items.each do |item|
          itemuuids << item.uuid
          selectables.append({name: item.uuid, uuid: item.uuid, type: dataclass.to_s})
        end
-       Link.where(head_uuid: itemuuids, link_class: ["tag", "identifier"]).each do |tag|
-         selectables.each do |selectable|
-           if selectable['uuid'] == tag.head_uuid
-             selectable['name'] += ' [' + tag.name + ']'
+       itemuuids.each do |itemuuid|
+         links_for_object(itemuuid).each do |link|
+           if link.link_class.in? ["tag", "identifier"]
+             selectables.each do |selectable|
+               if selectable['uuid'] == link.head_uuid
+                 selectable['name'] += ' [' + link.name + ']'
+               end
+             end
            end
          end
        end
        "data-title" => "Set value for #{subattr[-1].to_s}",
        "data-name" => dn,
        "data-pk" => "{id: \"#{object.uuid}\", key: \"#{object.class.to_s.underscore}\"}",
 -      "data-showbuttons" => "false",
        "data-value" => attrvalue,
 +      # "clear" button interferes with form-control's up/down arrows
 +      "data-clear" => false,
        :class => "editable #{'required' if required} form-control",
        :id => id
      }.merge(htmloptions)
                button_href, params, *rest)
      end
    end
 +
 +  def render_controller_partial partial, opts
 +    cname = opts.delete :controller_name
 +    begin
 +      render opts.merge(partial: "#{cname}/#{partial}")
 +    rescue ActionView::MissingTemplate
 +      render opts.merge(partial: "application/#{partial}")
 +    end
 +  end
 +    
 +  def fa_icon_class_for_object object
 +    case object.class.to_s.to_sym
 +    when :User
 +      'fa-user'
 +    when :Group
 +      object.group_class ? 'fa-folder' : 'fa-users'
 +    when :Job, :PipelineInstance, :PipelineTemplate
 +      'fa-gears'
 +    when :Collection
 +      'fa-archive'
 +    when :Specimen
 +      'fa-flask'
 +    when :Trait
 +      'fa-clipboard'
 +    when :Human
 +      'fa-male'
 +    when :VirtualMachine
 +      'fa-terminal'
 +    when :Repository
 +      'fa-code-fork'
 +    when :Link
 +      'fa-arrows-h'
 +    when :User
 +      'fa-user'
 +    when :Node
 +      'fa-cloud'
 +    when :KeepService
 +      'fa-exchange'
 +    when :KeepDisk
 +      'fa-hdd-o'
 +    else
 +      'fa-cube'
 +    end
 +  end
  end
index a66b3903d63f6c39c66721986cbcbdbfa9bdf1b2,33e107e3693c94b4954f4e155312b060ab50205e..aca87868c3ab7f1f3523fc66d6569f031c8e99f2
@@@ -1,6 -1,7 +1,7 @@@
  class ArvadosBase < ActiveRecord::Base
    self.abstract_class = true
    attr_accessor :attribute_sortkey
+   attr_accessor :create_params
  
    def self.arvados_api_client
      ArvadosApiClient.new_or_current
@@@ -29,8 -30,9 +30,9 @@@
        end
    end
  
-   def initialize raw_params={}
+   def initialize raw_params={}, create_params={}
      super self.class.permit_attribute_params(raw_params)
+     @create_params = create_params
      @attribute_sortkey ||= {
        'id' => nil,
        'name' => '000',
      new.private_reload(hash)
    end
  
 +  def self.find?(*args)
 +    find(*args) rescue nil
 +  end
 +
    def self.order(*args)
      ArvadosResourceList.new(self).order(*args)
    end
      ActionController::Parameters.new(raw_params).permit!
    end
  
-   def self.create raw_params={}
-     super(permit_attribute_params(raw_params))
+   def self.create raw_params={}, create_params={}
+     x = super(permit_attribute_params(raw_params))
+     x.create_params = create_params
+     x
    end
  
    def update_attributes raw_params={}
        obdata.delete :uuid
        resp = arvados_api_client.api(self.class, '/' + uuid, postdata)
      else
+       postdata.merge!(@create_params) if @create_params
        resp = arvados_api_client.api(self.class, '', postdata)
      end
      return false if !resp[:etag] || !resp[:uuid]
      uuid
    end
  
 -  def dup
 -    super.forget_uuid!
 +  def initialize_copy orig
 +    super
 +    forget_uuid!
    end
  
    def attributes_for_display
    end
  
    def class_for_display
 -    self.class.to_s
 +    self.class.to_s.underscore.humanize
    end
  
    def self.creatable?
      current_user
    end
  
 -  def self.goes_in_folders?
 +  def self.goes_in_projects?
      false
    end
  
      resource_class
    end
  
 +  def resource_param_name
 +    self.class.to_s.underscore
 +  end
 +
    def friendly_link_name
 -    (name if self.respond_to? :name) || uuid
 +    (name if self.respond_to? :name) || default_name
    end
  
    def content_summary
      friendly_link_name
    end
  
 +  def self.default_name
 +    self.to_s.underscore.humanize
 +  end
 +
 +  def controller
 +    (self.class.to_s.pluralize + 'Controller').constantize
 +  end
 +
 +  def controller_name
 +    self.class.to_s.tableize
 +  end
 +
 +  # Placeholder for name when name is missing or empty
 +  def default_name
 +    if self.respond_to? :name
 +      "New #{class_for_display.downcase}"
 +    else
 +      uuid
 +    end
 +  end
 +
    def owner
      ArvadosBase.find(owner_uuid) rescue nil
    end
index 2a69c284d8a8ced6fb15513496e2bbcda1f0626b,173d3a06964fb5667b9546aee4bab518baf3c190..aac6168d22aecac8d37d9afcaee56db844cecdcd
@@@ -1,12 -1,8 +1,12 @@@
  class Job < ArvadosBase
 -  def self.goes_in_folders?
 +  def self.goes_in_projects?
      true
    end
  
 +  def content_summary
 +    "#{script} job"
 +  end
 +
    def attribute_editable? attr, *args
      false
    end
      false
    end
  
 +  def default_name
 +    if script
 +      x = "\"#{script}\" job"
 +    else
 +      x = super
 +    end
 +    if finished_at
 +      x += " finished #{finished_at.strftime('%b %-d')}"
 +    elsif started_at
 +      x += " started #{started_at.strftime('%b %-d')}"
 +    elsif created_at
 +      x += " submitted #{created_at.strftime('%b %-d')}"
 +    end
 +  end
++
+   def cancel
+     arvados_api_client.api "jobs/#{self.uuid}/", "cancel", {}
+   end
  end
index e6343628f2cc444fe6346a989259b8a478d3246a,353bd74143103f7d87f0636d0668a2de4e60b280..b7f27df3d79539bbe7c01b86dadbf0303160dc78
@@@ -1,51 -1,77 +1,98 @@@
 +<% content_for :content_top do %>
 +  <% if @object and not @object.is_a?(Group) and @object.class.goes_in_projects? and @object.owner_uuid == current_user.uuid %>
 +    <div class="pull-right" style="width: 40%">
 +      <div class="alert alert-warning alert-dismissable">
 +        <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
 +        <strong>Hey.</strong> This <%= @object.class_for_display.downcase %> belongs to your account, but it's not in any of your projects. If you want it to be easy to find in the future, you should move it to a project.<br />
 +        <%= button_to(choose_projects_path(
 +                   title: 'Move to...',
 +                   editable: true,
 +                   action_name: 'Move',
 +                   action_href: url_for(action: :update),
 +                   action_method: 'patch',
 +                   action_data: {selection_param: @object.resource_param_name+'[owner_uuid]', success: 'page-refresh'}.to_json),
 +                  { class: "btn btn-primary btn-sm", remote: true, method: 'get' }) do %>
 +          <i class="fa fa-fw fa-folder"></i> Choose a project...
 +        <% end %>
 +      </div>
 +    </div>
 +  <% end %>
 +<% end %>
 +
+ <% content_for :js do %>
+   tab_pane_valid_state = {};
+   function ajaxRefreshTabPane(pane) {
+     if (!tab_pane_valid_state[pane]) {
+       tab_pane_valid_state[pane] = true;
+       $(document).trigger('ajax:send');
+       $.ajax('<%=j url_for @object %>?tab_pane='+pane, {dataType: 'html', type: 'GET'}).
+         done(function(data, status, jqxhr) {
+           $('#' + pane + ' > div > div').html(data);
+           $(document).trigger('ajax:complete');
+           ajaxRefreshTabPane(pane);
+         });
+     }
+   }
+   $(window).on('load', smart_scroll_fixup);
+   $(document).on('shown.bs.tab', 'ul.nav-tabs > li > a', smart_scroll_fixup);
+   $(document).on('shown.bs.tab', function(e) {
+     ajaxRefreshTabPane(e.target.id.slice(0, -4));
+   });
+   $(document).on('arv-log-event', function() {
+     <% pane_list.each do |pane| %>
+     tab_pane_valid_state['<%=j pane %>'] = false;
+     <% end %>
+     ajaxRefreshTabPane($('.tab-pane.active')[0].id);
+   });
+ <% end %>
  <% content_for :tab_panes do %>
  
  <% comparable = controller.respond_to? :compare %>
- <% pane_list ||= %w(recent) %>
- <% panes = Hash[pane_list.map { |pane|
-      [pane, render(partial: 'show_' + pane.downcase,
-                    locals: { comparable: comparable, objects: @objects })]
-    }.compact] %>
  
  <ul class="nav nav-tabs">
-   <% panes.each_with_index do |(pane, content), i| %>
+   <% pane_list.each_with_index do |pane, i| %>
      <li class="<%= 'active' if i==0 %>"><a href="#<%= pane %>" data-toggle="tab" id="<%= pane %>-tab"> <%= pane.gsub('_', ' ') %></a></li>
    <% end %>
  </ul>
  <div class="tab-content">
- <% panes.each_with_index do |(pane, content), i| %>
-   <div id="<%= pane %>" class="tab-pane fade <%= 'in active' if i==0 %>">
+ <% pane_list.each_with_index do |pane, i| %>
+   <div id="<%= pane %>"
+        class="tab-pane fade <%= 'in active' if i==0 %> arv-log-event-listener"
+ <% if controller.action_name == "index" %>
+        data-object-kind="arvados#<%= ArvadosApiClient.class_kind controller.model_class %>"
+ <% else %>
+        data-object-uuid="<%= @object.uuid %>"
+ <% end %>
+ >
+ <% content_for :js do %>
+   <% if i == 0 %>
+     tab_pane_valid_state['<%=j pane %>'] = true;
+   <% else %>
+     tab_pane_valid_state['<%=j pane %>'] = false;
+     $(document).on('ready', function() {
+       ajaxRefreshTabPane('<%=j pane %>');
+     });
+   <% end %>
+ <% end %>
 -    <div class="smart-scroll" style="margin-top:0.5em;">
 +    <div id="<%= pane %>-scroll" class="<%= 'smart-scroll' if pane.match(/graph/) %>" style="margin-top:0.5em;">
-       <%= content %>
+       <div class="pane-content">
+         <% if i == 0 %>
+           <%= render(partial: 'show_' + pane.downcase,
+                      locals: { comparable: comparable, objects: @objects }) %>
+           <% else %>
+             <%= image_tag 'ajax-loader.gif' %>
+         <% end %>
+       </div>
      </div>
    </div>
  <% end %>
  </div>
  
  <% end %>
- <% content_for :js do %>
-     $(window).on('load', smart_scroll_fixup);
-     $(document).on('shown.bs.tab', 'ul.nav-tabs > li > a', smart_scroll_fixup);
- <% end %>
index 68e4298cc97d0dc9d290b7126dc2d8a64d644402,0000000000000000000000000000000000000000..c036b362dec2d70d926948489a85637dbb0ba318
mode 100644,000000..100644
--- /dev/null
@@@ -1,44 -1,0 +1,56 @@@
 +<% outgoing = Link.where(tail_uuid: @object.uuid) %>
 +<% incoming = Link.where(head_uuid: @object.uuid) %>
 +
++<%
++  preload_uuids = []
++  preload_head_uuids = []
++  outgoing.results.each do |link|
++    preload_uuids << link.uuid
++    preload_uuids << link.head_uuid
++    preload_head_uuids << link.head_uuid
++  end
++  preload_collections_for_objects preload_uuids
++  preload_links_for_objects preload_head_uuids
++%>
++
 +<% if (outgoing | incoming).any? %>
 +<table class="table topalign">
 +  <colgroup>
 +    <col width="20%" />
 +    <col width="10%" />
 +    <col width="10%" />
 +    <col width="20%" />
 +    <col width="20%" />
 +    <col width="20%" />
 +  </colgroup>
 +  <thead>
 +    <tr>
 +      <th></th>
 +      <th>link_class</th>
 +      <th>name</th>
 +      <th>tail</th>
 +      <th>head</th>
 +      <th>properties</th>
 +    </tr>
 +  </thead>
 +  <tbody>
 +    <% (outgoing | incoming).each do |link| %>
 +      <tr>
 +        <td>
 +          <%= render partial: 'show_object_button', locals: { object: link, size: 'xs' } %>
 +          <span class="arvados-uuid"><%= link.uuid %></span>
 +        </td>
 +        <td><%= link.link_class %></td>
 +        <td><%= link.name %></td>
 +        <td><%= link.tail_uuid == object.uuid ? 'this' : (render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "tail_uuid", attrvalue: link.tail_uuid, editable: false }) %></td>
 +        <td><%= link.head_uuid == object.uuid ? 'this' : (render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "head_uuid", attrvalue: link.head_uuid, editable: false }) %></td>
 +        <td><%= render partial: 'application/arvados_attr_value', locals: { obj: link, attr: "properties", attrvalue: link.properties, editable: false } %></td>
 +      </tr>
 +    <% end %>
 +  </tbody>
 +</table>
 +<% else %>
 +<span class="deemphasize">
 +  (No metadata links found)
 +</span>
 +<% end %>
index d611dbdee818f14453faff673d0b06012e871fcb,710388d643d17099e99d2697caace53f57ead368..f91357b12067b4f4662bdfaebd6cf3ccfac81490
@@@ -3,7 -3,12 +3,12 @@@
      <div class="panel panel-info">
        <div class="panel-heading">
        <h3 class="panel-title">
-           <% "Collection #{@object.uuid}" %>
+           <% i = 0 %>
+             <% @folder_links.each do |l| %>
+               <%= if i > 0 then ', ' end %>
+               <% i += 1 %>
+               <%= l.name %>
+             <% end %>
        </h3>
        </div>
        <div class="panel-body">
          <% end %>
  
          <% if @output_of.andand.any? %>
 -          <p>Output of jobs:<br />
 +          <p>This collection was the output of:<br />
            <%= render_arvados_object_list_start(@output_of, 'Show all jobs',
                  jobs_path(filter: [['output', '=', @object.uuid]].to_json)) do |job| %>
 -          <%= link_to_if_arvados_object(job, friendly_name: true) %><br />
 +            <%= link_to_if_arvados_object(job, friendly_name: true) %><br />
            <% end %>
            </p>
          <% end %>
  
          <% if @log_of.andand.any? %>
 -          <p>Log of jobs:<br />
 +          <p>This collection contains log messages from:<br />
            <%= render_arvados_object_list_start(@log_of, 'Show all jobs',
                  jobs_path(filter: [['log', '=', @object.uuid]].to_json)) do |job| %>
 -          <%= link_to_if_arvados_object(job, friendly_name: true) %><br />
 +            <%= link_to_if_arvados_object(job, friendly_name: true) %><br />
            <% end %>
            </p>
          <% end %>
        <input type="text" class="form-control" placeholder="Search"/>
          -->
        <div style="height:0.5em;"></div>
 -        <% if not @logs.andand.any? %>
 +        <% name_or_object = @name_link.andand.uuid ? @name_link : @object %>
 +        <% if name_or_object.created_at and not @logs.andand.any? %>
            <p>
 -            Created: <%= @object.created_at.to_s(:long) %>
 +            Created: <%= name_or_object.created_at.to_s(:long) %>
            </p>
            <p>
 -            Last modified: <%= @object.modified_at.to_s(:long) %> by <%= link_to_if_arvados_object @object.modified_by_user_uuid, friendly_name: true %>
 +            Last modified: <%= name_or_object.modified_at.to_s(:long) %> by <%= link_to_if_arvados_object name_or_object.modified_by_user_uuid, friendly_name: true %>
            </p>
          <% else %>
            <%= render_arvados_object_list_start(@logs, 'Show all activity',
 -                logs_path(filters: [['object_uuid','=',@object.uuid]].to_json)) do |log| %>
 +                logs_path(filters: [['object_uuid','=',name_or_object.uuid]].to_json)) do |log| %>
            <p>
            <%= time_ago_in_words(log.event_at) rescue 'unknown time' %> ago: <%= log.summary %>
              <% if log.object_uuid %>
          </div>
  
        <div style="height:0.5em;"></div>
 -        <% if @folders.andand.any? %>
 -          <p>Included in folders:<br />
 -          <%= render_arvados_object_list_start(@folders, 'Show all folders',
 +        <% if @projects.andand.any? %>
 +          <p>Included in projects:<br />
 +          <%= render_arvados_object_list_start(@projects, 'Show all projects',
                  links_path(filter: [['head_uuid', '=', @object.uuid],
 -                                    ['link_class', '=', 'name']].to_json)) do |folder| %>
 -          <%= link_to_if_arvados_object(folder, friendly_name: true) %><br />
 +                                    ['link_class', '=', 'name']].to_json)) do |project| %>
 +          <%= link_to_if_arvados_object(project, friendly_name: true) %><br />
            <% end %>
            </p>
          <% end %>
index f39f35c45aa8f0778103e4e75daf0e9c1dba83a5,2948ec23519706b1eb3cd78996fabe02b47915cf..ec26409ca6a193125a7594a8f1808b1b5ea12487
@@@ -14,7 -14,7 +14,7 @@@
    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
    <meta name="description" content="">
    <meta name="author" content="">
-   <% if current_user %>
+   <% if current_user and $arvados_api_client.discovery[:websocketUrl] %>
    <meta name="arv-websocket-url" content="<%=$arvados_api_client.discovery[:websocketUrl]%>?api_token=<%=Thread.current[:arvados_api_token]%>">
    <% end %>
    <meta name="robots" content="NOINDEX, NOFOLLOW">
      height: 100%;
      }
  
 -    body > div.container-fluid {
 -    padding-top: 70px; /* 70px to make the container go all the way to the bottom of the navbar */
 -    }
 -
      @media (max-width: 979px) { body { padding-top: 0; } }
  
 -    .navbar .nav li.nav-separator > span.glyphicon.glyphicon-arrow-right {
 -    padding-top: 1.25em;
 -    }
 -
      @media (max-width: 767px) {
      .breadcrumbs {
      display: none;
      }
      }
    </style>
 -  <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
 +  <link href="//netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css" rel="stylesheet">
  </head>
  <body>
 -  <div id="wrapper">
 +  <div id="wrapper" class="container-fluid">
      <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
 -        <a class="navbar-brand" href="/"><%= Rails.configuration.site_name rescue Rails.application.class.parent_name %></a>
 +        <a class="navbar-brand" href="/"><%= Rails.configuration.site_name.downcase rescue Rails.application.class.parent_name %></a>
        </div>
  
        <div class="collapse navbar-collapse">
 -        <% if current_user.andand.is_active %>
 -          <ul class="nav navbar-nav side-nav">
 -
 -            <li class="<%= 'arvados-nav-active' if params[:action] == 'home' %>">
 -              <a href="/"><i class="fa fa-lg fa-dashboard fa-fw"></i> Dashboard</a>
 -            </li>
 -
 -            <li class="dropdown">
 -              <a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-lg fa-hand-o-up fa-fw"></i> Help <b class="caret"></b></a>
 -              <ul class="dropdown-menu">
 -                <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> Tutorials and User guide'), "#{Rails.configuration.arvados_docsite}/user", target: "_blank" %></li>
 -                <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> API Reference'), "#{Rails.configuration.arvados_docsite}/api", target: "_blank" %></li>
 -                <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> SDK Reference'), "#{Rails.configuration.arvados_docsite}/sdk", target: "_blank" %></li>
 -              </ul>
 -            </li>
 -
 -            <li class="dropdown">
 -              <a href="/folders">
 -                <i class="fa fa-lg fa-folder-o fa-fw"></i> Folders
 -            </a></li>
 -            <li><a href="/collections">
 -                <i class="fa fa-lg fa-briefcase fa-fw"></i> Collections (data files)
 -            </a></li>
 -            <li><a href="/jobs">
 -                <i class="fa fa-lg fa-tasks fa-fw"></i> Jobs
 -            </a></li>
 -            <li><a href="/pipeline_instances">
 -                <i class="fa fa-lg fa-tasks fa-fw"></i> Pipeline instances
 -            </a></li>
 -            <li><a href="/pipeline_templates">
 -                <i class="fa fa-lg fa-gears fa-fw"></i> Pipeline templates
 -            </a></li>
 -            <li>&nbsp;</li>
 -            <li><a href="/repositories">
 -                <i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories
 -            </a></li>
 -            <li><a href="/virtual_machines">
 -                <i class="fa fa-lg fa-terminal fa-fw"></i> Virtual machines
 -            </a></li>
 -            <li><a href="/humans">
 -                <i class="fa fa-lg fa-male fa-fw"></i> Humans
 -            </a></li>
 -            <li><a href="/specimens">
 -                <i class="fa fa-lg fa-flask fa-fw"></i> Specimens
 -            </a></li>
 -            <li><a href="/traits">
 -                <i class="fa fa-lg fa-clipboard fa-fw"></i> Traits
 -            </a></li>
 -            <li><a href="/links">
 -                <i class="fa fa-lg fa-arrows-h fa-fw"></i> Links
 -            </a></li>
 -            <% if current_user.andand.is_admin %>
 -              <li><a href="/users">
 -                  <i class="fa fa-lg fa-user fa-fw"></i> Users
 -              </a></li>
 -            <% end %>
 -            <li><a href="/groups">
 -                <i class="fa fa-lg fa-users fa-fw"></i> Groups
 -            </a></li>
 -            <li><a href="/nodes">
 -                <i class="fa fa-lg fa-cloud fa-fw"></i> Compute nodes
 -            </a></li>
 -            <li><a href="/keep_services">
 -                <i class="fa fa-lg fa-exchange fa-fw"></i> Keep services
 -            </a></li>
 -            <li><a href="/keep_disks">
 -                <i class="fa fa-lg fa-hdd-o fa-fw"></i> Keep disks
 -            </a></li>
 -          </ul>
 -        <% end %>
 -
 -        <ul class="nav navbar-nav navbar-left breadcrumbs">
 -          <% if current_user %>
 -            <% if content_for?(:breadcrumbs) %>
 -              <%= yield(:breadcrumbs) %>
 -            <% else %>
 -              <li class="nav-separator"><span class="glyphicon glyphicon-arrow-right"></span></li>
 -              <li>
 -                <%= link_to(
 -                            controller.controller_name.humanize.downcase,
 -                            url_for({controller: params[:controller]})) %>
 -              </li>
 -              <% if params[:action] != 'index' %>
 -                <li class="nav-separator">
 -                  <span class="glyphicon glyphicon-arrow-right"></span>
 -                </li>
 -                <li>
 -                  <%= link_to_if_arvados_object @object, {friendly_name: true}, {data: {object_uuid: @object.andand.uuid, name: 'name'}} %>
 -                </li>
 -                <li style="padding: 14px 0 14px">
 -                  <%= form_tag do |f| %>
 -                    <%= render :partial => "selection_checkbox", :locals => {:object => @object} %>
 -                  <% end %>
 -                </li>
 -              <% end %>
 -            <% end %>
 -          <% end %>
 -        </ul>
 -
          <ul class="nav navbar-nav navbar-right">
  
            <li>
            </li>
            -->
  
 -          <li class="dropdown notification-menu">
 -            <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="collections-menu">
 -              <span class="glyphicon glyphicon-paperclip"></span>
 -              <span class="badge" id="persistent-selection-count"></span>
 -              <span class="caret"></span>
 -            </a>
 -              <ul class="dropdown-menu" role="menu" id="persistent-selection-list">
 -                <%= form_tag '/actions' do %>
 -                <%= hidden_field_tag 'uuid', @object.andand.uuid %>
 -                <div id="selection-form-content"></div>
 -                <% end %>
 -            </ul>
 -          </li>
 -
 -          <% if current_user.is_active %>
 +          <% if current_user %>
            <li class="dropdown notification-menu">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="notifications-menu">
 -              <span class="glyphicon glyphicon-envelope"></span>
                <span class="badge badge-alert notification-count"><%= @notification_count %></span>
 -              <span class="caret"></span>
 +              <%= current_user.email %>
              </a>
              <ul class="dropdown-menu" role="menu">
 -              <% if (@notifications || []).length > 0 %>
 +              <% if current_user.is_active %>
 +              <li role="presentation"><a href="/authorized_keys" role="menuitem"><i class="fa fa-key fa-fw"></i> Manage ssh keys</a></li>
 +              <li role="presentation"><a href="/api_client_authorizations" role="menuitem"><i class="fa fa-ticket fa-fw"></i> Manage API tokens</a></li>
 +              <li role="presentation" class="divider"></li>
 +              <% end %>
 +              <li role="presentation"><a href="<%= logout_path %>" role="menuitem"><i class="fa fa-sign-out fa-fw"></i> Log out</a></li>
 +              <% if current_user.is_active and
 +                    (@notifications || []).length > 0 %>
 +                <li role="presentation" class="divider"></li>
                  <% @notifications.each_with_index do |n, i| %>
                    <% if i > 0 %><li class="divider"></li><% end %>
                    <li class="notification"><%= n.call(self) %></li>
                  <% end %>
 -              <% else %>
 -                <li class="notification empty">No notifications.</li>
                <% end %>
              </ul>
            </li>
            <% end %>
  
 -          <li class="dropdown">
 -            <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="user-menu">
 -              <span class="glyphicon glyphicon-user"></span><span class="caret"></span>
 +          <li class="dropdown notification-menu">
 +            <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="collections-menu">
 +              <span class="fa fa-lg fa-paperclip"></span>
 +              <span class="badge" id="persistent-selection-count"></span>
              </a>
 -            <ul class="dropdown-menu" role="menu">
 -              <li role="presentation" class="dropdown-header"><%= current_user.email %></li>
 -              <% if current_user.is_active %>
 -              <li role="presentation" class="divider"></li>
 -              <li role="presentation"><a href="/authorized_keys" role="menuitem"><i class="fa fa-key fa-fw"></i> Manage ssh keys</a></li>
 -              <li role="presentation"><a href="/api_client_authorizations" role="menuitem"><i class="fa fa-ticket fa-fw"></i> Manage API tokens</a></li>
 -              <li role="presentation" class="divider"></li>
 +            <ul class="dropdown-menu" role="menu" id="persistent-selection-list">
 +              <%= form_tag '/actions' do %>
 +                <%= hidden_field_tag 'uuid', @object.andand.uuid %>
 +                <div id="selection-form-content"></div>
                <% end %>
 -              <li role="presentation"><a href="<%= logout_path %>" role="menuitem"><i class="fa fa-sign-out fa-fw"></i> Log out</a></li>
              </ul>
            </li>
 +
 +          <% if current_user.is_active %>
 +            <li class="dropdown">
 +              <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="system-menu">
 +                <span class="fa fa-lg fa-gear"></span>
 +              </a>
 +              <ul class="dropdown-menu" role="menu">
 +                <li role="presentation" class="dropdown-header">
 +                  System tools
 +                </li>
 +                <li role="presentation"><a href="/repositories">
 +                    <i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories
 +                </a></li>
 +                <li role="presentation"><a href="/virtual_machines">
 +                    <i class="fa fa-lg fa-terminal fa-fw"></i> Virtual machines
 +                </a></li>
 +                <li role="presentation"><a href="/links">
 +                    <i class="fa fa-lg fa-arrows-h fa-fw"></i> Links
 +                </a></li>
 +                <% if current_user.andand.is_admin %>
 +                  <li role="presentation"><a href="/users">
 +                      <i class="fa fa-lg fa-user fa-fw"></i> Users
 +                  </a></li>
 +                <% end %>
 +                <li role="presentation"><a href="/groups">
 +                    <i class="fa fa-lg fa-users fa-fw"></i> Groups
 +                </a></li>
 +                <li role="presentation"><a href="/nodes">
 +                    <i class="fa fa-lg fa-cloud fa-fw"></i> Compute nodes
 +                </a></li>
 +                <li role="presentation"><a href="/keep_services">
 +                    <i class="fa fa-lg fa-exchange fa-fw"></i> Keep services
 +                </a></li>
 +                <li role="presentation"><a href="/keep_disks">
 +                    <i class="fa fa-lg fa-hdd-o fa-fw"></i> Keep disks
 +                </a></li>
 +              </ul>
 +            </li>
 +          <% end %>
            <% else %>
              <li><a href="<%= arvados_api_client.arvados_login_url(return_to: root_url) %>">Log in</a></li>
            <% end %>
        </div><!-- /.navbar-collapse -->
      </nav>
  
 +    <% if current_user.andand.is_active %>
 +      <nav class="navbar navbar-default breadcrumbs" role="navigation">
 +        <ul class="nav navbar-nav navbar-left">
 +          <li class="dropdown">
 +            <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="projects-menu">
 +              <i class="fa fa-lg fa-fw fa-home"></i>
 +              Projects
 +              <span class="caret"></span>
 +            </a>
 +            <ul class="dropdown-menu" role="menu">
 +              <li role="presentation" class="dropdown-header">
 +                <%= link_to projects_path('project[owner_uuid]' => current_project_uuid), method: 'post', class: 'btn btn-xs btn-default pull-right' do %>
 +                  <i class="fa fa-plus"></i> New project
 +                <% end %>
 +                My projects
 +              </li>
 +              <% my_projects.each do |p| %>
 +                <li>
 +                  <%= link_to(p.name, project_path(p.uuid)) %>
 +                </li>
 +              <% end %>
 +              <li class="divider">
 +              <li role="presentation" class="dropdown-header">
 +                Projects shared with me
 +              </li>
 +              <% projects_shared_with_me.each do |p| %>
 +                <li>
 +                  <%= link_to project_path(p.uuid) do %>
 +                    <i class="fa fa-fw fa-share-alt"></i> <%= p.name %>
 +                  <% end %>
 +                </li>
 +              <% end %>
 +            </ul>
 +          </li>
 +          <% project_breadcrumbs.each do |p| %>
 +            <li class="nav-separator">
 +              <i class="fa fa-lg fa-angle-double-right"></i>
 +            </li>
 +            <li>
 +              <%= link_to(p.name, project_path(p.uuid), data: {object_uuid: p.uuid, name: 'name'}) %>
 +            </li>
 +          <% end %>
 +          <% if current_project_uuid.andand != @object.andand.uuid %>
 +            <li class="nav-separator">
 +              <i class="fa fa-lg fa-angle-double-right"></i>
 +            </li>
 +          <% end %>
 +        </ul>
 +      </nav>
 +    <% end %>
 +
      <div id="page-wrapper">
        <%= yield %>
      </div>
    </div>
  
- </div>
    <%= yield :footer_html %>
    <%= piwik_tracking_tag %>
    <%= javascript_tag do %>
index df7f2e8eaeb37dbdaaf0a5d4cca51ddd4856c14a,383d4421e2eb0e84e7cd1d14b94ea1e51e739e2a..a4f69b36b00eb07c577bcc5d7bfb02f83cf2fa89
@@@ -18,7 -18,9 +18,9 @@@ ArvadosWorkbench::Application.routes.dr
    resources :virtual_machines
    resources :authorized_keys
    resources :job_tasks
-   resources :jobs
+   resources :jobs do
+     post 'cancel', :on => :member
+   end
    match '/logout' => 'sessions#destroy', via: [:get, :post]
    get '/logged_out' => 'sessions#index'
    resources :users do
    resources :uploaded_datasets
    resources :groups
    resources :specimens
 -  resources :pipeline_templates
 +  resources :pipeline_templates do
 +    get 'choose', on: :collection
 +  end
    resources :pipeline_instances do
      get 'compare', on: :collection
 +    post 'copy', on: :member
    end
    resources :links
    get '/collections/graph' => 'collections#graph'
      get 'sharing_popup', :on => :member
      post 'share', :on => :member
      post 'unshare', :on => :member
 +    get 'choose', on: :collection
    end
    get('/collections/download/:uuid/:reader_token/*file' => 'collections#show_file',
        format: false)
    get '/collections/download/:uuid/:reader_token' => 'collections#show_file_links'
    get '/collections/:uuid/*file' => 'collections#show_file', :format => false
 -  resources :folders do
 +  resources :projects do
      match 'remove/:item_uuid', on: :member, via: :delete, action: :remove_item
 +    match 'remove_items', on: :member, via: :delete, action: :remove_items
      get 'choose', on: :collection
    end
  
    post 'actions' => 'actions#post'
    get 'websockets' => 'websocket#index'
  
 -  root :to => 'users#welcome'
 +  root :to => 'projects#index'
  
    # Send unroutable requests to an arbitrary controller
    # (ends up at ApplicationController#render_not_found)
index 3651d33a46f4322194841f79865cb76ff1583b65,6646e5a3f1df27bb0d4fbd141ca2c67e27c25155..dc3957c7b19ece6508334a518002377c3b674d72
@@@ -16,7 -16,6 +16,7 @@@ class UsersTest < ActionDispatch::Integ
      visit page_with_token('admin_trustedclient')
  
      # go to Users list page
 +    find('#system-menu').click
      click_link 'Users'
  
      # check active user attributes in the list page
      end
  
      find('tr', text: 'zzzzz-tpzed-xurymjxw79nv3jz').
 -      find('a,button', text: 'Show').
 +      find('a', text: 'Show').
        click
      assert page.has_text? 'Attributes'
 -    assert page.has_text? 'Metadata'
 +    assert page.has_text? 'Advanced'
      assert page.has_text? 'Admin'
  
      # go to the Attributes tab
@@@ -51,7 -50,6 +51,7 @@@
  
      visit page_with_token('admin_trustedclient')
  
 +    find('#system-menu').click
      click_link 'Users'
  
      assert page.has_text? 'zzzzz-tpzed-d9tiejq69daie8f'
  
      # verify that the new user showed up in the users page and find
      # the new user's UUID
 -    new_user_uuid =
 -      find('tr[data-object-uuid]', text: 'foo@example.com').
 -      find('td', text: '-tpzed-').
 -      text
 +    new_user_uuid = 
 +      find('tr[data-object-uuid]', text: 'foo@example.com')['data-object-uuid']
      assert new_user_uuid, "Expected new user uuid not found"
  
      # go to the new user's page
      find('tr', text: new_user_uuid).
 -      find('a,button', text: 'Show').
 +      find('a', text: 'Show').
        click
  
      assert page.has_text? 'modified_by_user_uuid'
        assert_equal "false", text, "Expected new user's is_active to be false"
      end
  
 +    click_link 'Advanced'
      click_link 'Metadata'
 -    assert page.has_text? '(Repository: test_repo)'
 -    assert !(page.has_text? '(VirtualMachine:)')
 +    assert page.has_text? 'Repository: test_repo'
 +    assert !(page.has_text? 'VirtualMachine:')
  
      headless.stop
    end
      Capybara.current_driver = :selenium
      visit page_with_token('admin_trustedclient')
  
 +    find('#system-menu').click
      click_link 'Users'
  
      # click on active user
      find('tr', text: 'zzzzz-tpzed-xurymjxw79nv3jz').
 -      find('a,button', text: 'Show').
 +      find('a', text: 'Show').
        click
  
      # Setup user
        assert has_text? 'Virtual Machine'
        fill_in "repo_name", :with => "test_repo"
        click_button "Submit"
-       wait_for_ajax
      end
  
      assert page.has_text? 'modified_by_client_uuid'
  
 +    click_link 'Advanced'
      click_link 'Metadata'
 -    assert page.has_text? '(Repository: test_repo)'
 -    assert !(page.has_text? '(VirtualMachine:)')
 +    assert page.has_text? 'Repository: test_repo'
 +    assert !(page.has_text? 'VirtualMachine:')
  
      # Click on Setup button again and this time also choose a VM
      click_link 'Admin'
        fill_in "repo_name", :with => "second_test_repo"
        select("testvm.shell", :from => 'vm_uuid')
        click_button "Submit"
-       wait_for_ajax
      end
  
      assert page.has_text? 'modified_by_client_uuid'
  
 +    click_link 'Advanced'
      click_link 'Metadata'
 -    assert page.has_text? '(Repository: second_test_repo)'
 -    assert page.has_text? '(VirtualMachine: testvm.shell)'
 +    assert page.has_text? 'Repository: second_test_repo'
 +    assert page.has_text? 'VirtualMachine: testvm.shell'
  
      headless.stop
    end
  
      visit page_with_token('admin_trustedclient')
  
 +    find('#system-menu').click
      click_link 'Users'
  
      # click on active user
      find('tr', text: 'zzzzz-tpzed-xurymjxw79nv3jz').
 -      find('a,button', text: 'Show').
 +      find('a', text: 'Show').
        click
  
      # Verify that is_active is set
        assert_equal "false", text, "Expected user's is_active to be false after unsetup"
      end
  
 +    click_link 'Advanced'
      click_link 'Metadata'
 -    assert !(page.has_text? '(Repository: test_repo)')
 -    assert !(page.has_text? '(Repository: second_test_repo)')
 -    assert !(page.has_text? '(VirtualMachine: testvm.shell)')
 +    assert !(page.has_text? 'Repository: test_repo')
 +    assert !(page.has_text? 'Repository: second_test_repo')
 +    assert !(page.has_text? 'VirtualMachine: testvm.shell')
  
      # setup user again and verify links present
      click_link 'Admin'
        fill_in "repo_name", :with => "second_test_repo"
        select("testvm.shell", :from => 'vm_uuid')
        click_button "Submit"
-       wait_for_ajax
      end
  
      assert page.has_text? 'modified_by_client_uuid'
  
 +    click_link 'Advanced'
      click_link 'Metadata'
 -    assert page.has_text? '(Repository: second_test_repo)'
 -    assert page.has_text? '(VirtualMachine: testvm.shell)'
 +    assert page.has_text? 'Repository: second_test_repo'
 +    assert page.has_text? 'VirtualMachine: testvm.shell'
  
      headless.stop
    end