Merge remote-tracking branch 'origin/master' into 1971-show-image-thumbnails
authorPeter Amstutz <peter.amstutz@curoverse.com>
Fri, 7 Mar 2014 18:43:51 +0000 (13:43 -0500)
committerPeter Amstutz <peter.amstutz@curoverse.com>
Fri, 7 Mar 2014 18:43:51 +0000 (13:43 -0500)
27 files changed:
apps/workbench/Gemfile
apps/workbench/Gemfile.lock
apps/workbench/app/assets/javascripts/application.js
apps/workbench/app/assets/javascripts/editable.js
apps/workbench/app/assets/javascripts/pipeline_instances.js [new file with mode: 0644]
apps/workbench/app/assets/javascripts/pipeline_instances.js.coffee [deleted file]
apps/workbench/app/assets/javascripts/selection.js [new file with mode: 0644]
apps/workbench/app/assets/javascripts/sizing.js
apps/workbench/app/assets/stylesheets/application.css.scss
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/pipeline_instances_controller.rb
apps/workbench/app/helpers/application_helper.rb
apps/workbench/app/helpers/collections_helper.rb
apps/workbench/app/helpers/pipeline_instances_helper.rb
apps/workbench/app/helpers/provenance_helper.rb
apps/workbench/app/models/arvados_base.rb
apps/workbench/app/models/collection.rb
apps/workbench/app/models/pipeline_instance.rb
apps/workbench/app/views/application/_selection_checkbox.html.erb [new file with mode: 0644]
apps/workbench/app/views/application/_show_recent.html.erb
apps/workbench/app/views/collections/_index_tbody.html.erb
apps/workbench/app/views/collections/_show_files.html.erb
apps/workbench/app/views/collections/_show_recent.html.erb
apps/workbench/app/views/layouts/application.html.erb
apps/workbench/app/views/pipeline_instances/_show_components.html.erb
apps/workbench/app/views/pipeline_instances/_show_recent.html.erb
apps/workbench/app/views/pipeline_templates/_show_recent.html.erb

index 66734ef3cd785971034751bc50a6329a41d9c808..6ae12f75db306278fcb1227f4b711a962f66b84f 100644 (file)
@@ -43,7 +43,7 @@ gem 'less-rails'
 # gem 'capistrano'
 
 # To use debugger
-# gem 'debugger'
+#gem 'byebug'
 
 gem 'rvm-capistrano', :group => :test
 
@@ -54,3 +54,4 @@ gem 'RedCloth'
 gem 'piwik_analytics'
 gem 'httpclient'
 gem 'themes_for_rails'
+gem "deep_merge", :require => 'deep_merge/rails_compat'
\ No newline at end of file
index 7f4dc8e289150c38165528556f504eb6372dfe4f..c7ffeb00574af4be2d89aa22589e555bd02e75ff 100644 (file)
@@ -51,6 +51,7 @@ GEM
     coffee-script-source (1.6.3)
     commonjs (0.2.7)
     daemon_controller (1.1.7)
+    deep_merge (1.0.1)
     erubis (2.7.0)
     execjs (2.0.2)
     highline (1.6.20)
@@ -153,6 +154,7 @@ DEPENDENCIES
   bootstrap-sass (~> 3.1.0)
   bootstrap-x-editable-rails
   coffee-rails (~> 3.2.0)
+  deep_merge
   httpclient
   jquery-rails
   less
index 3b697a6aa992afbc36839ba9953ce63d16ca18b1..e7884b95165173bdce213624172957085e05c737 100644 (file)
@@ -41,6 +41,7 @@ jQuery(function($){
         }
         targets.fadeToggle(200);
     });
+
     $(document).
         on('ajax:send', function(e, xhr) {
             $('.loading').fadeTo('fast', 1);
@@ -139,4 +140,4 @@ jQuery(function($){
         fixer.duplicateTheadTr();
         fixer.fixThead();
     });
-})(jQuery);
+});
index 804eeb2d8fab23006ac9a75159183a90547766cc..a74358694213fe36041d32852111741bf1581f1a 100644 (file)
@@ -1,5 +1,6 @@
 $.fn.editable.defaults.ajaxOptions = {type: 'put', dataType: 'json'};
 $.fn.editable.defaults.send = 'always';
+//$.fn.editable.defaults.mode = 'inline';
 $.fn.editable.defaults.params = function (params) {
     var a = {};
     var key = params.pk.key;
@@ -7,4 +8,4 @@ $.fn.editable.defaults.params = function (params) {
     a[key] = {};
     a[key][params.name] = params.value;
     return a;
-};
\ No newline at end of file
+};
diff --git a/apps/workbench/app/assets/javascripts/pipeline_instances.js b/apps/workbench/app/assets/javascripts/pipeline_instances.js
new file mode 100644 (file)
index 0000000..ee14e3b
--- /dev/null
@@ -0,0 +1,46 @@
+
+(function() {
+    var run_pipeline_button_state = function() {
+        var a = $('a.editable.required.editable-empty');
+        if (a.length > 0) {
+            $("#run-pipeline-button").addClass("disabled");
+        }
+        else {
+            $("#run-pipeline-button").removeClass("disabled");
+        }
+    }
+
+    $.fn.editable.defaults.success = function (response, newValue) {
+        var tag = $(this);
+        if (tag.hasClass("required")) {
+            if (newValue && newValue.trim() != "") {
+                tag.removeClass("editable-empty");
+                tag.parent().css("background-color", "");
+                tag.parent().prev().css("background-color", "");
+            }
+            else {
+                tag.addClass("editable-empty");
+                tag.parent().css("background-color", "#ffdddd");
+                tag.parent().prev().css("background-color", "#ffdddd");
+            }
+        }
+        run_pipeline_button_state();
+    }
+
+    $(window).on('load', function() {
+        var a = $('a.editable.required');
+        for (var i = 0; i < a.length; i++) {
+            var tag = $(a[i]);
+            if (tag.hasClass("editable-empty")) {
+                tag.parent().css("background-color", "#ffdddd");
+                tag.parent().prev().css("background-color", "#ffdddd");
+            }
+            else {
+                tag.parent().css("background-color", "");
+                tag.parent().prev().css("background-color", "");
+            }
+        }
+        run_pipeline_button_state();
+    } );
+
+})();
diff --git a/apps/workbench/app/assets/javascripts/pipeline_instances.js.coffee b/apps/workbench/app/assets/javascripts/pipeline_instances.js.coffee
deleted file mode 100644 (file)
index 7615679..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/apps/workbench/app/assets/javascripts/selection.js b/apps/workbench/app/assets/javascripts/selection.js
new file mode 100644 (file)
index 0000000..c8ec810
--- /dev/null
@@ -0,0 +1,160 @@
+//= require jquery
+//= require jquery_ujs
+
+/** Javascript for local persistent selection. */
+
+get_selection_list = null;
+form_selection_sources = {};
+
+jQuery(function($){
+    var storage = localStorage; // sessionStorage
+
+    get_selection_list = function() {
+        if (!storage.persistentSelection) {
+            storage.persistentSelection = JSON.stringify([]);
+        }
+        return JSON.parse(storage.persistentSelection);
+    }
+
+    var put_storage = function(lst) {
+        storage.persistentSelection = JSON.stringify(lst);
+    }
+
+    var add_selection = function(uuid, name, href, type) {
+        var lst = get_selection_list();
+        lst.push({"uuid": uuid, "name": name, "href": href, "type": type});
+        put_storage(lst);
+        update_count();
+    };
+
+    var remove_selection = function(uuid) {
+        var lst = get_selection_list();
+        for (var i = 0; i < lst.length; i++) {
+            if (lst[i].uuid == uuid) {
+                lst.splice(i, 1);
+                i--;
+            }
+        }
+        put_storage(lst);
+        update_count();
+    };
+
+    var remove_selection_click = function(e) {
+        //remove_selection($(this).attr('name'));
+        remove_selection($(this).val());
+    };
+
+    var clear_selections = function() {
+        put_storage([]);
+        update_count();
+    }
+
+    var update_count = function(e) {
+        var lst = get_selection_list();
+        $("#persistent-selection-count").text(lst.length);
+
+        if (lst.length > 0) {
+            $('#persistent-selection-list').html('<li><a href="#" class="btn pull-right" id="clear_selections_button">Clear selections</a></li>'
+                                                 +'<li class="notification"><table style="width: 100%"></table></li>');
+            for (var i = 0; i < lst.length; i++) {
+                $('#persistent-selection-list > li > table').append("<tr>"
+                                                       + "<td>"
+                                                       + "<form>"
+                                                       + "<input class='remove-selection' type='checkbox' value='" + lst[i].uuid + "' checked='true'></input>"
+                                                       + "</form>"
+                                                       + "</td>"
+
+                                                       + "<td>"
+                                                       + "<span style='padding-left: 1em'><a href=\"" + lst[i].href + "\">" + lst[i].name + "</a></span>"
+                                                       + "</td>"
+
+                                                       + "<td style=\"vertical-align: top\">"
+                                                       + "<span style=\"padding-right: 1em\">" + lst[i].type + "</span>"
+                                                       + "</td>"
+
+                                                       + "</tr>");
+            }
+        } else {
+            $('#persistent-selection-list').html("<li class='notification empty'>No selections.</li>");
+        }
+
+        var checkboxes = $('.persistent-selection:checkbox');
+        for (i = 0; i < checkboxes.length; i++) {
+            for (var j = 0; j < lst.length; j++) {
+                if (lst[j].uuid == $(checkboxes[i]).val()) {
+                    checkboxes[i].checked = true;
+                    break;
+                }
+            }
+            if (j == lst.length) {
+                checkboxes[i].checked = false;
+            }
+        }
+        
+        $('.remove-selection').on('click', remove_selection_click);
+        $('#clear_selections_button').on('click', clear_selections);
+    };
+
+    $(document).
+        on('change', '.persistent-selection:checkbox', function(e) {
+            //console.log($(this));
+            //console.log($(this).val());
+            
+            var inc = 0;
+            if ($(this).is(":checked")) {
+                add_selection($(this).val(), $(this).attr('friendly_name'), $(this).attr('href'), $(this).attr('friendly_type'));
+            }
+            else {
+                remove_selection($(this).val());
+            }
+        });
+
+
+    $(window).on('load storage', update_count);
+});
+
+add_form_selection_sources = null;
+select_form_sources  = null;
+
+(function() {
+    var form_selection_sources = {};
+    add_form_selection_sources = function (src) {
+        for (var i = 0; i < src.length; i++) {
+            var t = form_selection_sources[src[i].type];
+            if (!t) {
+                t = form_selection_sources[src[i].type] = {};
+            }
+            if (!t[src[i].uuid]) {
+                t[src[i].uuid] = src[i];
+            }
+        }
+    };
+
+    select_form_sources = function(type) {
+        var ret = [];
+
+        if (get_selection_list) {
+            var lst = get_selection_list();
+            if (lst.length > 0) {
+                ret.push({text: "--- Selections ---", value: ""});
+
+                for (var i = 0; i < lst.length; i++) {
+                    if (lst[i].type == type) {
+                        ret.push({text: lst[i].name, value: lst[i].uuid})
+                    }
+                }
+            }
+        }
+        ret.push({text: "--- Recent ---", value: ""});
+
+        var t = form_selection_sources[type];
+        for (var key in t) {
+            if (t.hasOwnProperty(key)) {
+                var obj = t[key];
+                ret.push({text: obj.name, value: obj.uuid})
+            }
+        }
+        return ret;
+    };
+})();
+
index 388f7279903b10b194549243d0b2b62d69d6adde..55d2301387c90aa703d59ead3fa1d552c36014b5 100644 (file)
@@ -11,14 +11,14 @@ function graph_zoom(divId, svgId, scale) {
 }
 
 function smart_scroll_fixup(s) {
-    console.log(s);
+    //console.log(s);
     if (s != null && s.type == 'shown.bs.tab') {
         s = [s.target];
     }
     else {
         s = $(".smart-scroll");
     }
-    console.log(s);
+    //console.log(s);
     for (var i = 0; i < s.length; i++) {
         a = s[i];
         var h = window.innerHeight - a.getBoundingClientRect().top - 20;
index 3b57784c74f603203728b63e8c863f45e828c427..7a26c709ff678842597383190822b2933ec84ee4 100644 (file)
@@ -176,3 +176,19 @@ table.table-fixed-header-row tbody {
     position:relative;
     top:1.5em;
 }
+
+/* Setting the height needs to be fixed with javascript. */
+.dropdown-menu {
+    padding-right: 20px;
+    max-height: 440px;
+    width: 400px;
+    overflow-y: auto;
+}
+
+#persistent-selection-list {
+    width: 500px;
+}
+
+#persistent-selection-list li table tr {
+  border-top: 1px solid rgb(221, 221, 221);
+}
index e94428e92dca51ad00e415f38dac8ff69c35c4ef..412f86ce07a6b874d05b7d5d796bb84a9017bf09 100644 (file)
@@ -24,6 +24,7 @@ class ApplicationController < ActionController::Base
 
   def unprocessable(message=nil)
     @errors ||= []
+
     @errors << message if message
     render_error status: 422
   end
@@ -109,6 +110,7 @@ class ApplicationController < ActionController::Base
   def create
     @object ||= model_class.new params[model_class.to_s.underscore.singularize]
     @object.save!
+
     respond_to do |f|
       f.json { render json: @object }
       f.html {
index 42cb2e9d44c8af1155f56133c7463476013049f5..b477223b81d9041c7eb69e416b73b8f62290a510 100644 (file)
@@ -45,6 +45,38 @@ class PipelineInstancesController < ApplicationController
   end
 
   def show
+    if @object.components.empty? and @object.pipeline_template_uuid
+      template = PipelineTemplate.find(@object.pipeline_template_uuid)
+      pipeline = {}
+      template.components.each do |component_name, component_props|
+        pipeline[component_name] = {}
+        component_props.each do |k, v|
+          if k == :script_parameters
+            pipeline[component_name][:script_parameters] = {}
+            v.each do |param_name, param_value|
+              if param_value.is_a? Hash
+                if param_value[:value]
+                  pipeline[component_name][:script_parameters][param_name] = param_value[:value]
+                elsif param_value[:default]
+                  pipeline[component_name][:script_parameters][param_name] = param_value[:default]
+                elsif param_value[:optional] != nil or param_value[:required] != nil
+                    pipeline[component_name][:script_parameters][param_name] = ""
+                else
+                  pipeline[component_name][:script_parameters][param_name] = param_value
+                end
+              else
+                pipeline[component_name][:script_parameters][param_name] = param_value
+              end
+            end
+          else
+            pipeline[component_name][k] = v
+          end
+        end
+      end
+      @object.components= pipeline
+      @object.save
+    end
+
     @pipelines = [@object]
 
     if params[:compare]
@@ -141,6 +173,15 @@ class PipelineInstancesController < ApplicationController
     %w(Compare Graph)
   end 
 
+  def update
+    updates = params[@object.class.to_s.underscore.singularize.to_sym]
+    if updates["components"]
+      require 'deep_merge/rails_compat'
+      updates["components"] = updates["components"].deeper_merge(@object.components)
+    end
+    super
+  end
+
   protected
   def for_comparison v
     if v.is_a? Hash or v.is_a? Array
index cd8e5279dd0bdf595bbce465c2c9b693c0385a53..6de96e8995638aa3c4525ebe489218ff5680df31 100644 (file)
@@ -3,6 +3,10 @@ module ApplicationHelper
     controller.current_user
   end
 
+  def self.match_uuid(uuid)
+    /^([0-9a-z]{5})-([0-9a-z]{5})-([0-9a-z]{15})$/.match(uuid.to_s)
+  end
+
   def current_api_host
     Rails.configuration.arvados_v1_base.gsub /https?:\/\/|\/arvados\/v1/,''
   end
@@ -67,7 +71,7 @@ module ApplicationHelper
         end
       end
       style_opts[:class] = (style_opts[:class] || '') + ' nowrap'
-      link_to link_name, { controller: resource_class.to_s.underscore.pluralize, action: 'show', id: link_uuid }, style_opts
+      link_to link_name, { controller: resource_class.to_s.tableize, action: 'show', id: link_uuid }, style_opts
     else
       attrvalue
     end
@@ -100,4 +104,113 @@ module ApplicationHelper
       :class => "editable"
     }.merge(htmloptions)
   end
+
+  def render_editable_subattribute(object, attr, subattr, template, htmloptions={})
+    attrvalue = object.send(attr)
+    subattr.each do |k|
+      if attrvalue and attrvalue.is_a? Hash
+        attrvalue = attrvalue[k]
+      else
+        break
+      end
+    end
+
+    datatype = nil
+    required = true
+    if template
+      #puts "Template is #{template.class} #{template.is_a? Hash} #{template}"
+      if template.is_a? Hash
+        if template[:output_of]
+          return raw("<span class='label label-default'>#{template[:output_of]}</span>")
+        end
+        if template[:dataclass]
+          dataclass = template[:dataclass]
+        end
+        if template[:optional] != nil
+          required = (template[:optional] != "true")
+        end
+        if template[:required] != nil
+          required = template[:required]
+        end
+      end
+    end
+
+    return attrvalue if !object.attribute_editable? attr
+
+    if not dataclass
+      rsc = template
+      if template.is_a? Hash
+        if template[:value]
+          rsc = template[:value]
+        elsif template[:default]
+          rsc = template[:default]
+        end
+      end
+
+      dataclass = ArvadosBase.resource_class_for_uuid(rsc)
+    end
+
+    if dataclass && dataclass.is_a?(Class)
+      datatype = 'select'
+    elsif dataclass == 'number'
+      datatype = 'number'
+    else
+      if template.is_a? Array
+        # ?!?
+      elsif template.is_a? String
+        if /^\d+$/.match(template)
+          datatype = 'number'
+        else
+          datatype = 'text'
+        end
+      end
+    end
+
+    id = "#{object.uuid}-#{subattr.join('-')}"
+    dn = "[#{attr}]"
+    subattr.each do |a|
+      dn += "[#{a}]"
+    end
+
+    if attrvalue.is_a? String
+      attrvalue = attrvalue.strip
+    end
+
+    if dataclass and dataclass.is_a? Class
+      items = []
+      items.append({name: attrvalue, uuid: attrvalue, type: dataclass.to_s})
+      #dataclass.where(uuid: attrvalue).each do |item|
+      #  items.append({name: item.uuid, uuid: item.uuid, type: dataclass.to_s})
+      #end
+      dataclass.limit(10).each do |item|
+        items.append({name: item.uuid, uuid: item.uuid, type: dataclass.to_s})
+      end
+    end
+
+    lt = link_to attrvalue, '#', {
+      "data-emptytext" => "none",
+      "data-placement" => "bottom",
+      "data-type" => datatype,
+      "data-url" => url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore),
+      "data-title" => "Update #{subattr[-1].to_s.titleize}",
+      "data-name" => dn,
+      "data-pk" => "{id: \"#{object.uuid}\", key: \"#{object.class.to_s.underscore}\"}",
+      "data-showbuttons" => "false",
+      "data-value" => attrvalue,
+      :class => "editable #{'required' if required}",
+      :id => id
+    }.merge(htmloptions)
+
+    lt += raw('<script>')
+    
+    if items and items.length > 0
+      lt += raw("add_form_selection_sources(#{items.to_json});\n")
+    end
+
+    lt += raw("$('##{id}').editable({source: function() { return select_form_sources('#{dataclass}'); } });\n")
+
+    lt += raw("</script>")
+
+    lt 
+  end
 end
index b2eee48ea695bdec16988c2d1d9126ed66ba75e8..df0ba22eac92f411ed58a268ccfd5b6a4cb2af8d 100644 (file)
@@ -4,4 +4,8 @@ module CollectionsHelper
       {source: x.tail_uuid, target: x.head_uuid, type: x.name}
     end
   end
+
+  def self.match(uuid)
+    /^([a-f0-9]{32}(\+[0-9]+)?)(\+.*)?$/.match(uuid.to_s)
+  end
 end
index 348004620e6747f3466fb33d6e1a5a089477af09..3a923e1e007e320ff3804290ccec5f58c9e85bf5 100644 (file)
@@ -43,9 +43,19 @@ module PipelineInstancesHelper
 
   def render_pipeline_job pj
     if pj[:percent_done]
-      pj[:progress_bar] = raw("<div class=\"progress\" style=\"width:100px\"><span class=\"progress-bar progress-bar-success\" style=\"width:#{pj[:percent_done]}%\"></span><span class=\"progress-bar\" style=\"width:#{pj[:percent_running]}%\"></span></div>")
+      pj[:progress_bar] = raw <<EOF
+<div class="progress" style="width:100px">
+  <span class="progress-bar progress-bar-success" style="width:#{pj[:percent_done]}%"></span>
+  <span class="progress-bar" style="width:#{pj[:percent_running]}%"></span>
+</div>
+EOF
     elsif pj[:progress]
-      raw("<div class=\"progress\" style=\"width:100px\"><span class=\"progress-bar\" style=\"width:#{pj[:progress]*100}%\"></span></div>")
+      raw <<EOF
+<div class="progress" style="width:100px">
+<span class="progress-bar" style="width:#{pj[:progress]*100}%">
+</span>
+</div>
+EOF
     end
     pj[:output_link] = link_to_if_arvados_object pj[:output]
     pj[:job_link] = link_to_if_arvados_object pj[:job][:uuid]
index 6d6ae5516c838ebdfe0556948ee980e8f6782c2b..8278d37f3ec022525bdfc42b0f2103bdf080e078 100644 (file)
@@ -9,7 +9,7 @@ module ProvenanceHelper
     end
 
     def self.collection_uuid(uuid)
-      m = /^([a-f0-9]{32}(\+[0-9]+)?)(\+.*)?$/.match(uuid.to_s)
+      m = CollectionsHelper.match(uuid)
       if m
         #if m[2]
         return m[1]
index 72b76a522982d8e04b595b4ecda9dc0a01523504..fbf7ee5e799e48a07c831d553733b8c56bca9855 100644 (file)
@@ -61,13 +61,16 @@ class ArvadosBase < ActiveRecord::Base
     attr_reader :kind
     @columns
   end
+
   def self.column(name, sql_type = nil, default = nil, null = true)
     ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
   end
+
   def self.attribute_info
     self.columns
     @attribute_info
   end
+
   def self.find(uuid, opts={})
     if uuid.class != String or uuid.length < 27 then
       raise 'argument to find() must be a uuid string. Acceptable formats: warehouse locator or string with format xxxxx-xxxxx-xxxxxxxxxxxxxxx'
@@ -84,21 +87,27 @@ class ArvadosBase < ActiveRecord::Base
     end
     new.private_reload(hash)
   end
+
   def self.order(*args)
     ArvadosResourceList.new(self).order(*args)
   end
+
   def self.where(*args)
     ArvadosResourceList.new(self).where(*args)
   end
+
   def self.limit(*args)
     ArvadosResourceList.new(self).limit(*args)
   end
+
   def self.eager(*args)
     ArvadosResourceList.new(self).eager(*args)
   end
+
   def self.all(*args)
     ArvadosResourceList.new(self).all(*args)
   end
+
   def save
     obdata = {}
     self.class.columns.each do |col|
@@ -128,8 +137,11 @@ class ArvadosBase < ActiveRecord::Base
       end
     end
 
+    @new_record = false
+
     self
   end
+
   def save!
     self.save or raise Exception.new("Save failed")
   end
@@ -169,6 +181,7 @@ class ArvadosBase < ActiveRecord::Base
     @links = $arvados_api_client.api Link, '', { _method: 'GET', where: o, eager: true }
     @links = $arvados_api_client.unpack_api_response(@links)
   end
+
   def all_links
     return @all_links if @all_links
     res = $arvados_api_client.api Link, '', {
@@ -181,9 +194,11 @@ class ArvadosBase < ActiveRecord::Base
     }
     @all_links = $arvados_api_client.unpack_api_response(res)
   end
+
   def reload
     private_reload(self.uuid)
   end
+
   def private_reload(uuid_or_hash)
     raise "No such object" if !uuid_or_hash
     if uuid_or_hash.is_a? Hash
@@ -206,8 +221,14 @@ class ArvadosBase < ActiveRecord::Base
       end
     end
     @all_links = nil
+    @new_record = false
     self
   end
+
+  def to_param
+    uuid
+  end
+
   def dup
     super.forget_uuid!
   end
@@ -275,6 +296,10 @@ class ArvadosBase < ActiveRecord::Base
     (name if self.respond_to? :name) || uuid
   end
 
+  def selection_label
+    friendly_link_name
+  end
+
   protected
 
   def forget_uuid!
index bda5523d8cfdd9192aefc7923b9a7ba350f05e4e..e22a5f72f4a3a062c0fc9b5cc7fa8bb287ba7fe8 100644 (file)
@@ -24,4 +24,23 @@ class Collection < ArvadosBase
   def used_by
     $arvados_api_client.api "collections/#{self.uuid}/", "used_by"
   end
+
+  # def selection_label
+  #   name = ''
+  #   i = 0 
+  #   if self.files.length > 3
+  #     m = 3
+  #   else
+  #     m = self.files.length 
+  #   end
+  #   while i < m
+  #     name += "#{self.files[i][1]}"
+  #     i += 1
+  #     name += ", " if i < m
+  #   end
+  #   if i < self.files.length
+  #     name += "&ellip;"
+  #   end
+  #   name
+  # end
 end
index da6116e91626ad2590ec042e6256c92c7455aa22..ccb88351a761be86abeda65dec7346b4de76c145 100644 (file)
@@ -16,9 +16,9 @@ class PipelineInstance < ArvadosBase
       end
     end
   end
-
+  
   def attribute_editable?(attr)
-    attr == 'name'
+    attr.to_sym == :name || (attr.to_sym == :components and self.active == nil)
   end
 
   def attributes_for_display
diff --git a/apps/workbench/app/views/application/_selection_checkbox.html.erb b/apps/workbench/app/views/application/_selection_checkbox.html.erb
new file mode 100644 (file)
index 0000000..4d47d89
--- /dev/null
@@ -0,0 +1,8 @@
+<%if object %>
+<%= check_box_tag 'uuids[]', object.uuid, false, {
+  :class => 'persistent-selection', 
+  :friendly_type => object.class.name,
+  :friendly_name => object.selection_label,
+  :href => "#{url_for controller: object.class.name.tableize, action: 'show', id: object.uuid }" 
+} %>
+<% end %>
index c58c628ee9f557e6517a0f7dc152674782c1b098..ef4a8d1f041b5c91f00ea5e947059377c4aad185 100644 (file)
@@ -8,9 +8,12 @@
 
 <% attr_blacklist = ' created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at' %>
 
+<%= form_tag do |f| %>
+
 <table class="table table-condensed arv-index">
   <thead>
     <tr>
+      <th></th>
       <% @objects.first.attributes_for_display.each do |attr, attrvalue| %>
       <% next if attr_blacklist.index(" "+attr) %>
       <th class="arv-attr-<%= attr %>">
   <tbody>
     <% @objects.each do |object| %>
     <tr data-object-uuid="<%= object.uuid %>">
+      <td>
+        <%= render :partial => "selection_checkbox", :locals => {:object => object} %>
+      </td>
+
       <% object.attributes_for_display.each do |attr, attrvalue| %>
       <% next if attr_blacklist.index(" "+attr) %>
       <td class="arv-object-<%= object.class.to_s %> arv-attr-<%= attr %>">
@@ -55,3 +62,5 @@
 </table>
 
 <% end %>
+
+<% end %>
index eb9c93fbc375eded213e77f3b56eeefd1f44c255..96b73979eb60a645b69a9be0d67c4fa91d553617 100644 (file)
@@ -1,6 +1,9 @@
 <% @collections.each do |c| %>
 
 <tr class="collection" data-object-uuid="<%= c.uuid %>">
+  <td>
+    <%= render :partial => "selection_checkbox", :locals => {:object => c} %>
+  </td>
   <td>
     <%= link_to_if_arvados_object c.uuid %>
   </td>
index 385af8a27205c98be3cb43974cb0bbde5c9740ce..15daaf7e2ddac111f205dc566c399867fe671eda 100644 (file)
@@ -1,5 +1,13 @@
+<% content_for :css do %>
+.file-list-inline-image {
+  width: 50%;
+  height: auto; 
+}
+<% end %>
+
 <table class="table table-condensed table-fixedlayout">
   <colgroup>
+    <col width="4%" />
     <col width="35%" />
     <col width="40%" />
     <col width="15%" />
@@ -7,6 +15,7 @@
   </colgroup>
   <thead>
     <tr>
+      <th></th>
       <th>path</th>
       <th>file</th>
       <th style="text-align:right">size</th>
     <% if @object then @object.files.sort_by{|f|[f[0],f[1]]}.each do |file| %>
     <% file_path = "#{file[0]}/#{file[1]}" %>
     <tr>
+      <td>
+        <% fp2 = file_path[2..-1] if file_path[0..1] == './' %>
+        <% fp2 ||= file_path %>
+<%= check_box_tag 'uuids[]', @object.uuid+file_path, false, {
+  :class => 'persistent-selection', 
+  :friendly_type => "File",
+  :friendly_name => "#{@object.uuid}/#{fp2}",
+  :href => "#{url_for controller: 'collections', action: 'show', id: @object.uuid }/#{file_path}" 
+      } %>
+      </td>
       <td>
         <%= file[0] %>
       </td>
 
       <td>
-        <%= link_to file[1], {controller: 'collections', action: 'show_file', uuid: @object.uuid, file: file_path, size: file[2], disposition: 'inline'}, {title: 'View in browser'} %>
+        <%= link_to((if /\.(jpg|jpeg|gif|png|svg)$/i.match(file[1]) then
+                       image_tag "#{url_for @object}/#{file_path}", class: "file-list-inline-image"
+                     else
+                       file[1]
+                     end), 
+                    {controller: 'collections', action: 'show_file', uuid: @object.uuid, file: file_path, size: file[2], disposition: 'inline'}, 
+                    {title: file_path}) %>
       </td>
 
       <td style="text-align:right">
index 3cedb57e85c8a854efabad7bde2dc294a7d75746..a3b93d84e67aa9739b95592543bade636f4e0aaf 100644 (file)
 
 <div style="padding-right: 1em">
 
+<%= form_tag do |f| %>
+
 <table id="collections-index" class="topalign table table-condensed table-fixedlayout table-fixed-header-row">
   <colgroup>
+    <col width="4%" />
     <col width="10%" />
     <col width="36%" />
     <col width="15%" />
@@ -26,6 +29,7 @@
   </colgroup>
   <thead>
     <tr class="contain-align-left">
+      <th></th>
       <th>uuid</th>
       <th>contents</th>
       <th>owner</th>
@@ -38,6 +42,9 @@
     <%= render partial: 'index_tbody' %>
   </tbody>
 </table>
+
+<% end %>
+
 </div>
 
 <% content_for :footer_js do %>
index 1dc6284c83c93daa0874eddf62eacf6727c59b0c..68f0e109bd3a125a5087f808c93e701812785464 100644 (file)
     padding-top: 1.25em;
     }
 
-    /* Setting the height needs to be fixed with javascript. */
-    .dropdown-menu {
-    padding-right: 20px;
-    max-height: 440px;
-    width: 400px;
-    overflow-y: auto;
-    }
-
     @media (min-width: 768px) {
     .left-nav {
     position: fixed;
               <span class="glyphicon glyphicon-arrow-right"></span>
             </li>
             <li>
-<%= link_to controller.breadcrumb_page_name, request.fullpath %>
+              <%= link_to controller.breadcrumb_page_name, request.fullpath %>
+            </li>
+            <li style="padding: 14px 0 14px">
+              <%= form_tag do |f| %>
+                <%= render :partial => "selection_checkbox", :locals => {:object => @object} %>
+              <% end %>
             </li>
           <% end %>
         <% end %>
         </li>
         -->
 
-        <!-- XXX placeholder for this when persistent selection is implemented
-        <li class="dropdown">
+        <li class="dropdown notification-menu">
           <a href="#" class="dropdown-toggle" data-toggle="dropdown">
             <span class="glyphicon glyphicon-paperclip"></span>
-            <span class="badge badge-alert"><%= @selection_count %></span>
+            <span class="badge" id="persistent-selection-count"></span>
             <span class="caret"></span>
           </a>
-          <ul class="dropdown-menu" role="menu">
-              <li style="padding: 10px">No selections.</li>
+          <ul class="dropdown-menu" role="menu" id="persistent-selection-list">
           </ul>
         </li>
-        -->
 
         <% if current_user.is_active %>
         <li class="dropdown notification-menu">
index 9597964103c792bc2677771d9344e28fd05ba132..24579c9b4b4c74bda726f13cec1455c78a9b4a1f 100644 (file)
@@ -3,31 +3,54 @@
     padding-left: 1em;
     padding-right: 1em;
   }
+table.pipeline-components-table {
+  width: 100%;
+  table-layout: fixed;
+  overflow: hidden;
+}
+
 table.pipeline-components-table thead th {
   text-align: bottom;
 }
 table.pipeline-components-table div.progress {
   margin-bottom: 0;
 }
+
+table.pipeline-components-table td {
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+td.required {
+  background: #ffdddd;
+}
+
 <% end %>
-<br />
 
+<% template = PipelineTemplate.find(@object.pipeline_template_uuid) %>
+<% if template %>
+  <h2><%= template.name %></h2>
+<% end %>
+
+<% if @object.active != nil %>
 <table class="table pipeline-components-table">
   <colgroup>
-    <col width="15%" />
-    <col width="15%" />
-    <col width="35%" />
-    <col width="35%" />
+    <col style="width: 15%" />
+    <col style="width: 20%" />
+    <col style="width: 12%" />
+    <col style="width: 8%" />
+    <col style="width: 45%" />
   </colgroup>
   <thead>
     <tr>
       <th>
         component
+      </th><th>
+        script, version
       </th><th>
         progress
         <%= link_to '(refresh)', request.fullpath, class: 'refresh', remote: true, method: 'get' %>
       </th><th>
-        script, version
       </th><th>
         output
       </th>
@@ -37,23 +60,38 @@ table.pipeline-components-table div.progress {
     <% render_pipeline_jobs.each do |pj| %>
     <tr>
       <td>
+        <% label = if pj[:job].andand[:uuid] 
+             if pj[:job][:success] == true
+               'label-success'
+             elsif pj[:job][:success] == false
+               'label-danger'
+             elsif pj[:job][:running] == true
+               'label-info'
+             else
+               'label-default'
+             end
+           else
+             'label-default'
+         end %>
         <% if pj[:job].andand[:uuid] %>
-        <%= link_to pj[:name], job_url(id: pj[:job][:uuid]) %>
+        <%= link_to pj[:name], job_url(id: pj[:job][:uuid]), class: "label #{label}" %>
         <% else %>
-        <%= pj[:name] %>
+          <span class="label <%= label %>"><%= pj[:name] %></span>
         <% end %>
+
+      </td><td>
+        <%= pj[:script] %>
+        <br /><span class="deemphasize"><%= pj[:script_version] %></span>
       </td><td>
         <%= pj[:progress_bar] %>
+      </td><td>
         <% if pj[:job].andand[:cancelled_at] %>
-        <span class="pull-right label label-warning">cancelled</span>
+        <span class="label label-warning">cancelled</span>
         <% elsif pj[:failed] %>
-        <span class="pull-right label label-warning">failed</span>
+        <span class="label label-danger">failed</span>
         <% elsif pj[:result] == 'queued' %>
-        <span class="pull-right label">queued</span>
+        <span class="label">queued</span>
         <% end %>
-      </td><td>
-        <%= pj[:script] %>
-        <br /><span class="deemphasize"><%= pj[:script_version] %></span>
       </td><td>
         <%= link_to_if_arvados_object pj[:output] %>
       </td>
@@ -67,8 +105,67 @@ table.pipeline-components-table div.progress {
 
 <% if @object.active %>
 <% content_for :js do %>
-setInterval(function(){$('a.refresh').click()}, 30000);
+setInterval(function(){$('a.refresh').click()}, 15000);
 <% end %>
 <% end %>
 
-<pre><%= JSON.pretty_generate @object.attributes %></pre>
+<% else %>
+
+  <%= form_tag @object, :method => :put do |f| %>
+
+  <%= hidden_field @object.class.to_s.underscore.singularize.to_sym, :active, :value => true %>
+  <%= button_tag "Run pipeline", {class: 'btn btn-primary pull-right', id: "run-pipeline-button"} %>
+  <% end %>
+
+<table class="table pipeline-components-table" style="margin-top: -.1em">
+  <colgroup>
+    <col style="width: 15%" />
+    <col style="width: 20%" />
+    <col style="width: 20%" />
+    <col style="width: 45%" />
+  </colgroup>
+
+  <thead>
+    <tr>
+      <th>
+        component
+      </th><th>
+        script
+      </th><th>
+        parameter
+      </th><th>
+        value
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <% template.components.each do |k, template_value| %>
+
+    <tr>
+      <td><span class="label label-default"><%= k %></span></td>
+
+      <td><%= render_editable_subattribute @object, :components, [k, :script], template_value[:script] %></td>
+
+      <td>script version</td>
+
+      <td>
+        <%= render_editable_subattribute @object, :components, [k, :script_version], template_value[:script_version] %>
+      </td>
+    </tr>
+
+    <% if template_value[:script_parameters].length > 0 %>
+      <% template_value[:script_parameters].each do |p, tv| %>
+        <tr>
+          <td style="border-top: none"></td>
+          <td style="border-top: none"></td>
+          
+          <td class="property-edit-row"><%= p %></td>
+          <td class="property-edit-row"><%= render_editable_subattribute @object, :components, [k, :script_parameters, p.to_sym], tv %></td>
+      <% end %>
+      </tr>
+    <% end %>
+  <% end %>
+  </tbody>
+  </table>
+  
+<% end %>
index bfb3dc840e3bc9e8647e1d4cff40eeaa7b1ef027..ed5b2d086a83a843ac96fa38bd364ecc2ba7f958 100644 (file)
@@ -39,7 +39,7 @@
 
     <tr data-object-uuid="<%= ob.uuid %>">
       <td>
-        <%= check_box_tag 'uuids[]', ob.uuid, false %>
+        <%= check_box_tag 'uuids[]', ob.uuid, false, :class => 'persistent-selection' %>
       </td><td>
         <% if ob.success %>
         <span class="label label-success">success</span>
       </td>
     </tr>
     <tr>
-      <td style="border-top: 0;" colspan="3">
+      <td style="border-top: 0;" colspan="2">
       </td>
-      <td style="border-top: 0; opacity: 0.5;" colspan="4">
+      <td style="border-top: 0; opacity: 0.5;" colspan="5">
         <% ob.components.each do |cname, c| %>
         <% status = if !(c.is_a?(Hash) && c[:job].is_a?(Hash))
                       nil
-                    elsif c[:job][:success]
+                    elsif c[:job][:success] == true
                       'success'
-                    elsif c[:job][:running]
+                    elsif c[:job][:success] == false
+                      'danger'
+                    elsif c[:job][:running] == true
                       'info'
                     else
                       'warning'
index c389aa0a481194a41f23823572886ffc9b0ce9c4..f878f5980664c4b775d7386bc7c437832864b1d9 100644 (file)
@@ -1,7 +1,22 @@
+<% content_for :css do %>
+  .playbutton {
+  color: white;
+  background: gray;
+  border: 0px;
+  border-radius: 3px;
+  padding: 0px 3px;
+  }
+  .playbutton:hover {
+  color: white;
+  background: blackh;
+  }
+<% end %>
+
 <table class="table table-hover">
   <thead>
     <tr class="contain-align-left">
       <th>
+      </th><th>
        id
       </th><th>
        name
     <% @objects.sort_by { |ob| ob[:created_at] }.reverse.each do |ob| %>
 
     <tr>
+      <td>
+        <%= form_tag '/pipeline_instances' do |f| %>
+          <%= hidden_field :pipeline_instance, :pipeline_template_uuid, :value => ob.uuid %>
+          <%= button_tag nil, {class: 'playbutton'} do %>
+            <span class="glyphicon glyphicon-play"></span>
+          <% end %>
+        <% end %>
+      </td>
       <td>
         <%= link_to_if_arvados_object ob %>
       </td><td>